chore: initialisation du projet ul_tresorerie et ajout du sous-module documentation Nextcloud

This commit is contained in:
Florian Morget 2026-11-02 20:09:18 +01:00
commit f3ff8f5f74
22 changed files with 12249 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Exports CoGétise et fichiers de données sensibles
*.asp
*.csv
# Archives de packaging
*.zip
*.tar.gz
# Dépendances et outils de build éventuels
vendor/
node_modules/
# Fichiers système et éditeurs
.DS_Store
Thumbs.db
Desktop.ini
.idea/
.vscode/
*.swp
*.swo
*~
# Logs et fichiers temporaires
*.log
tmp/
temp/

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "documentation"]
path = documentation
url = https://github.com/nextcloud/documentation.git

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# Cotisations-Nextcloud — Trésorerie UL CGT (`ul_tresorerie`)
Application native **Nextcloud** (versions 33 à 35) conçue pour l'**Union Locale CGT de Foix et de la Haute-Ariège** permettant le suivi rapide des cotisations mensuelles des syndiqués isolés (*INDIVIDUELS UL FOIX HAUTE-ARIEGE* / *Multipros*), l'import depuis CoGétise (`.asp`) et le publipostage intégré.
---
## Fonctionnalités principales
- **Grille annuelle de saisie rapide** : Suivi mois par mois (Janvier à Décembre) des cotisations par adhérent, filtrage en temps réel, édition groupée et totaux mensuels/annuels.
- **Système d'alertes à deux niveaux** :
- **Niveau 1 (Orange)** : Retard modéré (configurable, par défaut 3 mois impayés).
- **Niveau 2 (Rouge)** : Retard critique (configurable, par défaut 6 mois impayés).
- **Import CoGétise (`.asp`)** : Importation et synchronisation des syndiqués depuis un fichier d'export tabulé CoGétise avec détection d'encodage (UTF-8 / Windows-1252) et aperçu des créations/mises à jour.
- **Publipostage & Reçus fiscaux (66 %)** :
- Génération de **reçus fiscaux** (déduction fiscale de 66 % des cotisations syndicales versées sur l'année).
- Lettres de **rappel de mois manquants** et de **rappel annuel**.
- Modèles personnalisables avec variables dynamiques et impression directe.
- **Contrôle d'accès** : Gestion des administrateurs, utilisateurs et groupes Nextcloud autorisés à consulter et modifier la trésorerie.
---
## Structure du dépôt
```text
Cotisations-Nextcloud/
├── documentation/ # Sous-module Git officiel Nextcloud Documentation
├── ul_tresorerie/ # Code source de l'application Nextcloud
│ ├── appinfo/ # Métadonnées (info.xml) et routes (routes.php)
│ ├── css/ # Feuilles de style de l'interface
│ ├── img/ # Logos et bannières UL CGT
│ ├── js/ # Logique front-end de l'application
│ ├── lib/ # Contrôleurs, Services et Migrations PHP
│ └── templates/ # Template principal Nextcloud (main.php)
├── .gitignore # Exclusion des exports sensibles (*.asp) et archives (*.zip)
├── .gitmodules # Configuration du sous-module de documentation Nextcloud
└── README.md # Documentation du projet
```
---
## Gestion de la documentation officielle Nextcloud
La documentation Nextcloud est gérée directement depuis le dépôt officiel (`https://github.com/nextcloud/documentation.git`) via un **sous-module Git** situé dans le dossier `documentation/`.
### Cloner le projet avec la documentation
```bash
git clone --recurse-submodules <url-du-depot>
```
Si le dépôt a déjà été cloné sans l'option `--recurse-submodules` :
```bash
git submodule update --init --recursive
```
### Mettre à jour la documentation depuis le dépôt officiel Nextcloud
```bash
git submodule update --remote documentation
```
---
## Installation de l'application dans Nextcloud
### Prérequis
- **Nextcloud** : 33 à 35
- **PHP** : 8.3 minimum
### Déploiement
1. Copier ou lier le dossier `ul_tresorerie` dans le répertoire `custom_apps/` (ou `apps/`) de votre instance Nextcloud :
```bash
cp -r ul_tresorerie /var/www/nextcloud/custom_apps/
chown -R www-data:www-data /var/www/nextcloud/custom_apps/ul_tresorerie
```
2. Activer l'application via `occ` ou depuis l'interface d'administration Nextcloud :
```bash
sudo -u www-data php /var/www/nextcloud/occ app:enable ul_tresorerie
```
---
## Licence
Distribué sous licence **AGPL-3.0-or-later**.

1
documentation Submodule

@ -0,0 +1 @@
Subproject commit d89f524273c99edd5e1034422fa33228fde47646

View File

@ -0,0 +1,27 @@
<?xml version="1.0"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>ul_tresorerie</id>
<name>Trésorerie UL CGT</name>
<summary>Suivi des cotisations mensuelles et publipostage — UL CGT Foix Haute-Ariège</summary>
<description><![CDATA[Application native de suivi rapide des cotisations mensuelles des syndiqués isolés (INDIVIDUELS UL FOIX HAUTE-ARIEGE / Multipros), avec système d'alertes à deux niveaux, import CoGétise (.asp) et outil de publipostage intégré (reçus fiscaux 66 %, rappels de mois manquants, rappels annuels).]]></description>
<version>1.0.0</version>
<licence>AGPL-3.0-or-later</licence>
<author>Union Locale CGT de Foix et de la Haute-Ariège</author>
<namespace>ULTresorerie</namespace>
<category>organization</category>
<category>office</category>
<bugs>https://github.com/ul-cgt-foix/ul_tresorerie/issues</bugs>
<dependencies>
<php min-version="8.3"/>
<nextcloud min-version="33" max-version="35"/>
</dependencies>
<navigations>
<navigation>
<name>Trésorerie UL CGT</name>
<route>ul_tresorerie.page.index</route>
<icon>app.svg</icon>
<order>10</order>
</navigation>
</navigations>
</info>

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'api#getState', 'url' => '/api/state', 'verb' => 'GET'],
['name' => 'api#createMember', 'url' => '/api/members', 'verb' => 'POST'],
['name' => 'api#updateMember', 'url' => '/api/members/{id}', 'verb' => 'PUT'],
['name' => 'api#deleteMember', 'url' => '/api/members/{id}', 'verb' => 'DELETE'],
['name' => 'api#importAsp', 'url' => '/api/import', 'verb' => 'POST'],
['name' => 'api#saveDue', 'url' => '/api/dues', 'verb' => 'POST'],
['name' => 'api#deleteDue', 'url' => '/api/dues/delete', 'verb' => 'POST'],
['name' => 'api#saveTemplate', 'url' => '/api/templates/{slug}', 'verb' => 'PUT'],
['name' => 'api#resetTemplates', 'url' => '/api/templates/reset', 'verb' => 'POST'],
['name' => 'api#sendEmails', 'url' => '/api/mailing/send', 'verb' => 'POST'],
['name' => 'api#getPermissions', 'url' => '/api/permissions', 'verb' => 'GET'],
['name' => 'api#savePermissions', 'url' => '/api/permissions', 'verb' => 'PUT'],
],
];

1823
ul_tresorerie/css/app_v2.css Normal file

File diff suppressed because it is too large Load Diff

1823
ul_tresorerie/css/style.css Normal file

File diff suppressed because it is too large Load Diff

187
ul_tresorerie/img/app.svg Normal file
View File

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24" height="31" viewBox="0 0 612 792" xml:space="preserve">
<style type="text/css">
.st0{fill:#F03E3E;}
.st1{fill:#FFFFFF;}
.st2{fill:#FAD43A;}
.st3{fill:#FAB005;}
</style>
<rect x="9.8" y="6.3" class="st0" width="591.6" height="779.5"/>
<path class="st1" d="M9.8,533.9c9.8-53.7,47.7-94.8,113.3-94.8c25.7,0,69.4,7.1,98.6,55.3l-50.4,28.7c-4-8.4-8.4-16.4-19.5-24.3
c-6.6-4.4-15-8-25.6-8c-36.7,0-49.1,42-49.1,69.9c0,30.5,15,69,51.3,69c3.5,0,20.3,0,34.5-15.9c5.7-6.6,8-11.9,10.2-18.6l51.3,26.5
c-8.4,13.3-17.7,27.9-36.3,41.1c-17.7,12.4-40.2,20.8-66.3,20.8c-44.2,0-114.5-26.5-114.5-120.7C7.2,552.9,8.1,543.2,9.8,533.9"/>
<path class="st1" d="M380.4,645.6c-13.3,8.8-29.6,19.5-61,19.5c-19,0-37.6-4.9-52.6-15c-31-19.9-42.9-55.7-42.9-95.5
c0-46.4,21.7-113.2,98.2-113.2c35.4,0,51.7,15.5,61.9,25.2v-21.7h63.7v222c-0.9,25.2-1.8,54.8-35.4,74.3
c-23.4,14.2-57.9,17.7-78.7,17.7c-46,0-76.1-15.9-88.4-28.7c-18.1-18.1-17.7-41.6-17.7-53.9h70.3c0.9,10.2,1.8,24.8,19.9,33.2
c4.4,2.2,11.1,4.4,21.7,4.4c41.1,0,41.1-24.3,41.1-43.3V645.6z M294.2,554c0,19.5,1.3,59.7,43.3,59.7c34.5,0,42.5-27,42.5-59.7
c0-11.1-0.9-34-8.4-45.1c-5.3-8.4-17.7-17.7-37.1-16.8C302.2,493.5,294.2,521.3,294.2,554z"/>
<path class="st1" d="M494.9,375.8h67.7v69h42.2v47.3h-42.6V596c-0.4,14.2-0.4,26.1,17.7,26.1l24.3-0.9v53.1
c-14.6,2.2-23.4,3.5-35.8,3.5c-23.4,0-49.1-1.3-63.7-22.6c-9.3-13.7-9.7-30.1-9.7-52.2V491.7h-33.6v-46.9h33.6V375.8z"/>
<g>
<path class="st2" d="M252.5,260C252.5,260,252.5,260.1,252.5,260C252.7,259.9,252.6,259.9,252.5,260z"/>
<path class="st2" d="M250.8,262.1C250.8,262.4,251.2,262.1,250.8,262.1L250.8,262.1z"/>
<path class="st2" d="M41.1,417.7C41.1,417.7,41.1,417.7,41.1,417.7C41,417.5,41,417.5,41.1,417.7z"/>
<path class="st2" d="M452.6,424.5L452.6,424.5C452.5,424.5,452.5,424.5,452.6,424.5z"/>
<path class="st2" d="M453.2,424.5C453.2,424.5,453.2,424.5,453.2,424.5C453.3,424.5,453.3,424.5,453.2,424.5z"/>
<path class="st3" d="M601.4,390.8c-0.7,0-1.4,0.1-2.1,0.2c-0.2,0-0.5,0.9-0.6,1c-1,0.4-5,0.9-5.5,1c-2.5,0.2-4.7-1-7.7-1
c0.2,1.4,0,2.1,1,3.5c-0.5-0.1-1.5,0.2-1.9,0c-0.6-0.3-0.7-2.4-2.9-1c-0.3,0.2-0.3,1.2-0.6,1.3c-0.7,0.2,0-0.1-0.3-0.3l-7.1-1
l-0.6-0.6c-0.6,0.1-0.7,0.2-1,0.3c-1.2,0.4-0.3,0.6-1,1.3h-0.6c-0.2-0.1,0-1.3,0-1.3c-1.1-0.3-1.9,1.1-2.6,1.3
c-1.1,0.3-2.3-0.5-4.2-0.3c-2.5,0.2-6,1.2-8,2.3c-0.5-0.4-1-0.8-1.6-1.3c-0.1,0.1-2.3,2.9-2.3,2.9c-0.6,0.3-1.4-0.5-0.6-0.3
c0-0.2,0.5-1.9,0-1.9c-0.7,0-0.7,0.9-1,1.3c-1.2-0.6-2.5-1.2-3.9,0c-0.2,0.1-0.1,0.9-0.3,1c-1.4,0.4,0.2-1.7-1.6-1.3
c-0.9,0.2-1.8,1.6-2.6,1.9c-1.3,0.6-2.4,0.6-3.9,1c0-0.1,0.6-0.9,0.3-1c-0.3-0.1-0.7,0.1-1,0c-0.6,0.7-0.7,1.3-1.3,2.2
c-0.7-0.1-1.6,0-2.3,0c0-0.6,0-1.3,0-1.9c1.1,0-0.7-0.5-1-0.6c-0.7,1.5-0.6,1.3-0.3,2.9c-0.2,0-0.8,0.1-1,0c0,0-1.8,0.3-2.9,0.3
c1.1-0.7,0.4-0.5,0.6-1.3l1-0.6c-0.1,0-1.1-0.6-1.3-0.6c-0.4,1.3-1,3.4-3.5,3.2c-2.9-0.3,0.2-2.5-3.9-0.6v1.9
c-0.1,0-4.5-0.6-4.5-0.6c-0.3-0.3-0.2-0.9-0.6-1.3c-0.6,0.4-1.5,2-2.3,2.3c-2.4,0.7-3.8-0.6-5.8,0c-0.1,0,0.3,0.5,0.3,0.6
c-0.9,0.3-1.7,0.9-2.9,0.6c-0.5-0.1,0-1.3,0-1.3c-0.5-0.2-3.1-0.3-3.2-0.3c0,0.1,0,0.2,0,0.3c0,0-1.8,0.8-2.9,1l-0.3,1.6
c-0.9,0.5-2.6-1-3.9-1c-0.5,0-1,1-1,1c-4.2,1.2-7.4,0.8-11.6,1.3c-0.9,0.1-3.8,1.5-4.8,1.6c-2,0.2-4.4,0.4-5.5,0
c-0.7-0.3-0.4-1.4-2.3-1.3c-1.4,0.1-2.6,1.7-3.5,1.9c-1.5,0.4-3.9,0.6-4.8,0.6c-2,0.2-3-0.5-4.2-1.3c-0.1,0.1-0.3,1.6-0.3,1.6
c-0.8,0.2-1.8-1.2-3.2-1c0,0-0.8,0.9-1,1c-1.5,0.4-4.2,0.6-5.5,0.6c-2.9,0-5.7-0.6-8.7-0.3c-3.4,0.4-5.1,1.7-8.4,1.9
c-0.9,0-3.9-0.8-5.5-0.6c-1.1,0.1-2.2,1.8-3.2,1.9c-2.3,0.4-1.8-1.3-3.9-1c-0.4,0-0.2,0.9-0.3,1.3c-2.5-0.2-7.3,1.3-8.7-1
c-0.2,0.1-0.3,1.1,0,1.3c-2.7,0-4.6,0.8-6.8,0.6c-1.5-0.1-3.3-0.1-5.5-0.6c-0.5-0.1-1.1-1.2-1.3-1.3c-2.1-0.5-4.6,0.9-6.8,1
c-0.1,0-3.2,0-3.2,0s-0.5-1.5-1-1.6c-1.1-0.3-2.3,0.7-3.2,0.6c-0.5,0-1.1-0.9-1.3-1c-3.4-0.6-2.2,1.8-4.5,1.6c0,0-3.7-1.6-1.9-1.3
c0-0.1-0.6-0.8-0.3-1c-1.3,0.2-1.9,0.1-3.2,0.3c0,0.1,0.3,1.6,0.3,1.6c-0.8,0.1-2.6-1.3-2.6-1.3c-1.9-0.4-0.3,0.7-1.9,0.6
c-2-0.1-3.8-2-5.1-2.9c-0.6,0.4-0.8,0.5-1.6,1c-0.5-1.2-1.1-2.2-1.6-3.2c-2,0.5-3.5-0.4-5.8-0.6c-0.1-0.4,0-1.2-0.3-1.3
c-1.4-0.3-1.1,0.9-2.6,0.3c-0.4-0.2-1.3-1.8-1.6-1.9c-0.6-0.2-1.3,0.5-2.3,0.3c-1-0.2-0.5-1.2-0.6-1.3c-0.4-0.2-1.2,0.2-1.6,0
c-0.1-0.1,0-1.2-0.3-1.3c-0.3-0.1-1.5,0.8-2.6-0.6c-0.2-0.3-0.2-0.5-0.3-1c0,0-0.3-0.8-0.6-1.3c0.2,0.4,0.6-0.8,0-1.3
c-1-0.9-1.7-0.3-2.9-0.3c0.3-0.2-0.3-0.8-0.3-1c0.3-0.1,0.6,0,1,0c-0.5-2.3-1.2-2.1-1.6-2.9c-1.1-2.3-1.4-5.8-1.6-8
c-0.6,0-1.3-0.2-1.9-0.3c0.4-1.6,0.6-2,1.6-2.9c-0.5-1.5-2.7-4.4-2.3-6.1c0.2-0.9,1-0.8,1.6-1.3c-1-2.2-1-2.8,0.6-3.9
c-0.1-0.3-0.8-1.5-0.6-1.9c0-0.2,1.5-1.3,1.6-1.6c0.3-1.1-0.5-1.3-0.3-2.2c0.1-0.5,1-2.1,1.3-2.9c-0.3-0.3-2.3-2.9-2.3-2.9
c-0.6-0.2-1.7,0.3-2.2,0c-0.1,0,0-0.8,0-1c-0.5-1.3-2.2-1.1-3.5-1c-0.3-0.4-0.5-1.8-1-1.9c-0.9-0.3-0.3,2.8-0.3,2.9
c-2.5,0.4-3.7,0.6-5.2,1.9c-2.5,2.4-2.7,8.3-3.9,11.7l-2.9,3.4c-0.2,0.1-1,0.4-1.3,0.6c0.3,1.9,0.4,3-1,3.9c-2.9,1.8-2.5-0.8-2.9-1
c-0.4-0.1-0.9,0.2-1.3,0c0,0-0.5,1-0.6,1.3c0.5,0.4,0.7,0.1,1.3,0.3c-0.1,0.2-0.3,1-0.3,1c-0.3-0.2-1,0.1-1.3,0
c-0.1-0.1-0.2-1-0.3-1.3c-0.2,0.1-1.7,0.6-1.3,0.6c-0.6,0.6-1.6,2.2-2.3,2.6c-0.7,0.4-0.5-0.5-1.3-0.3c-1.5,0.3-2.5,3.5-2.9,5.1
c-0.5-0.2-1.4,0.2-1.9,0c-0.1,0,0-0.9,0-1c0,0-0.1-1.7-2.3-1.3c-0.3,0-1.3,1.2-1.6,1.3c-0.8,0.3-2.2-0.1-3.2,0.3
c-0.9,0.4-2,2.5-2.9,3.2c-1.5-2.8-2.2-1.5-3.9,0c-3.5-2.8-9.7,1.1-13.5-1.6c-1.1-0.7-1-4.2-1-4.2c0-0.2,0.1-1.3-0.3-1
c1.4-1.2,1.2-2.3,2.2-3.9c0.3-0.5,1.6-0.8,1.9-1.3c0.5-0.8,0.1-1.8,0.6-2.6c0.2-0.3,1.2-0.3,1.3-0.6c0.2-0.5-0.2-1.4,0-1.9
c0.3,0,1.1,0.4,1.6,0.6c1.4-2,2.9-2.4,4.8-3.9c1.2-0.9,2.1-2.6,3.2-3.5c1.7-1.5,3.8-2.3,5.8-3.5c1-0.7,1.9-2.3,2.9-2.9
c0.3-0.2,0.9,0.1,1.3,0c1.3-0.5,2.7-2.3,3.9-2.9c3.2-1.7,6.1-2,10-3.2c2.1-0.7,6.2-2.8,8.7-3.2c3-0.6,6.3-0.2,8.4-0.6
c0.8-0.2,1.6-1.2,2.9-1c0.5,0.1,0.7,1.2,1,1.3c0.5,0.1,1.6-0.7,2.3-0.6c1.2,0.1,2.8,1.7,3.9,1.9c0.7,0.2,1.6-0.4,2.6,0
c0,0.1,0,0.2,0,0.3c0.9,0.5,1.6,0.8,2.6,1.3c0,0.1-0.1,0.9,0,1c1.7,0.5,2.7-0.7,5.5,0.3c-0.2-0.1-1.1-1.2-1.3-1.3
c-0.3,0.5-0.3-0.6-0.3-0.6c0.3-0.3,1.5-1,1.6-1.3c0.3-1.2-0.6-4.3-1-5.8c-0.5-1.9-3.5-4.2-3.5-4.2c0-0.2,0.1-1.2,0-1.3
c-0.3-0.3-1.3,0.2-1.6,0c-0.5-0.2,0-1-0.3-1.3c-0.6-0.5-4.6-3.1-5.1-3.2c-2.5-0.8-4.2,0.2-7.1,0c-1.1-0.1-2.6-1.3-3.9-1.3
c-0.6,0-1.8,0.9-2.3,1c-3,0.3-5.9,0-9.3,1c-0.5,0.1-1.3,1.2-1.6,1.3c-1.5,0.5-2.8-0.2-4.2,0.3c-0.7,0.3-1.2,1.8-0.3,1.9
c-2.7,0.9-3.9,2-6.8,2.9c-0.1-0.5,0.1-1.1-0.3-1.3h-0.6c-0.4,1-1.8,4-2.9,4.2c-0.3,0.1-3.9-1.4-4.5-1.6c0.4,0.8,0.5,1.2,1,1.9
c-2.7,0.9-5.9,1.4-7.7,0.3c-0.3,2.7,0.3,3.8-0.3,6.4c-2.6-0.5-6.4,3-7.1-1.6c-0.3,1.7-0.6,2-2.6,2.6c-0.2,2.7-0.2,6.7-2.9,7.7
c-0.3,0.1-1-0.2-1.3,0c-1.6,1.4,0.4,1.7-2.3,1.3c-0.1,0.3-0.1,0.9-0.3,1c0,0,0.8,0.6,0.6,0.6c-1.7,0-1.1-1.3-1.9-1.9
c-0.4,0.2-1.2,0-1.6,0l-0.3,1.6c-1,0.4-6.9-0.2-7.7-0.3c-2.1-0.3-6.4,0.1-8.7,1c-0.2,0.1-0.5,0.9-0.6,1c-0.9,0.2-1.2-0.4-2.6,0
c0,0.1,0,0.2,0,0.3c-1,0.5-3.9,0.8-4.5,1.6c-0.2,0.3-0.1,1.1-0.3,1.3c-0.8,0.6-2,0.4-2.9,1c0,0-0.3,1-0.3,1
c-2.2,1.3-5.9,1.7-7.7,3.2c-0.4,0.4-0.7,1.4-1,1.9c-3.5-0.9-2.4,0.5-4.5,1.3c-1.4,0.5-2.9,0.1-4.5,1c-0.2,0.1-0.5,0.9-0.6,1
c-2.2,0.6-3.1,0.1-5.5,1l0.3,2.6l-1-0.3c0,0-0.3-1.8-0.3-1.9c0,0-1,0.3-1,0.3c-0.2,2.4,0.4,2.5-1.9,1.9c0.1,0.2,0.5,1.2,0.3,1.3
c-1.6,0.5-3.8-0.7-4.5-0.6c-0.6,0-0.6,0.8-1,1c-0.9,0.3-2-0.2-2.9,0c-0.4,0.1-1.7,1.2-2.2,1.3c-0.7,0.1-1.2-0.5-2.3-0.3
c-2.5,0.5-5.6,3-8.4,3.5c-1.5,0.3-3.1-0.4-5.2,0c-1.9,0.4-4.8,2.2-6.4,2.3c-1,0-1.1-0.8-2.3-0.6c-0.1,0-0.8,0.9-1,1
c-1.1,0.3-6.2,0.1-7.1,0c-0.2,0-1.1-1-1.3-1c-1.9-0.2-3,1-4.8,1c-0.3,0-1.6-0.9-1.9-1c-2.5-0.4-3.3,0.1-6.1-0.6c0-0.1,0.1-1,0-1
c-0.9-0.3-2.3,0.2-3.2,0c-2.2-0.4-4-2.6-5.5-3.9c-0.4-0.3-1.4-0.5-1.6-1c-0.5-0.9,0.1-1.9-0.3-2.9c-0.3-0.7-2.1-2-2.6-3.2
c0.5-0.4,0-1.6,0-1.6c0.3,0,0.3-3.2,0.3-3.2c1.3-2.8,0.2-4.4,2.6-5.5c-0.4-1.6-0.5-1.7,0-3.5c-0.4,0.1-0.5,0.5-1.3,0.3
c0.3-1,2.5-5.9,3.2-6.4c0.4-0.3,1.5,0,1.9-0.3l0.3-1.6c3-1,2.8-2.6,5.1-4.5c0.6-0.5,1.3-0.1,1.9-0.6c0.6-0.5,0.6-1.8,1.3-2.3
c1.1-0.7,2.3-0.4,3.2-1c1.1-0.7,1-2,1.9-2.9c1-1,3-1.1,4.5-1.6c0-0.7-0.2-1.6,0-2.3c1.2-0.4,2.9-0.4,3.2-0.6c0.1-0.1,0-1.1,0-1.3
c2-1.1,3.1-1.9,5.1-2.9c-0.1-0.2-0.3-1-0.3-1c1-1.3,3.2-1,1.6-2.3c-0.4-0.3-0.3-1-0.3-1c1.6-0.5,2.5-0.7,3.9-1.3
c0-0.1-0.4-0.6-0.3-0.6c1.3-0.5,2.7,0.3,4.2-1c0.9-0.7,1.3-2.3,2.3-2.9c0.3-0.2,1.1-0.1,1.3-0.3c0.5-0.6,0-1.5,0.6-1.9
c1.6-1.2,3.5-1.2,5.1-2.3c1.1-0.7,2-1.8,3.2-2.6c-0.1-0.2-0.3-1-0.3-1c0.8-0.8,1.8-0.3,2.9-1c0.4-0.2-0.3-0.6-0.3-0.6
c1.1-0.8,1.3-1,1.9-2.3c0.8,0,0.8,0.5,1.6,0c0,0-1-1.2,0.3-1.6c3.9-1.3-0.8,3.4-1.6,4.2c2.7-1.3,3.2-2.3,6.4-2.6
c1.2-2.8,1.6-4,3.5-5.5c-0.1-0.2-0.5-0.4-0.3-0.6c0.1-0.1,1.2-0.5,1.3-0.6c1.1-2.1,0.3-2.3,2.6-3.2c0,0-1.2-0.5-1-0.6
c0.4-0.3,1.2,0.2,1.6,0c0.1-0.1,0.3-1.5,0.3-1.6c1.3-0.2,1.7,0.2,2.6-0.6c0.3-0.3,0-1.2,0.3-1.6c0.3-0.4,1.4-0.5,1.6-1
c0.9-1.6,0.2-2.6,1.3-3.5c0.8-0.7,1.9-0.3,2.6-1c0.1-0.2-0.1-0.8,0-1c0.3-0.7,3.9-3.4,4.8-4.5c1,0.2,1.2,0.2,2.3,0.3
c0.1-0.5-0.2-1.4,0-1.9l0.3-0.3c2-0.7,1.7-2.4,2.6-2.9c0.9-0.5,2.5,0,2.9,0c0.7-1.8,1.2-2.2,2.9-2.9c-1.4-1.1,1.5-4.2,2.3-5.5
c0.7,0.5,0.7,0.5,1.6,1l0.6-1.3c0.5-2.1-0.1-1.9,1.9-2.6c-0.3-2.6,1.2-3.5,4.2-4.2c0.1-0.7-0.3-1.6,0-2.3c0.2-0.4,1.3-0.4,1.6-0.6
c0.8-0.7,2.3-4.5,2.3-4.5l-0.3-0.6c0.7-0.4,1.1-0.5,1.3-0.5c0-0.2,0.2-0.6,0.7-1.4c0,0,0.7-0.5,1-0.7c0-0.1,0-0.2,0-0.3
c0.4-0.4,2.2-0.8,2.3-1c0.7-1.4-0.2-2.2-0.3-2.9c0.2-0.1,0.8,0.1,1,0c0.1-0.1-0.1-0.8,0-1c0.6-1.2,2.6-2,3.5-2.3
c-0.2-0.6,0.2-0.6,0.3-1c-0.3-0.2-1.3-1.3-1.3-1.3c1.1,0.3,3.9-6,4.5-8c-0.6-0.3-0.7,0-1-0.6c1.8,0.1,4.2,0.2,3.5-2.6l-0.3-0.6
c0.6-0.7,2.8-1.6,3.2-2.3c1.5-2.3,0.4-7.7,0.3-10c-0.1-2,1-6.2,0.6-7.4c-0.2-0.5-1.5-1.3-1.6-1.6c-0.5-1.6,1-2.5-0.3-3.2
c-0.5-0.3-1.7-0.6-1.9-0.6c-0.3-2.5-0.3-3-2.3-3.2c0.5-1.3,0.5-1.6,1-2.6c-2.4-0.9-1.5-1.8-2.6-2.6c-0.6-0.4-0.5,0.4-1,0.3
c-0.5-0.1-1.4-1.2-1.9-1.3c-2.3-0.5-5.3-0.6-7.7-0.3c-2.9,0.4-7,2.2-10.3,3.5c-0.9,0.4-2.7,2.1-3.5,2.3c-1,0.2-1.4-0.7-2.6-0.3
c-1.7,0.5-4.9,6.9-6.8,8c-0.7,0.4-1.3-0.1-1.6,0c-1.2,0.4-1.1,2.1-1.9,2.9c-0.8,0.7-2,0.3-2.9,1c-0.6,0.4-0.8,1.5-1.3,1.9
c-0.7,0.7-1.8,0.6-2.6,1.3c-1.1,0.9-1.2,2.9-2.3,3.9c-1,0.8-2.2,0.8-3.2,1.6c-1.9,1.5-3.3,3.9-5.1,5.5c-0.4,0.3-1.4,0.6-1.6,1
c-0.2,0.3-0.1,1.6-0.6,2.3c-0.6,0.7-2.9,1.7-3.2,2.6c-0.2,0.4,0.2,1.3,0,1.6c-0.4,0.6-1-0.1-1.6,0.3c-0.2,0.1-0.2,1.4-0.3,1.6
c-0.8,1.2-3.2,1.9-4.2,2.9c-0.7,0.7-0.3,1.6-1,2.6c-0.5,0.7-1.2,0.1-1.9,0.6c-0.1,0.1,0.1,0.8,0,1c-0.4,1-3,1.6-3.5,2.6
c-0.5,0.8,0,1.2-0.3,1.6c-0.4,0.5-0.5,0.2-1.3,0.3c-0.7,2.2-3.2,3.9-1,5.1c-2.1,0.3-2.8,0.9-4.8,1c-0.6,1.3-0.5,2.5-1.3,3.5
c-0.7,0.8-1.4,0.3-2.3,1c-0.9,0.7-0.7,1.9-1.3,2.9c-1.2,1.9-3.7,3.6-4.8,5.5c-0.2,0.3-0.9,3.8-1,3.9c-0.2,0.2-1.3,0-1.6,0
c-0.8,1.8-0.6,2.3-2.6,2.6c-0.1,0.6,0.2,0.8,0.3,1.3c-1.4,1-1.8,1.9-2.9,3.2c-0.5,0.6-2.3,1.3-2.6,1.9c-0.5,1.1,0.3,2.2,0.6,3.9
c-0.8,0.3-1.4,0.4-2.3,0.6c0,0.1,0.1,0.9,0,1c-0.6,0.2-1.9-0.9-2.6,1c-0.6,1.5,0.1,2.7,0.3,3.9c-3,0.9-3.9,0.4-4.5,3.9
c-0.3,0-1.5,0.3-1.9,0.3c0,0.2,0,2.6,0,2.6s-1.5,1.5-1.6,1.6c0,0-1.1,1.8,0.3,1.3c-0.1,0.3-0.5,1.8-0.3,1.9
c-3.7-1.9-6.4,6.4-8.4,9.6c-0.1,0.2-1.2,0.3-1.3,0.6c-0.1,0.4,0.2,1.3,0,1.6c-0.3,0.5-1.1,0.2-1.6,0.6c-0.4,0.4-1.7,2.8-1.9,3.2
c-1,1.9-0.6,3.3,0.6,4.5c-2-0.6-2.7-0.4-3.9,1.6c-1.7,3-2.6,6-4.2,9.3c-0.6,1.2-2.1,2.4-2.6,3.9c-0.4,1.4,0.1,2.5-0.3,3.9
c-0.3,0.7-2.8,2.5-2.9,2.6c1.4-0.5,1.9-0.6,2.9-1.3c0.2-0.2,0.7-0.9,1-1c1.7-0.8,0.2,0.6,1,1c-1.3,0.5-3.7,1.3-1.3,1.9
c-1.7,1.1-3,2.2-4.8,3.2c0,0.2,0.1,1.2,0,1.3c-0.2,0.2-1.3,0-1.6,0c-0.4,0.8-0.2,1-0.6,1.6c-0.3-0.1-0.9-0.4-1-0.3
c0.4,0.3-0.5,1.2,0.6,1c-0.1,0.1-0.7,0.9-0.6,1c-0.8-0.1-0.9-0.3-1.6-0.6c-0.3,0.7-0.3,0.3-1,0.6c0,0.3,0.2,1.8,0,1.9
c-0.3,0.3-0.6-0.3-0.6-0.3c-1,0-4.9,3-5.8,3.5c-0.5,0.3-1.2,0-1.6,0.3c-0.2,0.2-0.1,1.1-0.3,1.3c-1,0.7-2,0.3-2.9,1
c-0.5,0.4-1.7,2.3-1.6,2.3v0.6c0.4,0.1,1.9,0.6,1.9,0.6c-1.6,0.7-8.4,3-9,3.9c-0.1,0.1-0.4,1.9-0.6,2.6c-5.2,0.1-2.5,1.6-5.5,2.9
c-1.7,0.7-2.1,0.4-3.2,1.3c0.6,1,0.4,1.3,1,2.2c-5.3,0.8-4.8,3.7-8.4,4.2c0.4,1.3,0.2,1.3,0.6,2.6c-0.7,0.3-1.3,0-1.9,0.3
c-0.3,0.1-0.8,1.5-1,1.6c-0.3,0.1-4-0.7-2.2,0.6c0.3,0.3,0.3,0,0.3,0c1.4,0.5,2.8,0.5,1,1.6c-1.6,1-3.8,0.9-5.5,1.6
c3.8,2.2-1.7,1.6-1.9,1.6c-0.1,0-0.2,0-0.3,0c-1,0.4-1.6,2.6-2.6,3.2c-0.1,0-4.6,2.2-4.8,2.2c-0.8,0.2-1.9-0.3-2.6,0
c-0.6,0.2,0.5,1.4-0.6,1.9c0.5-0.1,1.1-0.5,1.6-0.6c-1.4,2.8-0.4,3.4-4.5,4.8c0.8,0,0-3.2,0-3.2c-0.4-0.3-0.1-0.5-0.3-1
c-0.6,0.2-2.1,1.2-2.6,1.3c-2,0.1-0.5-2.1-3.5,0.6c-2.1,1.9-4.2,4.9-6.4,6.4c0.2,0.5,0.5,0.7,0.3,1.3c-1.7,0.3-1.5,0-1,0.3
c0.6,0.3,0,0.8,0.6,1c0,0-0.5,1.1-0.6,1.2c0.2,0.3,0.6,1,0.6,1c0.1,0.6-0.4,0.9-0.6,1.3c2.7,0.2,2.3,1.2,1.3,3.2
c-0.9-0.4,0,1.4,0.3,0.6c1.9,0.9,1.6,1.5,0.6,3.5c1,0.9,1.1,0.5,1.9,1.3c0,0-0.2,0.6-0.6,0.3c0,0.1-0.3,1-0.3,1
c0.7,0.3,1.6,0.6,1.9,0.6c0,0-0.7,0.7-0.3,1c0.8,0.6,1.8,0.3,2.9,0.6c0.4-0.8,0.5-1.8,0.6-2.3c3.2-0.4,3.6,0,3.9-2.6
c0.5,0,0.9,0.3,1,0.3c-0.7-0.3-0.7,1.3,1,1c0.9-0.2,4.9-3.1,5.8-3.9c0,0-0.4-0.6-0.3-0.6c1.2-0.6,1.5,0,2.2-0.6
c0.5-0.5-0.1-0.8,0-1c-0.9,0.2-1,0.3-1.9,0.3c0.1-0.3,0.3-1,0.3-1c1.1,0.2,1.7,0.5,2.9,0.3c1.3-0.2,1.4-1.6,2.3-2.3
c2.1-1.6,4.8-1.9,6.8-3.5c0.2-0.2,0.1-1.1,0.3-1.3c0.8-0.6,1.8-0.4,2.6-1c1.1-0.8,1.7-2.5,2.9-3.2c0.9-0.6,2.2-0.3,3.2-1
c0.7-0.5,1.1-2.4,1.9-2.9c1.2-0.8,2.4-0.7,3.5-1.6c2.3-1.9,2.4-3.4,5.1-4.8c0,0-0.1-1.6,0-1.9c0.9,0.2,0.5,0.6,1.6,0.6
c0.2,0-0.6-1.3-0.6-1.3c1.8-0.7,2.3-1.4,3.5-2.3c1.5-1,3.4-0.9,4.8-1.9l1.6-1.3c0.1-0.2-0.2-1.1,0-1.3c0.6-0.5,1.3-0.2,1.9-0.6
c1.7-1.2,1.4-2.6,4.2-3.2c-0.1,0,0-1.1,0-1.3c-1,0-1.9,0-2.9,0c0.1-0.2,0.3-1,0.3-1c2-0.3,2.5,0,3.9,0.3c0.3-0.9,1.1-2.2,2.6-2.6
c0.4-0.1,1.3,0.2,1.6,0c0.2-0.1-0.5-1.1-0.6-1.3c2-0.8,3-1.4,5.1-2.3c-0.1,0,0,0.9,0,1c0.4-0.4,0.3,0.3,0.3,0.3
c-0.3,0.2-1.1,1-1.3,1.3l-0.3,1.3c0.5-0.1,0.8-0.4,1.3-0.6c-0.1,1.1-1.1,6.3-0.3,8.7l1.3,0.3c0,0.7,0.3,2.3,0.3,2.6
c0,0.6-0.8,1.2-0.6,1.6c0.2,0.6,1.7,0.8,1.9,1.6c0.4,1.4-0.5,3.2,0,4.5c0.4,1.2,2.7,1,3.9,1.6c1.3,0.7,1.9,2.9,3.2,3.5
c0.8,0.4,0.9-0.6,1.6-0.3c0,0,0.3,1,0.3,1c6,0,11.9,2.4,18.3,1.6c1.7-0.2,3.4-1,5.8-1.3c0,0,0.3-1,0.3-1c2.7-0.9,3.6-0.3,6.8-1
c0.9-0.2,2.5-1.7,3.2-1.9c0.5-0.1,1.5,0.2,1.9,0c0.3-0.2,1.1-2.1,2.9-2.3c3.6-0.4,6.7,0.9,10,0.3c0.6-0.1,2.9-1.4,3.5-1.3
c0.2,0,1.3,1.1,1.6,1.3c0.3-1.1,0.6-1.9,1-2.9c1.6,0.2,3,0.8,5.1,0c0.1,0,0.5-0.9,0.6-1c0.8-0.2,2.2,0.6,3.9,0
c0.7-0.3,1.4-1.7,2.3-1.9c0.8-0.3,2.9,0.3,4.5-0.3c0.3-0.1,0.8-1.4,1.3-1.6c1-0.4,5.1,0.7,6.4,0.3c0.4-0.1,1.1-2.1,1.6-2.3
c1.2-0.4,4.2,0,5.5-0.3c0.6-0.1,2.4-1.9,2.9-1.9c0.3,0,1.8,0.7,2.6,1c0.2-0.4,0.7-1.8,1-1.9c0.9-0.4,2.3,0.2,3.2,0
c0.5-0.1,1.4-1.1,1.9-1.3c0.9-0.3,1.3,0.5,1.9,0.3c0,0,0-0.9,0-1c1.3-0.4,3.1-0.3,4.8-0.6c0,0,0.8-0.9,1-1c1.7-0.6,2.2,0.2,3.9-0.3
c0.9-0.3,2-2.4,2.9-2.6c1.7-0.4,2.9,0.5,5.1,0c2.8,5.1,0.4,9.1,3.9,13.2c1,1.2,2.6,1,3.5,1.6c0.4,0.2,0.2,0.8,0.3,1.3
c2.2,0.6,3.7,1.9,5.5,2.3l7.4,1.9c1.2,0.3,1.1-1.5,2.3-1.6c0.7-0.1-0.4,0.8,0.3,1c1,0.3,1.8-0.2,2.9-0.3c0,0.1-0.7,0.8-0.3,1h0.6
l2.6-0.3l0.6-1.6c2.2-0.1,6.5,0.5,9.7,0c0.6-0.1,3-1.2,3.5-1.3c1.3-0.2,3.1,0.3,3.9,0.3c1,0,3.4-1.1,4.5-1.3
c0.9-0.2,0.9,0.5,1.6,0.3c0.1,0,0-0.9,0-1c4.6-0.6,2.8-1.9,6.1-3.9c0.8,1.1,1.7,2.3,3.5,1.6c0.6-0.2,0.6-0.9,1-1.3
c-0.9-0.1-1.1,0-1.6,0c1.9-0.8,3.8-2.7,6.8-2.9c-1.3-0.8-1-1.2-0.6-2.9c0.3,0,1.1,0.1,1.3,0c0,0.9,0.1,1.9,0,3.5c-0.1,0-1,0-1,0
c1.5,1.4-1.3,2.8,1.3,4.2c-0.4,0.7-0.8,1.3-1.3,2.2c0.4,0.2,1.2,0,1.6,0c1.3,2.3-0.1,0.8,2.3,1.9c0,0-0.4,0.6-0.3,0.6
c0.6,0.2-1.1-0.1-1.9-0.3c0.2,0.3,0.6,1.3,0.6,1.3c1.2,0.1,3.2-0.1,3.5,0l-0.6,1.7c0,0,2.7,3.1,2.5,3.2c-0.6,0.2-1.2,0.2-1.6,0.3
c1.9,0.4,1.9,0.9,1.6,2.6c0.7-0.4,1.4-0.4,1.9-0.6c0,0,0-0.9,0-1c-2.3-2.2,0.7-0.5,1.3,1.9c0.3,0.1,1.3,0.1,1.6,0.3
c0.1,0.1-0.1,0.8,0,1c0.2,0.6,2.6,2,3.2,2.3c1,0.4,2.3,0,3.2,0.6c0.1,0.1,0,1.1,0,1.3c1,0.3,1.4,0.3,2.6,0.6c0,0-0.4,0.6-0.3,0.6
c1.2,0.2,0.8-1.1,2.9-0.6c0.2,0-1,0.6-1,0.6c2.8,1.9,3,0.9,6.1,0.3c-2.2,0.6-0.4,1.4,1,1.9c0.8,0.3,2.3,0.4,2.9,1
c0.1,0.1,0.3,1.3,0.3,1.3c2.4,0.9,3.6-1.1,5.5-0.3c0,0-0.6,0.9-0.6,1c0.4,0.3,0.6,0.6,1,1c0.5-0.4,0.9-1,1.3-1.3l5.6,2l8.3,0.5
c0.1-0.3,0.3-1.1,0.6-1.3c0.5,0.5,0.3,0.9,0.6,1.6c3.1,0.2,3.5,0.5,6.4,0.6c2,0.1,4.6-0.6,6.4,0c0,0,0.6,1.3,0.6,1.3
c0.2,0.1,0.6-0.6,2.6,0c0.1-0.4,0-1.2,0.3-1.3c0,0,2.2,0.6,3.2,0.6c-0.4-0.1,1.1-0.4,1.3-0.3v0.6c2.3-0.2,3.4-0.5,4.8-1
c0.5,1.1,0.7,1.5,1,2.3c1.2-0.3,2.8-0.4,4.2-1c0,0,0.7-1.2,1.6-1.3c0.8-0.1,2.5,1.2,3.2,1.3c1.5,0.2,4.9-0.9,7.1-0.6
c0.3,0,1.4,0.8,1.9,0.6c0.5-0.1,0.6-1.2,1.3-1.3c0.3,0,3.2,1.3,3.5,1.3c0.2,0,1.1-1.6,1.3-1.6c1.6-0.4,1.4,1.1,2.9,0.6
c0.4-0.1-0.3-1.3-0.3-1.3c0.3-0.1,0.9,0,1-0.3c1,0.3,1.9,1.5,3.5,1.6c1.4,0.1,2.7-0.9,3.5-1c0.7-0.1,2.7,0.6,2.9,0.6
c0.9,0,2.8-0.9,3.5-1c0.9,0,2.5,0.7,3.9,0.6c1.4,0,4.3-0.5,5.8-0.6l0.3-1c0.2,0,1.2,0.5,1.6,0.6c0.1,0,0.5,0,0.7,0
c2.9-0.4,4.4-1,7.4-0.6l0.3-1.3c0.8-0.1,1.4,1.5,2.6,1.3c0.3-0.1-0.3-1.3-0.3-1.3c2.9,0.8,3.5,0.3,5.1-1.3c0.4,0.2,1.2-0.2,1.6,0
c0.1,0,0,1.1,0,1.3c0,0,1,0.3,1,0.3c0.3-0.2,0.5-1.4,1.3-1.6l4.2,0.3c0.3,0,0.8,0.7,1.6,0.6l0.6-1c0.6-0.2,3,0,4.5,0
c0-0.2-0.1-1.6,0-1.6c0.5-0.2,1.3,0.3,1.3,0.3s0.4,1.1,1.9,1c2.2-0.2,3.4-1.3,5.8-1c-0.4-0.5-0.8-1.1-1-1.6
c0.2-0.1,0.5,0.1,0.3-0.3c0.5,0.2,1.4,0,1.9,0c0,0.2-0.1,1.5,0,1.6c0.3-0.2,1,0.2,1.3,0c0.1-0.1,0-1.1,0-1.3c1.3,0.4,2.8,1,3.5,1
c1.9-0.2,1.9-1.7,2.9-1.9c2.1-0.4-0.2,1,2.9-0.6c2.6,1.7,2.5,0.6,4.8,0.3c1.5-0.1,2.3,0.8,4.2,0.6c3.1-0.3,3.9-1.3,7.4-1.6v-1
c1.3-0.3,1.1,0.8,1.9,0.6l0.3-1c2.5-0.2,2.8,0,4.2,0.6c0.1-0.2,0.5-1.2,0.6-1.3c3.1-0.8,5.5,0.6,7.1,0.3v-0.6c0.7,0,1.8,0,2.6,0
l0.6-1.3c1.3-0.3,1.4,0.9,1.6,1c0.6,0.1,3.7-0.2,4.8-0.3c2.9-0.4,8.7-1.1,11.3-2.3c0.3-0.2,0.8-1.5,1-1.6c1.5-0.5,3,0.8,3.9,1.3
c-0.3-1.5-0.7-2,1.9-2.6c0,0.1,0.4,2.2,0.3,1.9c0.6,0,1.1,0.7,2.6,0.3c0.2-0.1-0.6-0.6-0.6-0.6c0.8-0.2,1.1-0.2,1.6-0.3
c-0.2-0.4-0.6-1.3-0.6-1.3c2.3-0.8,2.9,1.2,3.2,1.3c0.6,0.2,1.6,0,2.3,0c-0.2-0.1,0.5-0.3,0-0.6c-2.7-2,3.3-0.6,5.1-0.6
c-0.7-0.6-0.4-0.5-0.6-1.3c0.5,0,1.2,0.2,1.6,0c0,0.3-0.2,1.8,0,1.9c0.2-0.1,0.7,0,1,0l-0.3-2.3c0,0,1.3,1,1.6,1.6
c-0.6,0.1-0.2,1,0.6,0.6c1.3-0.5,0.8-0.9,2.6,0c0.9-1.3,1.4-2,1.9-2.9c0.3,0.6,3,2.6,5.8,1.6c0,0,2.4-2.8,2.6-2.9
c2.7-0.9,5.4,0.1,7.7-1c0.5,0.7,0.6,1.3,1,1.9L601.4,390.8z M151.8,326.3c0.3-0.9,0.5-1.7,0.6-3.2l1-0.3c0,0,1.1,0.5,1.6,0.3
c0.1-0.1-0.1-0.8,0-1c0.1-0.1,1.2-0.5,1.3-0.6c0.2-0.4,0.1-1.6,0-1.3l3.1-5.5c0-0.1,1.6-2.1,1.6-2.2l0.8-1.6c1.6-2.1,0.1-2.8,1-5.5
c0.2-0.7,1.9-1.5,2.6-1.9l0.6,0.6c-0.3-2.3-0.4-3.1,1.3-4.2c0,0.1,0.3,1.6,0.3,1.6c0.7,0.3,1-0.5,1.3-0.3c0.1-1.3,1-2.8,1.4-3.4
c-0.1,0.1-0.1,0.1-0.2,0.3c2.5-3.3,2.1-5.4,2.3-7.5c1.6,0,1.3,0.7,1.9-1.6c3.3,1,3.1-0.3,1.9-2.9c1.3-0.6,0.3-1.6,0.3-1.6
c1-0.2,1.8,0.9,2.9,0.6c0.5-0.1,0-1,0-1c-0.2,0.7-0.6-0.6-0.6-0.6s1.3-1.9,1.3-1.9c0.2,0.1,0.5,1,0.6,1.3c0.1,0,1.3-1,1.3-1
c0-0.1,1-1.3,1-1.3c0.5-2.1,0.6-4.4,1-5.8c0,0,0.5,0.7,0.6,0.6c0.3,0.7,0.6,1.4,1,2.2c0.2-0.1,1-0.3,1-0.3c0.4-1.9,0.6-3.8,1-6.4
c0.7,0,0.4-0.8,1.3-0.6c0,0,1.1,0.8,1.3,1c0.1-0.4,0-0.8,0.3-1.6c0.3,0,1.4,0.2,1.6,0c0.2-0.2,0-1.3,0-1.6c1-0.4,0.5,0.3,1-0.6
l-2.3-1.3c1.1-0.6,2.7-0.8,3.9-1.6c1.2-0.8,1.4-3.3,2.6-4.2c0.8-0.6,1.7-0.2,2.6-0.6c1.2-0.6,1.8-2.6,2.6-3.2
c1.2-0.9,2.7-0.5,3.5-1.6c0.8-1.1-0.1-1.9,1-2.9c0.8-0.8,1.8-0.4,2.9-1c1.7-1,2.5-4,4.8-5.5c1.7-1.1,0.5-0.2,1-2.3
c0,0,0.8-0.6,0.3-0.6c1.3-0.7,2.3-0.2,3.2-0.6c0.6-0.3,0.7-1.8,1.3-2.3c0.3-0.2,1,0.1,1.3,0c0.2-0.1,0.1-1.1,0.3-1.3
c0.4-0.4,1.2,0,1.6-0.3c0.5-0.4,0.4-1.6,1-1.9c0.8-0.4,1.9,0,2.6-0.3c0.6-0.3,2.5-2.5,3.5-3.2c-0.1-0.1-0.5-0.5-0.3-0.6
c0.3-0.3,1.2,0.2,1.6,0c0.4-0.2,0.8-1.9,1.3-2.3c1.1-0.8,2.4-0.5,3.5-1.3c0.5-0.3,0.2-1.4,1-1.6c0.4-0.1,1.2,0.2,1.6,0
c0.7-0.4,0.7-1.8,1.3-2.3c2.2-1.8,3.8,0,6.4-3.2c1.9,0.5,1.2,0.6,3.2,0c2.8,3.9-0.2,8.1-1.6,11.9c-0.7,1.7-0.8,3-1.6,4.5
c-0.6,1.1-2,2.1-2.6,3.2c-0.8,1.8-0.6,2.9-1.6,4.5c-0.5,0.8-1.8,1.5-2.2,2.2c-0.5,0.8-0.4,1.9-1,2.6c-0.6,0.7-2.1,0.8-2.6,1.6
c-0.3,0.6,0.1,1.6-0.3,2.3c-0.5,0.8-1.2,0.1-1.9,0.6c-0.5,0.4,0,1-0.3,1.6c-0.2,0.3-1.1,0.4-1.3,0.6c-0.7,0.9-0.6,2-1.3,2.9
c-0.6,0.7-2.6,1.4-2.9,2.3c-0.2,0.6,0.1,1.3,0,1.9c-0.4,0-1.7-0.3-2.3,0c-0.4,0.2-0.4,1.5-0.6,1.9c-0.2,0.3-2.8,2.1-2.9,2.3
c-0.8,1.4-0.1,2.4-1.3,3.5c-1.1,1-1.4,0-2.6,0.6c-1.2,0.7-1.7,3.6-2.9,4.5c-1,0.7-2.5,0.5-3.5,1.3c-1,0.8-0.9,3.1-1.9,3.9
c-0.5,0.4-1.5,0-1.9,0.3c-1.3,0.8-1.6,3.4-2.9,4.2c-1.2,0.7-2.6,0-3.9,0.6c-0.4,0.2-0.3,1.4-0.6,1.6c-0.3,0.1-1.4-0.2-1.6,0
c-0.3,0.3,0.1,1.2,0,1.6c0,0-2.2,2.2-2.3,2.3c-1.4,0.7-2.2,0.1-3.9,1c-0.5,0.3-1.2,1.4-1.6,1.6c-1.5,0.8-2.6,0.3-3.5,1.6
c-0.8,1.2-0.7,2.5-1.6,3.5c-0.5,0.6-1.8,1-2.3,1.6c-0.2,0.3-0.1,1.1-0.3,1.3c-2.5,1.9-2.4-1.2-3.5,2.9c-0.2,0-1.5-0.2-1.6-0.3
c-0.4,0.5-1.4,1.7-1.9,2.2c-1.9,1.9-3.7,3-5.2,5.5c1.3-0.7,2.1-0.8,3.2-1.6l-1,1.3l-0.3,0.3c-0.1,0.1-0.4,1.4-0.6,1.6
c-1.2,0.9-3.1,0.7-4.2,1.6c-0.1,0.1,0,0.8,0,1c-1.8,0.9-3.2,2.8-4.8,3.5c-1.9,0.8,0.9-1.8,1.9-2.3l0.3-1.3
c-1.1,0.7-4.7,1.5-5.1,2.3c-0.7,1.2,0.2,1.6-0.3,2.6c-0.2,0.3-0.8,0.5-1.1,0.6c0,0-0.1,0-0.2,0c0,0,0.1,0,0.2,0
c0.1-0.2-1-0.7-1.8-0.3c-0.4,0.2-0.4,1.1-0.6,1.3c-1.8,1.2-2.9,1.5-5.5,2.6c0,0.2,0.1,1.5,0,1.6c-0.2-0.2-0.7-0.7-1-0.6
C151.6,327.4,151.5,326.9,151.8,326.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32" height="41" viewBox="0 0 612 792" xml:space="preserve">
<style type="text/css">
.st0{fill:#F03E3E;}
.st1{fill:#FFFFFF;}
.st2{fill:#FAD43A;}
.st3{fill:#FAB005;}
</style>
<rect x="9.8" y="6.3" class="st0" width="591.6" height="779.5"/>
<path class="st1" d="M9.8,533.9c9.8-53.7,47.7-94.8,113.3-94.8c25.7,0,69.4,7.1,98.6,55.3l-50.4,28.7c-4-8.4-8.4-16.4-19.5-24.3
c-6.6-4.4-15-8-25.6-8c-36.7,0-49.1,42-49.1,69.9c0,30.5,15,69,51.3,69c3.5,0,20.3,0,34.5-15.9c5.7-6.6,8-11.9,10.2-18.6l51.3,26.5
c-8.4,13.3-17.7,27.9-36.3,41.1c-17.7,12.4-40.2,20.8-66.3,20.8c-44.2,0-114.5-26.5-114.5-120.7C7.2,552.9,8.1,543.2,9.8,533.9"/>
<path class="st1" d="M380.4,645.6c-13.3,8.8-29.6,19.5-61,19.5c-19,0-37.6-4.9-52.6-15c-31-19.9-42.9-55.7-42.9-95.5
c0-46.4,21.7-113.2,98.2-113.2c35.4,0,51.7,15.5,61.9,25.2v-21.7h63.7v222c-0.9,25.2-1.8,54.8-35.4,74.3
c-23.4,14.2-57.9,17.7-78.7,17.7c-46,0-76.1-15.9-88.4-28.7c-18.1-18.1-17.7-41.6-17.7-53.9h70.3c0.9,10.2,1.8,24.8,19.9,33.2
c4.4,2.2,11.1,4.4,21.7,4.4c41.1,0,41.1-24.3,41.1-43.3V645.6z M294.2,554c0,19.5,1.3,59.7,43.3,59.7c34.5,0,42.5-27,42.5-59.7
c0-11.1-0.9-34-8.4-45.1c-5.3-8.4-17.7-17.7-37.1-16.8C302.2,493.5,294.2,521.3,294.2,554z"/>
<path class="st1" d="M494.9,375.8h67.7v69h42.2v47.3h-42.6V596c-0.4,14.2-0.4,26.1,17.7,26.1l24.3-0.9v53.1
c-14.6,2.2-23.4,3.5-35.8,3.5c-23.4,0-49.1-1.3-63.7-22.6c-9.3-13.7-9.7-30.1-9.7-52.2V491.7h-33.6v-46.9h33.6V375.8z"/>
<g>
<path class="st2" d="M252.5,260C252.5,260,252.5,260.1,252.5,260C252.7,259.9,252.6,259.9,252.5,260z"/>
<path class="st2" d="M250.8,262.1C250.8,262.4,251.2,262.1,250.8,262.1L250.8,262.1z"/>
<path class="st2" d="M41.1,417.7C41.1,417.7,41.1,417.7,41.1,417.7C41,417.5,41,417.5,41.1,417.7z"/>
<path class="st2" d="M452.6,424.5L452.6,424.5C452.5,424.5,452.5,424.5,452.6,424.5z"/>
<path class="st2" d="M453.2,424.5C453.2,424.5,453.2,424.5,453.2,424.5C453.3,424.5,453.3,424.5,453.2,424.5z"/>
<path class="st3" d="M601.4,390.8c-0.7,0-1.4,0.1-2.1,0.2c-0.2,0-0.5,0.9-0.6,1c-1,0.4-5,0.9-5.5,1c-2.5,0.2-4.7-1-7.7-1
c0.2,1.4,0,2.1,1,3.5c-0.5-0.1-1.5,0.2-1.9,0c-0.6-0.3-0.7-2.4-2.9-1c-0.3,0.2-0.3,1.2-0.6,1.3c-0.7,0.2,0-0.1-0.3-0.3l-7.1-1
l-0.6-0.6c-0.6,0.1-0.7,0.2-1,0.3c-1.2,0.4-0.3,0.6-1,1.3h-0.6c-0.2-0.1,0-1.3,0-1.3c-1.1-0.3-1.9,1.1-2.6,1.3
c-1.1,0.3-2.3-0.5-4.2-0.3c-2.5,0.2-6,1.2-8,2.3c-0.5-0.4-1-0.8-1.6-1.3c-0.1,0.1-2.3,2.9-2.3,2.9c-0.6,0.3-1.4-0.5-0.6-0.3
c0-0.2,0.5-1.9,0-1.9c-0.7,0-0.7,0.9-1,1.3c-1.2-0.6-2.5-1.2-3.9,0c-0.2,0.1-0.1,0.9-0.3,1c-1.4,0.4,0.2-1.7-1.6-1.3
c-0.9,0.2-1.8,1.6-2.6,1.9c-1.3,0.6-2.4,0.6-3.9,1c0-0.1,0.6-0.9,0.3-1c-0.3-0.1-0.7,0.1-1,0c-0.6,0.7-0.7,1.3-1.3,2.2
c-0.7-0.1-1.6,0-2.3,0c0-0.6,0-1.3,0-1.9c1.1,0-0.7-0.5-1-0.6c-0.7,1.5-0.6,1.3-0.3,2.9c-0.2,0-0.8,0.1-1,0c0,0-1.8,0.3-2.9,0.3
c1.1-0.7,0.4-0.5,0.6-1.3l1-0.6c-0.1,0-1.1-0.6-1.3-0.6c-0.4,1.3-1,3.4-3.5,3.2c-2.9-0.3,0.2-2.5-3.9-0.6v1.9
c-0.1,0-4.5-0.6-4.5-0.6c-0.3-0.3-0.2-0.9-0.6-1.3c-0.6,0.4-1.5,2-2.3,2.3c-2.4,0.7-3.8-0.6-5.8,0c-0.1,0,0.3,0.5,0.3,0.6
c-0.9,0.3-1.7,0.9-2.9,0.6c-0.5-0.1,0-1.3,0-1.3c-0.5-0.2-3.1-0.3-3.2-0.3c0,0.1,0,0.2,0,0.3c0,0-1.8,0.8-2.9,1l-0.3,1.6
c-0.9,0.5-2.6-1-3.9-1c-0.5,0-1,1-1,1c-4.2,1.2-7.4,0.8-11.6,1.3c-0.9,0.1-3.8,1.5-4.8,1.6c-2,0.2-4.4,0.4-5.5,0
c-0.7-0.3-0.4-1.4-2.3-1.3c-1.4,0.1-2.6,1.7-3.5,1.9c-1.5,0.4-3.9,0.6-4.8,0.6c-2,0.2-3-0.5-4.2-1.3c-0.1,0.1-0.3,1.6-0.3,1.6
c-0.8,0.2-1.8-1.2-3.2-1c0,0-0.8,0.9-1,1c-1.5,0.4-4.2,0.6-5.5,0.6c-2.9,0-5.7-0.6-8.7-0.3c-3.4,0.4-5.1,1.7-8.4,1.9
c-0.9,0-3.9-0.8-5.5-0.6c-1.1,0.1-2.2,1.8-3.2,1.9c-2.3,0.4-1.8-1.3-3.9-1c-0.4,0-0.2,0.9-0.3,1.3c-2.5-0.2-7.3,1.3-8.7-1
c-0.2,0.1-0.3,1.1,0,1.3c-2.7,0-4.6,0.8-6.8,0.6c-1.5-0.1-3.3-0.1-5.5-0.6c-0.5-0.1-1.1-1.2-1.3-1.3c-2.1-0.5-4.6,0.9-6.8,1
c-0.1,0-3.2,0-3.2,0s-0.5-1.5-1-1.6c-1.1-0.3-2.3,0.7-3.2,0.6c-0.5,0-1.1-0.9-1.3-1c-3.4-0.6-2.2,1.8-4.5,1.6c0,0-3.7-1.6-1.9-1.3
c0-0.1-0.6-0.8-0.3-1c-1.3,0.2-1.9,0.1-3.2,0.3c0,0.1,0.3,1.6,0.3,1.6c-0.8,0.1-2.6-1.3-2.6-1.3c-1.9-0.4-0.3,0.7-1.9,0.6
c-2-0.1-3.8-2-5.1-2.9c-0.6,0.4-0.8,0.5-1.6,1c-0.5-1.2-1.1-2.2-1.6-3.2c-2,0.5-3.5-0.4-5.8-0.6c-0.1-0.4,0-1.2-0.3-1.3
c-1.4-0.3-1.1,0.9-2.6,0.3c-0.4-0.2-1.3-1.8-1.6-1.9c-0.6-0.2-1.3,0.5-2.3,0.3c-1-0.2-0.5-1.2-0.6-1.3c-0.4-0.2-1.2,0.2-1.6,0
c-0.1-0.1,0-1.2-0.3-1.3c-0.3-0.1-1.5,0.8-2.6-0.6c-0.2-0.3-0.2-0.5-0.3-1c0,0-0.3-0.8-0.6-1.3c0.2,0.4,0.6-0.8,0-1.3
c-1-0.9-1.7-0.3-2.9-0.3c0.3-0.2-0.3-0.8-0.3-1c0.3-0.1,0.6,0,1,0c-0.5-2.3-1.2-2.1-1.6-2.9c-1.1-2.3-1.4-5.8-1.6-8
c-0.6,0-1.3-0.2-1.9-0.3c0.4-1.6,0.6-2,1.6-2.9c-0.5-1.5-2.7-4.4-2.3-6.1c0.2-0.9,1-0.8,1.6-1.3c-1-2.2-1-2.8,0.6-3.9
c-0.1-0.3-0.8-1.5-0.6-1.9c0-0.2,1.5-1.3,1.6-1.6c0.3-1.1-0.5-1.3-0.3-2.2c0.1-0.5,1-2.1,1.3-2.9c-0.3-0.3-2.3-2.9-2.3-2.9
c-0.6-0.2-1.7,0.3-2.2,0c-0.1,0,0-0.8,0-1c-0.5-1.3-2.2-1.1-3.5-1c-0.3-0.4-0.5-1.8-1-1.9c-0.9-0.3-0.3,2.8-0.3,2.9
c-2.5,0.4-3.7,0.6-5.2,1.9c-2.5,2.4-2.7,8.3-3.9,11.7l-2.9,3.4c-0.2,0.1-1,0.4-1.3,0.6c0.3,1.9,0.4,3-1,3.9c-2.9,1.8-2.5-0.8-2.9-1
c-0.4-0.1-0.9,0.2-1.3,0c0,0-0.5,1-0.6,1.3c0.5,0.4,0.7,0.1,1.3,0.3c-0.1,0.2-0.3,1-0.3,1c-0.3-0.2-1,0.1-1.3,0
c-0.1-0.1-0.2-1-0.3-1.3c-0.2,0.1-1.7,0.6-1.3,0.6c-0.6,0.6-1.6,2.2-2.3,2.6c-0.7,0.4-0.5-0.5-1.3-0.3c-1.5,0.3-2.5,3.5-2.9,5.1
c-0.5-0.2-1.4,0.2-1.9,0c-0.1,0,0-0.9,0-1c0,0-0.1-1.7-2.3-1.3c-0.3,0-1.3,1.2-1.6,1.3c-0.8,0.3-2.2-0.1-3.2,0.3
c-0.9,0.4-2,2.5-2.9,3.2c-1.5-2.8-2.2-1.5-3.9,0c-3.5-2.8-9.7,1.1-13.5-1.6c-1.1-0.7-1-4.2-1-4.2c0-0.2,0.1-1.3-0.3-1
c1.4-1.2,1.2-2.3,2.2-3.9c0.3-0.5,1.6-0.8,1.9-1.3c0.5-0.8,0.1-1.8,0.6-2.6c0.2-0.3,1.2-0.3,1.3-0.6c0.2-0.5-0.2-1.4,0-1.9
c0.3,0,1.1,0.4,1.6,0.6c1.4-2,2.9-2.4,4.8-3.9c1.2-0.9,2.1-2.6,3.2-3.5c1.7-1.5,3.8-2.3,5.8-3.5c1-0.7,1.9-2.3,2.9-2.9
c0.3-0.2,0.9,0.1,1.3,0c1.3-0.5,2.7-2.3,3.9-2.9c3.2-1.7,6.1-2,10-3.2c2.1-0.7,6.2-2.8,8.7-3.2c3-0.6,6.3-0.2,8.4-0.6
c0.8-0.2,1.6-1.2,2.9-1c0.5,0.1,0.7,1.2,1,1.3c0.5,0.1,1.6-0.7,2.3-0.6c1.2,0.1,2.8,1.7,3.9,1.9c0.7,0.2,1.6-0.4,2.6,0
c0,0.1,0,0.2,0,0.3c0.9,0.5,1.6,0.8,2.6,1.3c0,0.1-0.1,0.9,0,1c1.7,0.5,2.7-0.7,5.5,0.3c-0.2-0.1-1.1-1.2-1.3-1.3
c-0.3,0.5-0.3-0.6-0.3-0.6c0.3-0.3,1.5-1,1.6-1.3c0.3-1.2-0.6-4.3-1-5.8c-0.5-1.9-3.5-4.2-3.5-4.2c0-0.2,0.1-1.2,0-1.3
c-0.3-0.3-1.3,0.2-1.6,0c-0.5-0.2,0-1-0.3-1.3c-0.6-0.5-4.6-3.1-5.1-3.2c-2.5-0.8-4.2,0.2-7.1,0c-1.1-0.1-2.6-1.3-3.9-1.3
c-0.6,0-1.8,0.9-2.3,1c-3,0.3-5.9,0-9.3,1c-0.5,0.1-1.3,1.2-1.6,1.3c-1.5,0.5-2.8-0.2-4.2,0.3c-0.7,0.3-1.2,1.8-0.3,1.9
c-2.7,0.9-3.9,2-6.8,2.9c-0.1-0.5,0.1-1.1-0.3-1.3h-0.6c-0.4,1-1.8,4-2.9,4.2c-0.3,0.1-3.9-1.4-4.5-1.6c0.4,0.8,0.5,1.2,1,1.9
c-2.7,0.9-5.9,1.4-7.7,0.3c-0.3,2.7,0.3,3.8-0.3,6.4c-2.6-0.5-6.4,3-7.1-1.6c-0.3,1.7-0.6,2-2.6,2.6c-0.2,2.7-0.2,6.7-2.9,7.7
c-0.3,0.1-1-0.2-1.3,0c-1.6,1.4,0.4,1.7-2.3,1.3c-0.1,0.3-0.1,0.9-0.3,1c0,0,0.8,0.6,0.6,0.6c-1.7,0-1.1-1.3-1.9-1.9
c-0.4,0.2-1.2,0-1.6,0l-0.3,1.6c-1,0.4-6.9-0.2-7.7-0.3c-2.1-0.3-6.4,0.1-8.7,1c-0.2,0.1-0.5,0.9-0.6,1c-0.9,0.2-1.2-0.4-2.6,0
c0,0.1,0,0.2,0,0.3c-1,0.5-3.9,0.8-4.5,1.6c-0.2,0.3-0.1,1.1-0.3,1.3c-0.8,0.6-2,0.4-2.9,1c0,0-0.3,1-0.3,1
c-2.2,1.3-5.9,1.7-7.7,3.2c-0.4,0.4-0.7,1.4-1,1.9c-3.5-0.9-2.4,0.5-4.5,1.3c-1.4,0.5-2.9,0.1-4.5,1c-0.2,0.1-0.5,0.9-0.6,1
c-2.2,0.6-3.1,0.1-5.5,1l0.3,2.6l-1-0.3c0,0-0.3-1.8-0.3-1.9c0,0-1,0.3-1,0.3c-0.2,2.4,0.4,2.5-1.9,1.9c0.1,0.2,0.5,1.2,0.3,1.3
c-1.6,0.5-3.8-0.7-4.5-0.6c-0.6,0-0.6,0.8-1,1c-0.9,0.3-2-0.2-2.9,0c-0.4,0.1-1.7,1.2-2.2,1.3c-0.7,0.1-1.2-0.5-2.3-0.3
c-2.5,0.5-5.6,3-8.4,3.5c-1.5,0.3-3.1-0.4-5.2,0c-1.9,0.4-4.8,2.2-6.4,2.3c-1,0-1.1-0.8-2.3-0.6c-0.1,0-0.8,0.9-1,1
c-1.1,0.3-6.2,0.1-7.1,0c-0.2,0-1.1-1-1.3-1c-1.9-0.2-3,1-4.8,1c-0.3,0-1.6-0.9-1.9-1c-2.5-0.4-3.3,0.1-6.1-0.6c0-0.1,0.1-1,0-1
c-0.9-0.3-2.3,0.2-3.2,0c-2.2-0.4-4-2.6-5.5-3.9c-0.4-0.3-1.4-0.5-1.6-1c-0.5-0.9,0.1-1.9-0.3-2.9c-0.3-0.7-2.1-2-2.6-3.2
c0.5-0.4,0-1.6,0-1.6c0.3,0,0.3-3.2,0.3-3.2c1.3-2.8,0.2-4.4,2.6-5.5c-0.4-1.6-0.5-1.7,0-3.5c-0.4,0.1-0.5,0.5-1.3,0.3
c0.3-1,2.5-5.9,3.2-6.4c0.4-0.3,1.5,0,1.9-0.3l0.3-1.6c3-1,2.8-2.6,5.1-4.5c0.6-0.5,1.3-0.1,1.9-0.6c0.6-0.5,0.6-1.8,1.3-2.3
c1.1-0.7,2.3-0.4,3.2-1c1.1-0.7,1-2,1.9-2.9c1-1,3-1.1,4.5-1.6c0-0.7-0.2-1.6,0-2.3c1.2-0.4,2.9-0.4,3.2-0.6c0.1-0.1,0-1.1,0-1.3
c2-1.1,3.1-1.9,5.1-2.9c-0.1-0.2-0.3-1-0.3-1c1-1.3,3.2-1,1.6-2.3c-0.4-0.3-0.3-1-0.3-1c1.6-0.5,2.5-0.7,3.9-1.3
c0-0.1-0.4-0.6-0.3-0.6c1.3-0.5,2.7,0.3,4.2-1c0.9-0.7,1.3-2.3,2.3-2.9c0.3-0.2,1.1-0.1,1.3-0.3c0.5-0.6,0-1.5,0.6-1.9
c1.6-1.2,3.5-1.2,5.1-2.3c1.1-0.7,2-1.8,3.2-2.6c-0.1-0.2-0.3-1-0.3-1c0.8-0.8,1.8-0.3,2.9-1c0.4-0.2-0.3-0.6-0.3-0.6
c1.1-0.8,1.3-1,1.9-2.3c0.8,0,0.8,0.5,1.6,0c0,0-1-1.2,0.3-1.6c3.9-1.3-0.8,3.4-1.6,4.2c2.7-1.3,3.2-2.3,6.4-2.6
c1.2-2.8,1.6-4,3.5-5.5c-0.1-0.2-0.5-0.4-0.3-0.6c0.1-0.1,1.2-0.5,1.3-0.6c1.1-2.1,0.3-2.3,2.6-3.2c0,0-1.2-0.5-1-0.6
c0.4-0.3,1.2,0.2,1.6,0c0.1-0.1,0.3-1.5,0.3-1.6c1.3-0.2,1.7,0.2,2.6-0.6c0.3-0.3,0-1.2,0.3-1.6c0.3-0.4,1.4-0.5,1.6-1
c0.9-1.6,0.2-2.6,1.3-3.5c0.8-0.7,1.9-0.3,2.6-1c0.1-0.2-0.1-0.8,0-1c0.3-0.7,3.9-3.4,4.8-4.5c1,0.2,1.2,0.2,2.3,0.3
c0.1-0.5-0.2-1.4,0-1.9l0.3-0.3c2-0.7,1.7-2.4,2.6-2.9c0.9-0.5,2.5,0,2.9,0c0.7-1.8,1.2-2.2,2.9-2.9c-1.4-1.1,1.5-4.2,2.3-5.5
c0.7,0.5,0.7,0.5,1.6,1l0.6-1.3c0.5-2.1-0.1-1.9,1.9-2.6c-0.3-2.6,1.2-3.5,4.2-4.2c0.1-0.7-0.3-1.6,0-2.3c0.2-0.4,1.3-0.4,1.6-0.6
c0.8-0.7,2.3-4.5,2.3-4.5l-0.3-0.6c0.7-0.4,1.1-0.5,1.3-0.5c0-0.2,0.2-0.6,0.7-1.4c0,0,0.7-0.5,1-0.7c0-0.1,0-0.2,0-0.3
c0.4-0.4,2.2-0.8,2.3-1c0.7-1.4-0.2-2.2-0.3-2.9c0.2-0.1,0.8,0.1,1,0c0.1-0.1-0.1-0.8,0-1c0.6-1.2,2.6-2,3.5-2.3
c-0.2-0.6,0.2-0.6,0.3-1c-0.3-0.2-1.3-1.3-1.3-1.3c1.1,0.3,3.9-6,4.5-8c-0.6-0.3-0.7,0-1-0.6c1.8,0.1,4.2,0.2,3.5-2.6l-0.3-0.6
c0.6-0.7,2.8-1.6,3.2-2.3c1.5-2.3,0.4-7.7,0.3-10c-0.1-2,1-6.2,0.6-7.4c-0.2-0.5-1.5-1.3-1.6-1.6c-0.5-1.6,1-2.5-0.3-3.2
c-0.5-0.3-1.7-0.6-1.9-0.6c-0.3-2.5-0.3-3-2.3-3.2c0.5-1.3,0.5-1.6,1-2.6c-2.4-0.9-1.5-1.8-2.6-2.6c-0.6-0.4-0.5,0.4-1,0.3
c-0.5-0.1-1.4-1.2-1.9-1.3c-2.3-0.5-5.3-0.6-7.7-0.3c-2.9,0.4-7,2.2-10.3,3.5c-0.9,0.4-2.7,2.1-3.5,2.3c-1,0.2-1.4-0.7-2.6-0.3
c-1.7,0.5-4.9,6.9-6.8,8c-0.7,0.4-1.3-0.1-1.6,0c-1.2,0.4-1.1,2.1-1.9,2.9c-0.8,0.7-2,0.3-2.9,1c-0.6,0.4-0.8,1.5-1.3,1.9
c-0.7,0.7-1.8,0.6-2.6,1.3c-1.1,0.9-1.2,2.9-2.3,3.9c-1,0.8-2.2,0.8-3.2,1.6c-1.9,1.5-3.3,3.9-5.1,5.5c-0.4,0.3-1.4,0.6-1.6,1
c-0.2,0.3-0.1,1.6-0.6,2.3c-0.6,0.7-2.9,1.7-3.2,2.6c-0.2,0.4,0.2,1.3,0,1.6c-0.4,0.6-1-0.1-1.6,0.3c-0.2,0.1-0.2,1.4-0.3,1.6
c-0.8,1.2-3.2,1.9-4.2,2.9c-0.7,0.7-0.3,1.6-1,2.6c-0.5,0.7-1.2,0.1-1.9,0.6c-0.1,0.1,0.1,0.8,0,1c-0.4,1-3,1.6-3.5,2.6
c-0.5,0.8,0,1.2-0.3,1.6c-0.4,0.5-0.5,0.2-1.3,0.3c-0.7,2.2-3.2,3.9-1,5.1c-2.1,0.3-2.8,0.9-4.8,1c-0.6,1.3-0.5,2.5-1.3,3.5
c-0.7,0.8-1.4,0.3-2.3,1c-0.9,0.7-0.7,1.9-1.3,2.9c-1.2,1.9-3.7,3.6-4.8,5.5c-0.2,0.3-0.9,3.8-1,3.9c-0.2,0.2-1.3,0-1.6,0
c-0.8,1.8-0.6,2.3-2.6,2.6c-0.1,0.6,0.2,0.8,0.3,1.3c-1.4,1-1.8,1.9-2.9,3.2c-0.5,0.6-2.3,1.3-2.6,1.9c-0.5,1.1,0.3,2.2,0.6,3.9
c-0.8,0.3-1.4,0.4-2.3,0.6c0,0.1,0.1,0.9,0,1c-0.6,0.2-1.9-0.9-2.6,1c-0.6,1.5,0.1,2.7,0.3,3.9c-3,0.9-3.9,0.4-4.5,3.9
c-0.3,0-1.5,0.3-1.9,0.3c0,0.2,0,2.6,0,2.6s-1.5,1.5-1.6,1.6c0,0-1.1,1.8,0.3,1.3c-0.1,0.3-0.5,1.8-0.3,1.9
c-3.7-1.9-6.4,6.4-8.4,9.6c-0.1,0.2-1.2,0.3-1.3,0.6c-0.1,0.4,0.2,1.3,0,1.6c-0.3,0.5-1.1,0.2-1.6,0.6c-0.4,0.4-1.7,2.8-1.9,3.2
c-1,1.9-0.6,3.3,0.6,4.5c-2-0.6-2.7-0.4-3.9,1.6c-1.7,3-2.6,6-4.2,9.3c-0.6,1.2-2.1,2.4-2.6,3.9c-0.4,1.4,0.1,2.5-0.3,3.9
c-0.3,0.7-2.8,2.5-2.9,2.6c1.4-0.5,1.9-0.6,2.9-1.3c0.2-0.2,0.7-0.9,1-1c1.7-0.8,0.2,0.6,1,1c-1.3,0.5-3.7,1.3-1.3,1.9
c-1.7,1.1-3,2.2-4.8,3.2c0,0.2,0.1,1.2,0,1.3c-0.2,0.2-1.3,0-1.6,0c-0.4,0.8-0.2,1-0.6,1.6c-0.3-0.1-0.9-0.4-1-0.3
c0.4,0.3-0.5,1.2,0.6,1c-0.1,0.1-0.7,0.9-0.6,1c-0.8-0.1-0.9-0.3-1.6-0.6c-0.3,0.7-0.3,0.3-1,0.6c0,0.3,0.2,1.8,0,1.9
c-0.3,0.3-0.6-0.3-0.6-0.3c-1,0-4.9,3-5.8,3.5c-0.5,0.3-1.2,0-1.6,0.3c-0.2,0.2-0.1,1.1-0.3,1.3c-1,0.7-2,0.3-2.9,1
c-0.5,0.4-1.7,2.3-1.6,2.3v0.6c0.4,0.1,1.9,0.6,1.9,0.6c-1.6,0.7-8.4,3-9,3.9c-0.1,0.1-0.4,1.9-0.6,2.6c-5.2,0.1-2.5,1.6-5.5,2.9
c-1.7,0.7-2.1,0.4-3.2,1.3c0.6,1,0.4,1.3,1,2.2c-5.3,0.8-4.8,3.7-8.4,4.2c0.4,1.3,0.2,1.3,0.6,2.6c-0.7,0.3-1.3,0-1.9,0.3
c-0.3,0.1-0.8,1.5-1,1.6c-0.3,0.1-4-0.7-2.2,0.6c0.3,0.3,0.3,0,0.3,0c1.4,0.5,2.8,0.5,1,1.6c-1.6,1-3.8,0.9-5.5,1.6
c3.8,2.2-1.7,1.6-1.9,1.6c-0.1,0-0.2,0-0.3,0c-1,0.4-1.6,2.6-2.6,3.2c-0.1,0-4.6,2.2-4.8,2.2c-0.8,0.2-1.9-0.3-2.6,0
c-0.6,0.2,0.5,1.4-0.6,1.9c0.5-0.1,1.1-0.5,1.6-0.6c-1.4,2.8-0.4,3.4-4.5,4.8c0.8,0,0-3.2,0-3.2c-0.4-0.3-0.1-0.5-0.3-1
c-0.6,0.2-2.1,1.2-2.6,1.3c-2,0.1-0.5-2.1-3.5,0.6c-2.1,1.9-4.2,4.9-6.4,6.4c0.2,0.5,0.5,0.7,0.3,1.3c-1.7,0.3-1.5,0-1,0.3
c0.6,0.3,0,0.8,0.6,1c0,0-0.5,1.1-0.6,1.2c0.2,0.3,0.6,1,0.6,1c0.1,0.6-0.4,0.9-0.6,1.3c2.7,0.2,2.3,1.2,1.3,3.2
c-0.9-0.4,0,1.4,0.3,0.6c1.9,0.9,1.6,1.5,0.6,3.5c1,0.9,1.1,0.5,1.9,1.3c0,0-0.2,0.6-0.6,0.3c0,0.1-0.3,1-0.3,1
c0.7,0.3,1.6,0.6,1.9,0.6c0,0-0.7,0.7-0.3,1c0.8,0.6,1.8,0.3,2.9,0.6c0.4-0.8,0.5-1.8,0.6-2.3c3.2-0.4,3.6,0,3.9-2.6
c0.5,0,0.9,0.3,1,0.3c-0.7-0.3-0.7,1.3,1,1c0.9-0.2,4.9-3.1,5.8-3.9c0,0-0.4-0.6-0.3-0.6c1.2-0.6,1.5,0,2.2-0.6
c0.5-0.5-0.1-0.8,0-1c-0.9,0.2-1,0.3-1.9,0.3c0.1-0.3,0.3-1,0.3-1c1.1,0.2,1.7,0.5,2.9,0.3c1.3-0.2,1.4-1.6,2.3-2.3
c2.1-1.6,4.8-1.9,6.8-3.5c0.2-0.2,0.1-1.1,0.3-1.3c0.8-0.6,1.8-0.4,2.6-1c1.1-0.8,1.7-2.5,2.9-3.2c0.9-0.6,2.2-0.3,3.2-1
c0.7-0.5,1.1-2.4,1.9-2.9c1.2-0.8,2.4-0.7,3.5-1.6c2.3-1.9,2.4-3.4,5.1-4.8c0,0-0.1-1.6,0-1.9c0.9,0.2,0.5,0.6,1.6,0.6
c0.2,0-0.6-1.3-0.6-1.3c1.8-0.7,2.3-1.4,3.5-2.3c1.5-1,3.4-0.9,4.8-1.9l1.6-1.3c0.1-0.2-0.2-1.1,0-1.3c0.6-0.5,1.3-0.2,1.9-0.6
c1.7-1.2,1.4-2.6,4.2-3.2c-0.1,0,0-1.1,0-1.3c-1,0-1.9,0-2.9,0c0.1-0.2,0.3-1,0.3-1c2-0.3,2.5,0,3.9,0.3c0.3-0.9,1.1-2.2,2.6-2.6
c0.4-0.1,1.3,0.2,1.6,0c0.2-0.1-0.5-1.1-0.6-1.3c2-0.8,3-1.4,5.1-2.3c-0.1,0,0,0.9,0,1c0.4-0.4,0.3,0.3,0.3,0.3
c-0.3,0.2-1.1,1-1.3,1.3l-0.3,1.3c0.5-0.1,0.8-0.4,1.3-0.6c-0.1,1.1-1.1,6.3-0.3,8.7l1.3,0.3c0,0.7,0.3,2.3,0.3,2.6
c0,0.6-0.8,1.2-0.6,1.6c0.2,0.6,1.7,0.8,1.9,1.6c0.4,1.4-0.5,3.2,0,4.5c0.4,1.2,2.7,1,3.9,1.6c1.3,0.7,1.9,2.9,3.2,3.5
c0.8,0.4,0.9-0.6,1.6-0.3c0,0,0.3,1,0.3,1c6,0,11.9,2.4,18.3,1.6c1.7-0.2,3.4-1,5.8-1.3c0,0,0.3-1,0.3-1c2.7-0.9,3.6-0.3,6.8-1
c0.9-0.2,2.5-1.7,3.2-1.9c0.5-0.1,1.5,0.2,1.9,0c0.3-0.2,1.1-2.1,2.9-2.3c3.6-0.4,6.7,0.9,10,0.3c0.6-0.1,2.9-1.4,3.5-1.3
c0.2,0,1.3,1.1,1.6,1.3c0.3-1.1,0.6-1.9,1-2.9c1.6,0.2,3,0.8,5.1,0c0.1,0,0.5-0.9,0.6-1c0.8-0.2,2.2,0.6,3.9,0
c0.7-0.3,1.4-1.7,2.3-1.9c0.8-0.3,2.9,0.3,4.5-0.3c0.3-0.1,0.8-1.4,1.3-1.6c1-0.4,5.1,0.7,6.4,0.3c0.4-0.1,1.1-2.1,1.6-2.3
c1.2-0.4,4.2,0,5.5-0.3c0.6-0.1,2.4-1.9,2.9-1.9c0.3,0,1.8,0.7,2.6,1c0.2-0.4,0.7-1.8,1-1.9c0.9-0.4,2.3,0.2,3.2,0
c0.5-0.1,1.4-1.1,1.9-1.3c0.9-0.3,1.3,0.5,1.9,0.3c0,0,0-0.9,0-1c1.3-0.4,3.1-0.3,4.8-0.6c0,0,0.8-0.9,1-1c1.7-0.6,2.2,0.2,3.9-0.3
c0.9-0.3,2-2.4,2.9-2.6c1.7-0.4,2.9,0.5,5.1,0c2.8,5.1,0.4,9.1,3.9,13.2c1,1.2,2.6,1,3.5,1.6c0.4,0.2,0.2,0.8,0.3,1.3
c2.2,0.6,3.7,1.9,5.5,2.3l7.4,1.9c1.2,0.3,1.1-1.5,2.3-1.6c0.7-0.1-0.4,0.8,0.3,1c1,0.3,1.8-0.2,2.9-0.3c0,0.1-0.7,0.8-0.3,1h0.6
l2.6-0.3l0.6-1.6c2.2-0.1,6.5,0.5,9.7,0c0.6-0.1,3-1.2,3.5-1.3c1.3-0.2,3.1,0.3,3.9,0.3c1,0,3.4-1.1,4.5-1.3
c0.9-0.2,0.9,0.5,1.6,0.3c0.1,0,0-0.9,0-1c4.6-0.6,2.8-1.9,6.1-3.9c0.8,1.1,1.7,2.3,3.5,1.6c0.6-0.2,0.6-0.9,1-1.3
c-0.9-0.1-1.1,0-1.6,0c1.9-0.8,3.8-2.7,6.8-2.9c-1.3-0.8-1-1.2-0.6-2.9c0.3,0,1.1,0.1,1.3,0c0,0.9,0.1,1.9,0,3.5c-0.1,0-1,0-1,0
c1.5,1.4-1.3,2.8,1.3,4.2c-0.4,0.7-0.8,1.3-1.3,2.2c0.4,0.2,1.2,0,1.6,0c1.3,2.3-0.1,0.8,2.3,1.9c0,0-0.4,0.6-0.3,0.6
c0.6,0.2-1.1-0.1-1.9-0.3c0.2,0.3,0.6,1.3,0.6,1.3c1.2,0.1,3.2-0.1,3.5,0l-0.6,1.7c0,0,2.7,3.1,2.5,3.2c-0.6,0.2-1.2,0.2-1.6,0.3
c1.9,0.4,1.9,0.9,1.6,2.6c0.7-0.4,1.4-0.4,1.9-0.6c0,0,0-0.9,0-1c-2.3-2.2,0.7-0.5,1.3,1.9c0.3,0.1,1.3,0.1,1.6,0.3
c0.1,0.1-0.1,0.8,0,1c0.2,0.6,2.6,2,3.2,2.3c1,0.4,2.3,0,3.2,0.6c0.1,0.1,0,1.1,0,1.3c1,0.3,1.4,0.3,2.6,0.6c0,0-0.4,0.6-0.3,0.6
c1.2,0.2,0.8-1.1,2.9-0.6c0.2,0-1,0.6-1,0.6c2.8,1.9,3,0.9,6.1,0.3c-2.2,0.6-0.4,1.4,1,1.9c0.8,0.3,2.3,0.4,2.9,1
c0.1,0.1,0.3,1.3,0.3,1.3c2.4,0.9,3.6-1.1,5.5-0.3c0,0-0.6,0.9-0.6,1c0.4,0.3,0.6,0.6,1,1c0.5-0.4,0.9-1,1.3-1.3l5.6,2l8.3,0.5
c0.1-0.3,0.3-1.1,0.6-1.3c0.5,0.5,0.3,0.9,0.6,1.6c3.1,0.2,3.5,0.5,6.4,0.6c2,0.1,4.6-0.6,6.4,0c0,0,0.6,1.3,0.6,1.3
c0.2,0.1,0.6-0.6,2.6,0c0.1-0.4,0-1.2,0.3-1.3c0,0,2.2,0.6,3.2,0.6c-0.4-0.1,1.1-0.4,1.3-0.3v0.6c2.3-0.2,3.4-0.5,4.8-1
c0.5,1.1,0.7,1.5,1,2.3c1.2-0.3,2.8-0.4,4.2-1c0,0,0.7-1.2,1.6-1.3c0.8-0.1,2.5,1.2,3.2,1.3c1.5,0.2,4.9-0.9,7.1-0.6
c0.3,0,1.4,0.8,1.9,0.6c0.5-0.1,0.6-1.2,1.3-1.3c0.3,0,3.2,1.3,3.5,1.3c0.2,0,1.1-1.6,1.3-1.6c1.6-0.4,1.4,1.1,2.9,0.6
c0.4-0.1-0.3-1.3-0.3-1.3c0.3-0.1,0.9,0,1-0.3c1,0.3,1.9,1.5,3.5,1.6c1.4,0.1,2.7-0.9,3.5-1c0.7-0.1,2.7,0.6,2.9,0.6
c0.9,0,2.8-0.9,3.5-1c0.9,0,2.5,0.7,3.9,0.6c1.4,0,4.3-0.5,5.8-0.6l0.3-1c0.2,0,1.2,0.5,1.6,0.6c0.1,0,0.5,0,0.7,0
c2.9-0.4,4.4-1,7.4-0.6l0.3-1.3c0.8-0.1,1.4,1.5,2.6,1.3c0.3-0.1-0.3-1.3-0.3-1.3c2.9,0.8,3.5,0.3,5.1-1.3c0.4,0.2,1.2-0.2,1.6,0
c0.1,0,0,1.1,0,1.3c0,0,1,0.3,1,0.3c0.3-0.2,0.5-1.4,1.3-1.6l4.2,0.3c0.3,0,0.8,0.7,1.6,0.6l0.6-1c0.6-0.2,3,0,4.5,0
c0-0.2-0.1-1.6,0-1.6c0.5-0.2,1.3,0.3,1.3,0.3s0.4,1.1,1.9,1c2.2-0.2,3.4-1.3,5.8-1c-0.4-0.5-0.8-1.1-1-1.6
c0.2-0.1,0.5,0.1,0.3-0.3c0.5,0.2,1.4,0,1.9,0c0,0.2-0.1,1.5,0,1.6c0.3-0.2,1,0.2,1.3,0c0.1-0.1,0-1.1,0-1.3c1.3,0.4,2.8,1,3.5,1
c1.9-0.2,1.9-1.7,2.9-1.9c2.1-0.4-0.2,1,2.9-0.6c2.6,1.7,2.5,0.6,4.8,0.3c1.5-0.1,2.3,0.8,4.2,0.6c3.1-0.3,3.9-1.3,7.4-1.6v-1
c1.3-0.3,1.1,0.8,1.9,0.6l0.3-1c2.5-0.2,2.8,0,4.2,0.6c0.1-0.2,0.5-1.2,0.6-1.3c3.1-0.8,5.5,0.6,7.1,0.3v-0.6c0.7,0,1.8,0,2.6,0
l0.6-1.3c1.3-0.3,1.4,0.9,1.6,1c0.6,0.1,3.7-0.2,4.8-0.3c2.9-0.4,8.7-1.1,11.3-2.3c0.3-0.2,0.8-1.5,1-1.6c1.5-0.5,3,0.8,3.9,1.3
c-0.3-1.5-0.7-2,1.9-2.6c0,0.1,0.4,2.2,0.3,1.9c0.6,0,1.1,0.7,2.6,0.3c0.2-0.1-0.6-0.6-0.6-0.6c0.8-0.2,1.1-0.2,1.6-0.3
c-0.2-0.4-0.6-1.3-0.6-1.3c2.3-0.8,2.9,1.2,3.2,1.3c0.6,0.2,1.6,0,2.3,0c-0.2-0.1,0.5-0.3,0-0.6c-2.7-2,3.3-0.6,5.1-0.6
c-0.7-0.6-0.4-0.5-0.6-1.3c0.5,0,1.2,0.2,1.6,0c0,0.3-0.2,1.8,0,1.9c0.2-0.1,0.7,0,1,0l-0.3-2.3c0,0,1.3,1,1.6,1.6
c-0.6,0.1-0.2,1,0.6,0.6c1.3-0.5,0.8-0.9,2.6,0c0.9-1.3,1.4-2,1.9-2.9c0.3,0.6,3,2.6,5.8,1.6c0,0,2.4-2.8,2.6-2.9
c2.7-0.9,5.4,0.1,7.7-1c0.5,0.7,0.6,1.3,1,1.9L601.4,390.8z M151.8,326.3c0.3-0.9,0.5-1.7,0.6-3.2l1-0.3c0,0,1.1,0.5,1.6,0.3
c0.1-0.1-0.1-0.8,0-1c0.1-0.1,1.2-0.5,1.3-0.6c0.2-0.4,0.1-1.6,0-1.3l3.1-5.5c0-0.1,1.6-2.1,1.6-2.2l0.8-1.6c1.6-2.1,0.1-2.8,1-5.5
c0.2-0.7,1.9-1.5,2.6-1.9l0.6,0.6c-0.3-2.3-0.4-3.1,1.3-4.2c0,0.1,0.3,1.6,0.3,1.6c0.7,0.3,1-0.5,1.3-0.3c0.1-1.3,1-2.8,1.4-3.4
c-0.1,0.1-0.1,0.1-0.2,0.3c2.5-3.3,2.1-5.4,2.3-7.5c1.6,0,1.3,0.7,1.9-1.6c3.3,1,3.1-0.3,1.9-2.9c1.3-0.6,0.3-1.6,0.3-1.6
c1-0.2,1.8,0.9,2.9,0.6c0.5-0.1,0-1,0-1c-0.2,0.7-0.6-0.6-0.6-0.6s1.3-1.9,1.3-1.9c0.2,0.1,0.5,1,0.6,1.3c0.1,0,1.3-1,1.3-1
c0-0.1,1-1.3,1-1.3c0.5-2.1,0.6-4.4,1-5.8c0,0,0.5,0.7,0.6,0.6c0.3,0.7,0.6,1.4,1,2.2c0.2-0.1,1-0.3,1-0.3c0.4-1.9,0.6-3.8,1-6.4
c0.7,0,0.4-0.8,1.3-0.6c0,0,1.1,0.8,1.3,1c0.1-0.4,0-0.8,0.3-1.6c0.3,0,1.4,0.2,1.6,0c0.2-0.2,0-1.3,0-1.6c1-0.4,0.5,0.3,1-0.6
l-2.3-1.3c1.1-0.6,2.7-0.8,3.9-1.6c1.2-0.8,1.4-3.3,2.6-4.2c0.8-0.6,1.7-0.2,2.6-0.6c1.2-0.6,1.8-2.6,2.6-3.2
c1.2-0.9,2.7-0.5,3.5-1.6c0.8-1.1-0.1-1.9,1-2.9c0.8-0.8,1.8-0.4,2.9-1c1.7-1,2.5-4,4.8-5.5c1.7-1.1,0.5-0.2,1-2.3
c0,0,0.8-0.6,0.3-0.6c1.3-0.7,2.3-0.2,3.2-0.6c0.6-0.3,0.7-1.8,1.3-2.3c0.3-0.2,1,0.1,1.3,0c0.2-0.1,0.1-1.1,0.3-1.3
c0.4-0.4,1.2,0,1.6-0.3c0.5-0.4,0.4-1.6,1-1.9c0.8-0.4,1.9,0,2.6-0.3c0.6-0.3,2.5-2.5,3.5-3.2c-0.1-0.1-0.5-0.5-0.3-0.6
c0.3-0.3,1.2,0.2,1.6,0c0.4-0.2,0.8-1.9,1.3-2.3c1.1-0.8,2.4-0.5,3.5-1.3c0.5-0.3,0.2-1.4,1-1.6c0.4-0.1,1.2,0.2,1.6,0
c0.7-0.4,0.7-1.8,1.3-2.3c2.2-1.8,3.8,0,6.4-3.2c1.9,0.5,1.2,0.6,3.2,0c2.8,3.9-0.2,8.1-1.6,11.9c-0.7,1.7-0.8,3-1.6,4.5
c-0.6,1.1-2,2.1-2.6,3.2c-0.8,1.8-0.6,2.9-1.6,4.5c-0.5,0.8-1.8,1.5-2.2,2.2c-0.5,0.8-0.4,1.9-1,2.6c-0.6,0.7-2.1,0.8-2.6,1.6
c-0.3,0.6,0.1,1.6-0.3,2.3c-0.5,0.8-1.2,0.1-1.9,0.6c-0.5,0.4,0,1-0.3,1.6c-0.2,0.3-1.1,0.4-1.3,0.6c-0.7,0.9-0.6,2-1.3,2.9
c-0.6,0.7-2.6,1.4-2.9,2.3c-0.2,0.6,0.1,1.3,0,1.9c-0.4,0-1.7-0.3-2.3,0c-0.4,0.2-0.4,1.5-0.6,1.9c-0.2,0.3-2.8,2.1-2.9,2.3
c-0.8,1.4-0.1,2.4-1.3,3.5c-1.1,1-1.4,0-2.6,0.6c-1.2,0.7-1.7,3.6-2.9,4.5c-1,0.7-2.5,0.5-3.5,1.3c-1,0.8-0.9,3.1-1.9,3.9
c-0.5,0.4-1.5,0-1.9,0.3c-1.3,0.8-1.6,3.4-2.9,4.2c-1.2,0.7-2.6,0-3.9,0.6c-0.4,0.2-0.3,1.4-0.6,1.6c-0.3,0.1-1.4-0.2-1.6,0
c-0.3,0.3,0.1,1.2,0,1.6c0,0-2.2,2.2-2.3,2.3c-1.4,0.7-2.2,0.1-3.9,1c-0.5,0.3-1.2,1.4-1.6,1.6c-1.5,0.8-2.6,0.3-3.5,1.6
c-0.8,1.2-0.7,2.5-1.6,3.5c-0.5,0.6-1.8,1-2.3,1.6c-0.2,0.3-0.1,1.1-0.3,1.3c-2.5,1.9-2.4-1.2-3.5,2.9c-0.2,0-1.5-0.2-1.6-0.3
c-0.4,0.5-1.4,1.7-1.9,2.2c-1.9,1.9-3.7,3-5.2,5.5c1.3-0.7,2.1-0.8,3.2-1.6l-1,1.3l-0.3,0.3c-0.1,0.1-0.4,1.4-0.6,1.6
c-1.2,0.9-3.1,0.7-4.2,1.6c-0.1,0.1,0,0.8,0,1c-1.8,0.9-3.2,2.8-4.8,3.5c-1.9,0.8,0.9-1.8,1.9-2.3l0.3-1.3
c-1.1,0.7-4.7,1.5-5.1,2.3c-0.7,1.2,0.2,1.6-0.3,2.6c-0.2,0.3-0.8,0.5-1.1,0.6c0,0-0.1,0-0.2,0c0,0,0.1,0,0.2,0
c0.1-0.2-1-0.7-1.8-0.3c-0.4,0.2-0.4,1.1-0.6,1.3c-1.8,1.2-2.9,1.5-5.5,2.6c0,0.2,0.1,1.5,0,1.6c-0.2-0.2-0.7-0.7-1-0.6
C151.6,327.4,151.5,326.9,151.8,326.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

2617
ul_tresorerie/js/app_v2.js Normal file

File diff suppressed because it is too large Load Diff

2617
ul_tresorerie/js/main.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\AppInfo;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
class Application extends App implements IBootstrap {
public const string APP_ID = 'ul_tresorerie';
public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
}
public function register(IRegistrationContext $context): void {
// Services and controllers are autowired via Nextcloud DI container
}
public function boot(IBootContext $context): void {
// Boot stage hook as required by Nextcloud 34 IBootstrap
}
}

View File

@ -0,0 +1,450 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\Controller;
use OCA\ULTresorerie\AppInfo\Application;
use OCA\ULTresorerie\Service\PermissionService;
use OCA\ULTresorerie\Service\TreasuryService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use Throwable;
class ApiController extends Controller {
public function __construct(
IRequest $request,
private readonly TreasuryService $treasuryService,
private readonly PermissionService $permissionService,
) {
parent::__construct(Application::APP_ID, $request);
}
private function ensureAuthorized(): ?JSONResponse {
if (!$this->permissionService->canAccessCurrentUser()) {
return new JSONResponse(
['error' => 'Accès refusé : vous ne disposez pas des droits nécessaires pour accéder à la Trésorerie UL CGT.'],
Http::STATUS_FORBIDDEN,
);
}
return null;
}
/**
* @param int|null $year
*/
#[NoAdminRequired]
public function getState(?int $year = null): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
try {
$targetYear = ($year !== null && $year >= 2000 && $year <= 2100)
? $year
: (int)date('Y');
$state = $this->treasuryService->getFullState($targetYear);
$userContext = $this->permissionService->getCurrentUserContext();
$state['userContext'] = $userContext;
if ($userContext['isAdmin']) {
$state['permissions'] = $this->permissionService->getPermissionsData();
}
return new JSONResponse($state);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* @param string $cogitiel_id
* @param string $civility
* @param string $last_name
* @param string $first_name
* @param string $phone_personal
* @param string $phone_pro
* @param string $address
* @param string $postal_code
* @param string $city
* @param string $email
* @param string $base_code
* @param string $base_name
* @param float $default_amount
* @param string $default_method
* @param int $is_archived
*/
#[NoAdminRequired]
public function createMember(
string $cogitiel_id = '',
string $civility = '',
string $last_name = '',
string $first_name = '',
string $phone_personal = '',
string $phone_pro = '',
string $address = '',
string $postal_code = '',
string $city = '',
string $email = '',
string $base_code = '90045110520251341',
string $base_name = 'INDIVIDUELS UL FOIX HAUTE-ARIEGE',
float $default_amount = 0.0,
string $default_method = 'Virement',
int $is_archived = 0,
): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
if (trim($last_name) === '' || trim($first_name) === '') {
return new JSONResponse(
['error' => 'Le nom et le prénom du syndiqué sont obligatoires.'],
Http::STATUS_BAD_REQUEST,
);
}
try {
$member = $this->treasuryService->createMember([
'cogitiel_id' => $cogitiel_id,
'civility' => $civility,
'last_name' => $last_name,
'first_name' => $first_name,
'phone_personal' => $phone_personal,
'phone_pro' => $phone_pro,
'address' => $address,
'postal_code' => $postal_code,
'city' => $city,
'email' => $email,
'base_code' => $base_code,
'base_name' => $base_name,
'default_amount' => $default_amount,
'default_method' => $default_method,
'is_archived' => $is_archived,
]);
return new JSONResponse(['member' => $member], Http::STATUS_CREATED);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param int $id
* @param string $cogitiel_id
* @param string $civility
* @param string $last_name
* @param string $first_name
* @param string $phone_personal
* @param string $phone_pro
* @param string $address
* @param string $postal_code
* @param string $city
* @param string $email
* @param string $base_code
* @param string $base_name
* @param float $default_amount
* @param string $default_method
* @param int $is_archived
*/
#[NoAdminRequired]
public function updateMember(
int $id,
string $cogitiel_id = '',
string $civility = '',
string $last_name = '',
string $first_name = '',
string $phone_personal = '',
string $phone_pro = '',
string $address = '',
string $postal_code = '',
string $city = '',
string $email = '',
string $base_code = '90045110520251341',
string $base_name = 'INDIVIDUELS UL FOIX HAUTE-ARIEGE',
float $default_amount = 0.0,
string $default_method = 'Virement',
int $is_archived = 0,
): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
if (trim($last_name) === '' || trim($first_name) === '') {
return new JSONResponse(
['error' => 'Le nom et le prénom du syndiqué sont obligatoires.'],
Http::STATUS_BAD_REQUEST,
);
}
try {
$member = $this->treasuryService->updateMember($id, [
'cogitiel_id' => $cogitiel_id,
'civility' => $civility,
'last_name' => $last_name,
'first_name' => $first_name,
'phone_personal' => $phone_personal,
'phone_pro' => $phone_pro,
'address' => $address,
'postal_code' => $postal_code,
'city' => $city,
'email' => $email,
'base_code' => $base_code,
'base_name' => $base_name,
'default_amount' => $default_amount,
'default_method' => $default_method,
'is_archived' => $is_archived,
]);
return new JSONResponse(['member' => $member]);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param int $id
*/
#[NoAdminRequired]
public function deleteMember(int $id): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
try {
$this->treasuryService->deleteMember($id);
return new JSONResponse([
'deleted' => true,
'id' => $id,
'multiYearStats' => $this->treasuryService->getMultiYearStats((int)date('Y')),
]);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param string|null $content
*/
#[NoAdminRequired]
public function importAsp(?string $content = null): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
try {
$rawContent = '';
$uploadedFile = $this->request->getUploadedFile('file');
if (is_array($uploadedFile) && isset($uploadedFile['tmp_name']) && is_uploaded_file((string)$uploadedFile['tmp_name'])) {
$rawContent = (string)file_get_contents((string)$uploadedFile['tmp_name']);
} elseif ($content !== null && trim($content) !== '') {
$rawContent = $content;
} else {
return new JSONResponse(
['error' => 'Aucun fichier .asp ou contenu valide reçu pour l\'importation.'],
Http::STATUS_BAD_REQUEST,
);
}
$summary = $this->treasuryService->importAspContent($rawContent);
return new JSONResponse($summary);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param int $member_id
* @param int $year
* @param int $month_start
* @param int $month_end
* @param float $amount
* @param string $payment_method
* @param string|null $note
*/
#[NoAdminRequired]
public function saveDue(
int $member_id,
int $year,
int $month_start,
int $month_end,
float $amount,
string $payment_method = 'Virement',
?string $note = null,
): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
if ($amount <= 0.0) {
return new JSONResponse(
['error' => 'Veuillez saisir un montant supérieur à 0,00 € (ou utilisez « Effacer ce mois »).'],
Http::STATUS_BAD_REQUEST,
);
}
try {
$savedDues = $this->treasuryService->saveDues(
$member_id,
$year,
$month_start,
$month_end,
$amount,
$payment_method,
$note,
);
return new JSONResponse([
'member_id' => $member_id,
'year' => $year,
'dues' => $savedDues,
'multiYearStats' => $this->treasuryService->getMultiYearStats($year),
]);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param int $member_id
* @param int $year
* @param int $month
*/
#[NoAdminRequired]
public function deleteDue(int $member_id, int $year, int $month): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
try {
$this->treasuryService->deleteDue($member_id, $year, $month);
return new JSONResponse([
'deleted' => true,
'member_id' => $member_id,
'year' => $year,
'month' => $month,
'multiYearStats' => $this->treasuryService->getMultiYearStats($year),
]);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param string $slug
* @param string $title
* @param string $subject
* @param string $body
*/
#[NoAdminRequired]
public function saveTemplate(
string $slug,
string $title,
string $subject,
string $body,
): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
try {
$template = $this->treasuryService->saveTemplate($slug, $title, $subject, $body);
return new JSONResponse(['template' => $template]);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
#[NoAdminRequired]
public function resetTemplates(): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
try {
$templates = $this->treasuryService->resetTemplates();
return new JSONResponse(['templates' => $templates]);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
/**
* @param int $year
* @param array<int, int> $member_ids
* @param string $subject
* @param string $body
*/
#[NoAdminRequired]
public function sendEmails(
int $year,
array $member_ids,
string $subject,
string $body,
): JSONResponse {
if (($denied = $this->ensureAuthorized()) !== null) {
return $denied;
}
if ($member_ids === []) {
return new JSONResponse(
['error' => 'Veuillez sélectionner au moins un destinataire.'],
Http::STATUS_BAD_REQUEST,
);
}
try {
$result = $this->treasuryService->sendMailingEmails($year, $member_ids, $subject, $body);
return new JSONResponse($result);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
#[NoAdminRequired]
public function getPermissions(): JSONResponse {
if (!$this->permissionService->isCurrentUserAdmin()) {
return new JSONResponse(
['error' => 'Seuls les administrateurs Nextcloud peuvent gérer les permissions de l\'application.'],
Http::STATUS_FORBIDDEN,
);
}
try {
return new JSONResponse($this->permissionService->getPermissionsData());
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* @param string $access_mode
* @param array<int, string> $allowed_groups
* @param array<int, string> $allowed_users
*/
#[NoAdminRequired]
public function savePermissions(
string $access_mode = 'restricted',
array $allowed_groups = [],
array $allowed_users = [],
): JSONResponse {
if (!$this->permissionService->isCurrentUserAdmin()) {
return new JSONResponse(
['error' => 'Seuls les administrateurs Nextcloud peuvent modifier les permissions de l\'application.'],
Http::STATUS_FORBIDDEN,
);
}
try {
$updated = $this->permissionService->savePermissionsData(
$access_mode,
$allowed_groups,
$allowed_users,
);
return new JSONResponse($updated);
} catch (Throwable $e) {
return new JSONResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\Controller;
use OCA\ULTresorerie\AppInfo\Application;
use OCA\ULTresorerie\Service\PermissionService;
use OCA\ULTresorerie\Service\TreasuryService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
class PageController extends Controller {
public function __construct(
IRequest $request,
private readonly TreasuryService $treasuryService,
private readonly PermissionService $permissionService,
) {
parent::__construct(Application::APP_ID, $request);
}
#[NoAdminRequired]
#[NoCSRFRequired]
public function index(): TemplateResponse {
$userContext = $this->permissionService->getCurrentUserContext();
if ($userContext['canAccess']) {
$this->treasuryService->ensureDefaultTemplates();
}
return new TemplateResponse(Application::APP_ID, 'main', [
'currentYear' => (int)date('Y'),
'canAccess' => $userContext['canAccess'],
'isAdmin' => $userContext['isAdmin'],
'userUid' => $userContext['uid'],
'userDisplayName' => $userContext['displayName'],
]);
}
}

View File

@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\Migration;
use Closure;
use OCA\ULTresorerie\Service\DefaultTemplates;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1000Date20261031 extends SimpleMigrationStep {
public function __construct(
private readonly IDBConnection $db,
) {
}
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
// 3.1. Table ul_cgt_members (Syndiqués)
if (!$schema->hasTable('ul_cgt_members')) {
$table = $schema->createTable('ul_cgt_members');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('cogitiel_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('civility', Types::STRING, [
'notnull' => false,
'length' => 16,
'default' => '',
]);
$table->addColumn('last_name', Types::STRING, [
'notnull' => true,
'length' => 128,
]);
$table->addColumn('first_name', Types::STRING, [
'notnull' => true,
'length' => 128,
]);
$table->addColumn('phone_personal', Types::STRING, [
'notnull' => false,
'length' => 32,
'default' => '',
]);
$table->addColumn('phone_pro', Types::STRING, [
'notnull' => false,
'length' => 32,
'default' => '',
]);
$table->addColumn('address', Types::STRING, [
'notnull' => false,
'length' => 255,
'default' => '',
]);
$table->addColumn('postal_code', Types::STRING, [
'notnull' => false,
'length' => 16,
'default' => '',
]);
$table->addColumn('city', Types::STRING, [
'notnull' => false,
'length' => 128,
'default' => '',
]);
$table->addColumn('email', Types::STRING, [
'notnull' => false,
'length' => 190,
'default' => '',
]);
$table->addColumn('base_code', Types::STRING, [
'notnull' => false,
'length' => 64,
'default' => '',
]);
$table->addColumn('base_name', Types::STRING, [
'notnull' => false,
'length' => 190,
'default' => '',
]);
$table->addColumn('default_amount', Types::DECIMAL, [
'notnull' => true,
'precision' => 10,
'scale' => 2,
'default' => '0.00',
]);
$table->addColumn('default_method', Types::STRING, [
'notnull' => true,
'length' => 32,
'default' => 'Virement',
]);
$table->addColumn('is_archived', Types::SMALLINT, [
'notnull' => true,
'default' => 0,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['cogitiel_id'], 'ul_cgt_mbr_cogitiel_uq');
$table->addIndex(['last_name', 'first_name'], 'ul_cgt_mbr_name_idx');
$table->addIndex(['is_archived'], 'ul_cgt_mbr_arch_idx');
}
// 3.2. Table ul_cgt_dues (Cotisations mensuelles)
if (!$schema->hasTable('ul_cgt_dues')) {
$table = $schema->createTable('ul_cgt_dues');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('member_id', Types::BIGINT, [
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('year', Types::INTEGER, [
'notnull' => true,
]);
$table->addColumn('month', Types::INTEGER, [
'notnull' => true,
]);
$table->addColumn('amount', Types::DECIMAL, [
'notnull' => true,
'precision' => 10,
'scale' => 2,
]);
$table->addColumn('payment_method', Types::STRING, [
'notnull' => true,
'length' => 32,
'default' => 'Virement',
]);
$table->addColumn('note', Types::STRING, [
'notnull' => false,
'length' => 255,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['member_id', 'year', 'month'], 'ul_cgt_dues_mbr_ym_uq');
$table->addIndex(['year'], 'ul_cgt_dues_year_idx');
}
// 3.3. Table ul_cgt_templates (Modèles de publipostage)
if (!$schema->hasTable('ul_cgt_templates')) {
$table = $schema->createTable('ul_cgt_templates');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 20,
'unsigned' => true,
]);
$table->addColumn('slug', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('title', Types::STRING, [
'notnull' => true,
'length' => 190,
]);
$table->addColumn('subject', Types::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('body', Types::TEXT, [
'notnull' => true,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['slug'], 'ul_cgt_tpl_slug_uq');
}
return $schema;
}
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
foreach (DefaultTemplates::getAll() as $template) {
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from('ul_cgt_templates')
->where($qb->expr()->eq('slug', $qb->createNamedParameter($template['slug'], IQueryBuilder::PARAM_STR)));
$result = $qb->executeQuery();
$existing = $result->fetchAssociative();
$result->closeCursor();
if ($existing === false) {
$insertQb = $this->db->getQueryBuilder();
$insertQb->insert('ul_cgt_templates')
->values([
'slug' => $insertQb->createNamedParameter($template['slug'], IQueryBuilder::PARAM_STR),
'title' => $insertQb->createNamedParameter($template['title'], IQueryBuilder::PARAM_STR),
'subject' => $insertQb->createNamedParameter($template['subject'], IQueryBuilder::PARAM_STR),
'body' => $insertQb->createNamedParameter($template['body'], IQueryBuilder::PARAM_STR),
])
->executeStatement();
}
}
}
}

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\Service;
final class AspImportParser {
/**
* Parse le contenu brut d'un fichier d'export CoGétise (.asp / CSV Windows-1252).
*
* @param string $rawContent Contenu brut du fichier .asp
* @return array<int, array{
* cogitiel_id: string,
* civility: string,
* last_name: string,
* first_name: string,
* phone_personal: string,
* phone_pro: string,
* address: string,
* postal_code: string,
* city: string,
* email: string,
* base_code: string,
* base_name: string,
* is_archived: int
* }>
*/
public function parse(string $rawContent): array {
// Suppression éventuelle du BOM UTF-8
if (str_starts_with($rawContent, "\xEF\xBB\xBF")) {
$rawContent = substr($rawContent, 3);
}
$rawLines = preg_split('/\r\n|\r|\n/', $rawContent) ?: [];
$parsedMembers = [];
$isHeaderSkipped = false;
foreach ($rawLines as $rawLine) {
// 1. Supprimer les espaces/tabulations de fin et convertir Windows-1252 -> UTF-8
$line = rtrim($rawLine, "\r\n\t");
if (trim($line) === '') {
continue;
}
if (!mb_check_encoding($line, 'UTF-8')) {
$line = mb_convert_encoding($line, 'UTF-8', 'Windows-1252');
}
// 2. Parser avec str_getcsv($line, ',', '"') et ignorer la première ligne d'en-tête
$columns = str_getcsv($line, ',', '"');
if (!$isHeaderSkipped) {
$isHeaderSkipped = true;
$firstCol = trim((string)($columns[0] ?? ''));
if ($firstCol === 'Code Responsable' || !ctype_digit($firstCol)) {
continue;
}
}
$cogitielId = trim((string)($columns[0] ?? ''));
if ($cogitielId === '' || $cogitielId === 'Code Responsable') {
continue;
}
$civility = trim((string)($columns[1] ?? ''));
$lastName = mb_strtoupper(trim((string)($columns[2] ?? '')), 'UTF-8');
$firstName = trim((string)($columns[3] ?? ''));
$phonePersonal = trim((string)($columns[4] ?? ''));
$phonePro = trim((string)($columns[5] ?? ''));
// 3. Concaténer proprement les colonnes 6, 7 et 8 non vides dans le champ address
$addressParts = [];
foreach ([6, 7, 8] as $colIndex) {
$part = trim((string)($columns[$colIndex] ?? ''));
if ($part !== '') {
$addressParts[] = $part;
}
}
$address = implode(', ', $addressParts);
$postalCode = trim((string)($columns[10] ?? ''));
$city = trim((string)($columns[11] ?? ''));
$email = trim((string)($columns[12] ?? ''));
$baseCode = trim((string)($columns[13] ?? ''));
$baseName = trim((string)($columns[14] ?? ''));
$isArchived = ((int)trim((string)($columns[15] ?? '0')) !== 0) ? 1 : 0;
if ($lastName === '' && $firstName === '') {
continue;
}
$parsedMembers[] = [
'cogitiel_id' => $cogitielId,
'civility' => $civility,
'last_name' => $lastName,
'first_name' => $firstName,
'phone_personal' => $phonePersonal,
'phone_pro' => $phonePro,
'address' => $address,
'postal_code' => $postalCode,
'city' => $city,
'email' => $email,
'base_code' => $baseCode,
'base_name' => $baseName !== '' ? $baseName : 'INDIVIDUELS UL FOIX HAUTE-ARIEGE',
'is_archived' => $isArchived,
];
}
return $parsedMembers;
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\Service;
final class DefaultTemplates {
/**
* Retourne les 3 modèles de publipostage pré-installés.
*
* @return array<int, array{slug: string, title: string, subject: string, body: string}>
*/
public static function getAll(): array {
return [
[
'slug' => 'tax_receipt',
'title' => '1. Reçu fiscal / Crédit d\'impôt 66 %',
'subject' => 'Attestation fiscale {{annee}} — Cotisations syndicales CGT ({{prenom}} {{nom}})',
'body' => <<<'EOT'
Foix, le {{date_du_jour}}
Destinataire :
{{civilite}} {{prenom}} {{nom}}
{{adresse_complete}}
Code syndiqué (CoGétise) : {{code_responsable}}
Objet : Attestation annuelle de versement de cotisations syndicales — Année {{annee}}
Chère camarade, cher camarade,
L'Union Locale CGT de Foix et de la Haute-Ariège atteste par la présente que {{civilite}} {{prenom}} {{nom}} (identifiant {{code_responsable}}), rattaché(e) à la base des Isolés / Multipros de l'UL CGT Foix Haute-Ariège, a acquitté au titre de l'année civile {{annee}} ({{nb_mois_payes}} mois réglé(s)) un montant total de cotisations syndicales de :
Montant total versé en {{annee}} : {{total_paye}}
Conformément aux dispositions de l'article 199 quater C du Code général des impôts, les cotisations versées aux organisations syndicales représentatives ouvrent droit à un crédit d'impôt sur le revenu égal à 66 % des sommes versées :
Montant de votre crédit d'impôt (66 %) : {{credit_impot}}
Conservez précieusement cette attestation afin de pouvoir la produire, le cas échéant, sur demande de l'administration fiscale lors de votre déclaration de revenus (case 7AC / 7AE / 7AG).
En te remerciant pour ta fidélité et ton engagement syndical à la CGT, nous t'adressons, chère camarade, cher camarade, nos salutations fraternelles et syndicalistes.
Pour la Trésorerie de l'Union Locale CGT de Foix et de la Haute-Ariège
Bourse du Travail — Foix (09000)
EOT,
],
[
'slug' => 'missing_months',
'title' => '2. Rappel des mois manquants (Alerte Orange)',
'subject' => 'Suivi de tes cotisations syndicales CGT {{annee}} — {{nb_mois_manquants}} mois en attente',
'body' => <<<'EOT'
Foix, le {{date_du_jour}}
Destinataire :
{{civilite}} {{prenom}} {{nom}}
{{adresse_complete}}
Code syndiqué (CoGétise) : {{code_responsable}}
Objet : Point sur tes cotisations syndicales CGT — Année {{annee}}
Chère camarade, cher camarade,
Lors du pointage de la trésorerie des syndiqués isolés (Multipros) de l'Union Locale CGT de Foix et de la Haute-Ariège pour l'année {{annee}}, nous avons bien enregistré tes règlements pour {{nb_mois_payes}} mois (soit un total perçu à ce jour de {{total_paye}}), et nous t'en remercions.
Sauf erreur ou omission de notre part (ou décalage de pointage bancaire récent), il apparaît toutefois que {{nb_mois_manquants}} échéance(s) mensuelle(s) ne figurent pas encore dans notre suivi pour l'année {{annee}} :
Mois non pointés : {{mois_manquants_liste}}
Cotisation mensuelle habituelle : {{cotisation_habituelle}}
Montant estimé à régulariser : {{montant_estime_retard}}
Les cotisations des syndiqués constituent la seule ressource financière garantissant l'indépendance de la CGT et les moyens d'action et de défense des salariés sur notre territoire de Foix et de la Haute-Ariège.
Si ton règlement a été effectué tout récemment ou si ta situation professionnelle ou personnelle a évolué (changement d'emploi, retraite, perte d'emploi), n'hésite pas à nous contacter afin que nous mettions à jour ta fiche et le montant de ta cotisation.
Restant à ton écoute, nous t'adressons, chère camarade, cher camarade, nos salutations fraternelles.
La Trésorerie de l'Union Locale CGT de Foix et de la Haute-Ariège
EOT,
],
[
'slug' => 'annual_zero_warning',
'title' => '3. Rappel annuel d\'absence de versement (Alerte Rouge — 0 €)',
'subject' => 'IMPORTANT : Régularisation de ton adhésion CGT pour l\'année {{annee}}',
'body' => <<<'EOT'
Foix, le {{date_du_jour}}
Destinataire :
{{civilite}} {{prenom}} {{nom}}
{{adresse_complete}}
Code syndiqué (CoGétise) : {{code_responsable}}
Objet : Absence de versement de cotisation enregistrée sur l'année {{annee}}
Chère camarade, cher camarade,
Sauf erreur de notre part, lors de la vérification des comptes de l'Union Locale CGT de Foix et de la Haute-Ariège pour l'année {{annee}}, nous constatons qu'aucun versement de cotisation syndicale (0,00 €) n'a été enregistré sur ton dossier depuis le début de l'année ({{nb_mois_manquants}} mois échus : {{mois_manquants_liste}}).
À titre indicatif, sur la base de ta cotisation mensuelle de référence de {{cotisation_habituelle}}, le montant cumulé représente {{montant_estime_retard}} (ouvrant droit à 66 % de crédit d'impôt).
Nous tenons à attirer fraternellement ton attention sur le fait que le paiement régulier des cotisations conditionne le maintien de ton statut de syndiqué(e) CGT ainsi que l'accès à l'accompagnement juridique et syndical de l'Union Locale. À défaut de régularisation ou de nouvelles de ta part, nous serions malheureusement contraints de procéder à l'archivage de ton dossier d'adhérent(e).
Si tu rencontres des difficultés financières passagères, un changement de situation (chômage, retraite, maladie, changement d'entreprise) ou un problème technique de virement/prélèvement, prends rapidement contact avec l'Union Locale : nous trouverons ensemble une solution adaptée.
Dans l'attente de ton retour, nous te prions de croire, chère camarade, cher camarade, en nos salutations syndicalistes et fraternelles.
La Trésorerie de l'Union Locale CGT de Foix et de la Haute-Ariège
EOT,
],
];
}
}

View File

@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\Service;
use OCA\ULTresorerie\AppInfo\Application;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\IUserSession;
class PermissionService {
private const string KEY_ACCESS_MODE = 'access_mode';
private const string KEY_ALLOWED_GROUPS = 'allowed_groups';
private const string KEY_ALLOWED_USERS = 'allowed_users';
public function __construct(
private readonly IConfig $config,
private readonly IUserSession $userSession,
private readonly IGroupManager $groupManager,
private readonly IUserManager $userManager,
) {
}
/**
* Retourne les informations de l'utilisateur courant et ses droits d'accès.
*
* @return array{uid: string, displayName: string, isAdmin: bool, canAccess: bool}
*/
public function getCurrentUserContext(): array {
$user = $this->userSession->getUser();
if ($user === null) {
return [
'uid' => '',
'displayName' => 'Invité',
'isAdmin' => false,
'canAccess' => false,
];
}
$uid = $user->getUID();
$isAdmin = $this->groupManager->isAdmin($uid);
return [
'uid' => $uid,
'displayName' => $user->getDisplayName(),
'isAdmin' => $isAdmin,
'canAccess' => $this->canAccessCurrentUser(),
];
}
/**
* Vérifie si l'utilisateur actuellement connecté est administrateur Nextcloud.
*/
public function isCurrentUserAdmin(): bool {
$user = $this->userSession->getUser();
if ($user === null) {
return false;
}
return $this->groupManager->isAdmin($user->getUID());
}
/**
* Vérifie si l'utilisateur actuellement connecté est autorisé à accéder à l'application.
*/
public function canAccessCurrentUser(): bool {
$user = $this->userSession->getUser();
if ($user === null) {
return false;
}
$uid = $user->getUID();
// Les administrateurs Nextcloud ont toujours accès
if ($this->groupManager->isAdmin($uid)) {
return true;
}
// Si le mode est ouvert à tous les utilisateurs connectés
if ($this->getAccessMode() === 'all_users') {
return true;
}
// Vérification dans la liste des utilisateurs explicitement autorisés
$allowedUsers = $this->getAllowedUsers();
if (in_array($uid, $allowedUsers, true)) {
return true;
}
// Vérification dans la liste des groupes autorisés
$allowedGroups = $this->getAllowedGroups();
foreach ($allowedGroups as $gid) {
if ($this->groupManager->isInGroup($uid, $gid)) {
return true;
}
}
return false;
}
public function getAccessMode(): string {
$mode = $this->config->getAppValue(Application::APP_ID, self::KEY_ACCESS_MODE, 'restricted');
return $mode === 'all_users' ? 'all_users' : 'restricted';
}
/**
* @return array<int, string>
*/
public function getAllowedGroups(): array {
$raw = $this->config->getAppValue(Application::APP_ID, self::KEY_ALLOWED_GROUPS, '[]');
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return [];
}
return array_values(array_unique(array_filter(array_map('strval', $decoded), static fn (string $v): bool => trim($v) !== '')));
}
/**
* @return array<int, string>
*/
public function getAllowedUsers(): array {
$raw = $this->config->getAppValue(Application::APP_ID, self::KEY_ALLOWED_USERS, '[]');
$decoded = json_decode($raw, true);
if (!is_array($decoded)) {
return [];
}
return array_values(array_unique(array_filter(array_map('strval', $decoded), static fn (string $v): bool => trim($v) !== '')));
}
/**
* Récupère la configuration complète des permissions ainsi que la liste des groupes et utilisateurs Nextcloud.
*
* @return array<string, mixed>
*/
public function getPermissionsData(): array {
$groups = [];
foreach ($this->groupManager->search('', 500, 0) as $group) {
$gid = $group->getGID();
$groups[] = [
'gid' => $gid,
'displayName' => method_exists($group, 'getDisplayName') ? $group->getDisplayName() : $gid,
];
}
usort($groups, static fn (array $a, array $b): int => strcasecmp($a['displayName'], $b['displayName']));
$users = [];
foreach ($this->userManager->search('', 500, 0) as $user) {
$uid = $user->getUID();
$users[] = [
'uid' => $uid,
'displayName' => $user->getDisplayName(),
'email' => (string)($user->getEMailAddress() ?? ''),
'isAdmin' => $this->groupManager->isAdmin($uid),
'groups' => $this->groupManager->getUserGroupIds($user),
];
}
usort($users, static fn (array $a, array $b): int => strcasecmp($a['displayName'], $b['displayName']));
return [
'access_mode' => $this->getAccessMode(),
'allowed_groups' => $this->getAllowedGroups(),
'allowed_users' => $this->getAllowedUsers(),
'available_groups' => $groups,
'available_users' => $users,
];
}
/**
* Enregistre la configuration des permissions d'accès à l'application.
*
* @param array<int, string> $allowedGroups
* @param array<int, string> $allowedUsers
* @return array<string, mixed>
*/
public function savePermissionsData(
string $accessMode,
array $allowedGroups,
array $allowedUsers,
): array {
$cleanMode = ($accessMode === 'all_users') ? 'all_users' : 'restricted';
$cleanGroups = array_values(array_unique(array_filter(
array_map(static fn ($g): string => trim((string)$g), $allowedGroups),
static fn (string $v): bool => $v !== '',
)));
$cleanUsers = array_values(array_unique(array_filter(
array_map(static fn ($u): string => trim((string)$u), $allowedUsers),
static fn (string $v): bool => $v !== '',
)));
$this->config->setAppValue(
Application::APP_ID,
self::KEY_ACCESS_MODE,
$cleanMode,
);
$this->config->setAppValue(
Application::APP_ID,
self::KEY_ALLOWED_GROUPS,
(string)json_encode($cleanGroups, JSON_UNESCAPED_UNICODE),
);
$this->config->setAppValue(
Application::APP_ID,
self::KEY_ALLOWED_USERS,
(string)json_encode($cleanUsers, JSON_UNESCAPED_UNICODE),
);
return $this->getPermissionsData();
}
}

View File

@ -0,0 +1,832 @@
<?php
declare(strict_types=1);
namespace OCA\ULTresorerie\Service;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Mail\IMailer;
use Throwable;
class TreasuryService {
private const array MONTH_NAMES_FR = [
1 => 'Janvier',
2 => 'Février',
3 => 'Mars',
4 => 'Avril',
5 => 'Mai',
6 => 'Juin',
7 => 'Juillet',
8 => 'Août',
9 => 'Septembre',
10 => 'Octobre',
11 => 'Novembre',
12 => 'Décembre',
];
private const array ALLOWED_METHODS = [
'Virement',
'Prélèvement',
'Chèque',
'Espèces',
];
public function __construct(
private readonly IDBConnection $db,
private readonly IMailer $mailer,
private readonly AspImportParser $importParser,
) {
}
/**
* S'assure que les 3 modèles de publipostage par défaut existent en base.
*/
public function ensureDefaultTemplates(): void {
foreach (DefaultTemplates::getAll() as $tpl) {
$qb = $this->db->getQueryBuilder();
$qb->select('id')
->from('ul_cgt_templates')
->where($qb->expr()->eq('slug', $qb->createNamedParameter($tpl['slug'], IQueryBuilder::PARAM_STR)));
$res = $qb->executeQuery();
$row = $res->fetchAssociative();
$res->closeCursor();
if ($row === false) {
$ins = $this->db->getQueryBuilder();
$ins->insert('ul_cgt_templates')
->values([
'slug' => $ins->createNamedParameter($tpl['slug'], IQueryBuilder::PARAM_STR),
'title' => $ins->createNamedParameter($tpl['title'], IQueryBuilder::PARAM_STR),
'subject' => $ins->createNamedParameter($tpl['subject'], IQueryBuilder::PARAM_STR),
'body' => $ins->createNamedParameter($tpl['body'], IQueryBuilder::PARAM_STR),
])
->executeStatement();
}
}
}
/**
* Récupère l'état complet pour une année donnée (syndiqués, cotisations, statistiques multi-années, modèles).
*
* @return array<string, mixed>
*/
public function getFullState(int $year): array {
$this->ensureDefaultTemplates();
// 1. Syndiqués
$qbMembers = $this->db->getQueryBuilder();
$qbMembers->select('*')
->from('ul_cgt_members')
->orderBy('last_name', 'ASC')
->addOrderBy('first_name', 'ASC');
$resMembers = $qbMembers->executeQuery();
$rawMembers = $resMembers->fetchAllAssociative();
$resMembers->closeCursor();
$members = array_map([$this, 'normalizeMemberRow'], $rawMembers);
// 2. Cotisations de l'année sélectionnée
$qbDues = $this->db->getQueryBuilder();
$qbDues->select('*')
->from('ul_cgt_dues')
->where($qbDues->expr()->eq('year', $qbDues->createNamedParameter($year, IQueryBuilder::PARAM_INT)))
->orderBy('month', 'ASC');
$resDues = $qbDues->executeQuery();
$rawDues = $resDues->fetchAllAssociative();
$resDues->closeCursor();
$duesByMember = [];
foreach ($rawDues as $dueRow) {
$memberId = (int)$dueRow['member_id'];
$month = (int)$dueRow['month'];
if (!isset($duesByMember[$memberId])) {
$duesByMember[$memberId] = [];
}
$duesByMember[$memberId][$month] = [
'id' => (int)$dueRow['id'],
'member_id' => $memberId,
'year' => (int)$dueRow['year'],
'month' => $month,
'amount' => round((float)$dueRow['amount'], 2),
'payment_method' => (string)$dueRow['payment_method'],
'note' => (string)($dueRow['note'] ?? ''),
];
}
// 3. Modèles de publipostage
$templates = $this->getTemplates();
// 4. Statistiques multi-années pour le Tableau de Bord
$multiYearStats = $this->getMultiYearStats($year);
return [
'year' => $year,
'currentYear' => (int)date('Y'),
'currentMonth' => (int)date('n'),
'currentDay' => (int)date('j'),
'members' => $members,
'dues' => (object)$duesByMember,
'templates' => $templates,
'multiYearStats' => $multiYearStats,
];
}
/**
* Calcule les agrégats mensuels, annuels et par mode de paiement sur toutes les années.
*
* @return array<string, mixed>
*/
public function getMultiYearStats(int $focusYear): array {
$currentYear = (int)date('Y');
$qbAll = $this->db->getQueryBuilder();
$qbAll->select('member_id', 'year', 'month', 'amount', 'payment_method')
->from('ul_cgt_dues');
$resAll = $qbAll->executeQuery();
$allDues = $resAll->fetchAllAssociative();
$resAll->closeCursor();
$yearsSet = [
$currentYear - 2 => true,
$currentYear - 1 => true,
$currentYear => true,
$focusYear - 1 => true,
$focusYear => true,
];
foreach ($allDues as $row) {
$y = (int)$row['year'];
if ($y >= 2000 && $y <= 2100) {
$yearsSet[$y] = true;
}
}
$availableYears = array_keys($yearsSet);
sort($availableYears);
$byYear = [];
$payingMembersByYear = [];
foreach ($availableYears as $y) {
$monthlyTotals = [];
$monthlyCounts = [];
for ($m = 1; $m <= 12; $m++) {
$monthlyTotals[$m] = 0.0;
$monthlyCounts[$m] = 0;
}
$byMethod = [];
foreach (self::ALLOWED_METHODS as $methodName) {
$byMethod[$methodName] = [
'amount' => 0.0,
'count' => 0,
];
}
$byYear[$y] = [
'year' => $y,
'totalAmount' => 0.0,
'duesCount' => 0,
'payingMembersCount' => 0,
'monthlyTotals' => $monthlyTotals,
'monthlyCounts' => $monthlyCounts,
'byMethod' => $byMethod,
];
$payingMembersByYear[$y] = [];
}
foreach ($allDues as $row) {
$y = (int)$row['year'];
$m = (int)$row['month'];
$amt = round((float)$row['amount'], 2);
$memberId = (int)$row['member_id'];
$method = $this->sanitizeMethod((string)($row['payment_method'] ?? 'Virement'));
if (!isset($byYear[$y]) || $m < 1 || $m > 12 || $amt <= 0.0) {
continue;
}
$byYear[$y]['totalAmount'] = round($byYear[$y]['totalAmount'] + $amt, 2);
$byYear[$y]['duesCount']++;
$byYear[$y]['monthlyTotals'][$m] = round($byYear[$y]['monthlyTotals'][$m] + $amt, 2);
$byYear[$y]['monthlyCounts'][$m]++;
$payingMembersByYear[$y][$memberId] = true;
$byYear[$y]['byMethod'][$method]['amount'] = round($byYear[$y]['byMethod'][$method]['amount'] + $amt, 2);
$byYear[$y]['byMethod'][$method]['count']++;
}
foreach ($availableYears as $y) {
$byYear[$y]['payingMembersCount'] = count($payingMembersByYear[$y]);
}
return [
'availableYears' => $availableYears,
'byYear' => (object)$byYear,
];
}
/**
* @return array<int, array<string, mixed>>
*/
public function getTemplates(): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('ul_cgt_templates')
->orderBy('id', 'ASC');
$res = $qb->executeQuery();
$rows = $res->fetchAllAssociative();
$res->closeCursor();
return array_map(static fn (array $r): array => [
'id' => (int)$r['id'],
'slug' => (string)$r['slug'],
'title' => (string)$r['title'],
'subject' => (string)$r['subject'],
'body' => (string)$r['body'],
], $rows);
}
/**
* Crée un nouveau syndiqué.
*
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function createMember(array $data): array {
$cogitielId = trim((string)($data['cogitiel_id'] ?? ''));
if ($cogitielId === '') {
$cogitielId = 'UL09-' . date('YmdHis') . '-' . random_int(100, 999);
}
$civility = trim((string)($data['civility'] ?? ''));
$lastName = mb_strtoupper(trim((string)($data['last_name'] ?? '')), 'UTF-8');
$firstName = trim((string)($data['first_name'] ?? ''));
$phonePersonal = trim((string)($data['phone_personal'] ?? ''));
$phonePro = trim((string)($data['phone_pro'] ?? ''));
$address = trim((string)($data['address'] ?? ''));
$postalCode = trim((string)($data['postal_code'] ?? ''));
$city = mb_strtoupper(trim((string)($data['city'] ?? '')), 'UTF-8');
$email = trim((string)($data['email'] ?? ''));
$baseCode = trim((string)($data['base_code'] ?? '90045110520251341'));
$baseName = trim((string)($data['base_name'] ?? 'INDIVIDUELS UL FOIX HAUTE-ARIEGE'));
$defaultAmount = round(max(0.0, (float)($data['default_amount'] ?? 0.0)), 2);
$defaultMethod = $this->sanitizeMethod((string)($data['default_method'] ?? 'Virement'));
$isArchived = !empty($data['is_archived']) ? 1 : 0;
$qb = $this->db->getQueryBuilder();
$qb->insert('ul_cgt_members')
->values([
'cogitiel_id' => $qb->createNamedParameter($cogitielId, IQueryBuilder::PARAM_STR),
'civility' => $qb->createNamedParameter($civility, IQueryBuilder::PARAM_STR),
'last_name' => $qb->createNamedParameter($lastName, IQueryBuilder::PARAM_STR),
'first_name' => $qb->createNamedParameter($firstName, IQueryBuilder::PARAM_STR),
'phone_personal' => $qb->createNamedParameter($phonePersonal, IQueryBuilder::PARAM_STR),
'phone_pro' => $qb->createNamedParameter($phonePro, IQueryBuilder::PARAM_STR),
'address' => $qb->createNamedParameter($address, IQueryBuilder::PARAM_STR),
'postal_code' => $qb->createNamedParameter($postalCode, IQueryBuilder::PARAM_STR),
'city' => $qb->createNamedParameter($city, IQueryBuilder::PARAM_STR),
'email' => $qb->createNamedParameter($email, IQueryBuilder::PARAM_STR),
'base_code' => $qb->createNamedParameter($baseCode, IQueryBuilder::PARAM_STR),
'base_name' => $qb->createNamedParameter($baseName, IQueryBuilder::PARAM_STR),
'default_amount' => $qb->createNamedParameter(number_format($defaultAmount, 2, '.', ''), IQueryBuilder::PARAM_STR),
'default_method' => $qb->createNamedParameter($defaultMethod, IQueryBuilder::PARAM_STR),
'is_archived' => $qb->createNamedParameter($isArchived, IQueryBuilder::PARAM_INT),
])
->executeStatement();
$newId = (int)$this->db->lastInsertId('*PREFIX*ul_cgt_members');
return $this->getMemberById($newId);
}
/**
* Met à jour la fiche d'un syndiqué existant.
*
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
public function updateMember(int $id, array $data): array {
$cogitielId = trim((string)($data['cogitiel_id'] ?? ''));
$civility = trim((string)($data['civility'] ?? ''));
$lastName = mb_strtoupper(trim((string)($data['last_name'] ?? '')), 'UTF-8');
$firstName = trim((string)($data['first_name'] ?? ''));
$phonePersonal = trim((string)($data['phone_personal'] ?? ''));
$phonePro = trim((string)($data['phone_pro'] ?? ''));
$address = trim((string)($data['address'] ?? ''));
$postalCode = trim((string)($data['postal_code'] ?? ''));
$city = mb_strtoupper(trim((string)($data['city'] ?? '')), 'UTF-8');
$email = trim((string)($data['email'] ?? ''));
$baseCode = trim((string)($data['base_code'] ?? ''));
$baseName = trim((string)($data['base_name'] ?? 'INDIVIDUELS UL FOIX HAUTE-ARIEGE'));
$defaultAmount = round(max(0.0, (float)($data['default_amount'] ?? 0.0)), 2);
$defaultMethod = $this->sanitizeMethod((string)($data['default_method'] ?? 'Virement'));
$isArchived = !empty($data['is_archived']) ? 1 : 0;
$qb = $this->db->getQueryBuilder();
$qb->update('ul_cgt_members')
->set('civility', $qb->createNamedParameter($civility, IQueryBuilder::PARAM_STR))
->set('last_name', $qb->createNamedParameter($lastName, IQueryBuilder::PARAM_STR))
->set('first_name', $qb->createNamedParameter($firstName, IQueryBuilder::PARAM_STR))
->set('phone_personal', $qb->createNamedParameter($phonePersonal, IQueryBuilder::PARAM_STR))
->set('phone_pro', $qb->createNamedParameter($phonePro, IQueryBuilder::PARAM_STR))
->set('address', $qb->createNamedParameter($address, IQueryBuilder::PARAM_STR))
->set('postal_code', $qb->createNamedParameter($postalCode, IQueryBuilder::PARAM_STR))
->set('city', $qb->createNamedParameter($city, IQueryBuilder::PARAM_STR))
->set('email', $qb->createNamedParameter($email, IQueryBuilder::PARAM_STR))
->set('base_code', $qb->createNamedParameter($baseCode, IQueryBuilder::PARAM_STR))
->set('base_name', $qb->createNamedParameter($baseName, IQueryBuilder::PARAM_STR))
->set('default_amount', $qb->createNamedParameter(number_format($defaultAmount, 2, '.', ''), IQueryBuilder::PARAM_STR))
->set('default_method', $qb->createNamedParameter($defaultMethod, IQueryBuilder::PARAM_STR))
->set('is_archived', $qb->createNamedParameter($isArchived, IQueryBuilder::PARAM_INT))
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
if ($cogitielId !== '') {
$qb->set('cogitiel_id', $qb->createNamedParameter($cogitielId, IQueryBuilder::PARAM_STR));
}
$qb->executeStatement();
return $this->getMemberById($id);
}
/**
* Supprime un syndiqué et toutes ses cotisations rattachées.
*/
public function deleteMember(int $id): void {
$this->db->beginTransaction();
try {
$qbDues = $this->db->getQueryBuilder();
$qbDues->delete('ul_cgt_dues')
->where($qbDues->expr()->eq('member_id', $qbDues->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->executeStatement();
$qbMember = $this->db->getQueryBuilder();
$qbMember->delete('ul_cgt_members')
->where($qbMember->expr()->eq('id', $qbMember->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->executeStatement();
$this->db->commit();
} catch (Throwable $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* Importe le contenu d'un fichier .asp CoGétise avec Upsert sur cogitiel_id.
* Ne remplace jamais default_amount, default_method ni les cotisations existantes.
*
* @return array{inserted: int, updated: int, total: int}
*/
public function importAspContent(string $rawContent): array {
$parsedRows = $this->importParser->parse($rawContent);
$inserted = 0;
$updated = 0;
$this->db->beginTransaction();
try {
foreach ($parsedRows as $row) {
$qbFind = $this->db->getQueryBuilder();
$qbFind->select('id')
->from('ul_cgt_members')
->where($qbFind->expr()->eq('cogitiel_id', $qbFind->createNamedParameter($row['cogitiel_id'], IQueryBuilder::PARAM_STR)));
$res = $qbFind->executeQuery();
$existing = $res->fetchAssociative();
$res->closeCursor();
if ($existing !== false) {
// Mise à jour administrative sans écraser default_amount ni default_method
$qbUpd = $this->db->getQueryBuilder();
$qbUpd->update('ul_cgt_members')
->set('civility', $qbUpd->createNamedParameter($row['civility'], IQueryBuilder::PARAM_STR))
->set('last_name', $qbUpd->createNamedParameter($row['last_name'], IQueryBuilder::PARAM_STR))
->set('first_name', $qbUpd->createNamedParameter($row['first_name'], IQueryBuilder::PARAM_STR))
->set('phone_personal', $qbUpd->createNamedParameter($row['phone_personal'], IQueryBuilder::PARAM_STR))
->set('phone_pro', $qbUpd->createNamedParameter($row['phone_pro'], IQueryBuilder::PARAM_STR))
->set('address', $qbUpd->createNamedParameter($row['address'], IQueryBuilder::PARAM_STR))
->set('postal_code', $qbUpd->createNamedParameter($row['postal_code'], IQueryBuilder::PARAM_STR))
->set('city', $qbUpd->createNamedParameter($row['city'], IQueryBuilder::PARAM_STR))
->set('email', $qbUpd->createNamedParameter($row['email'], IQueryBuilder::PARAM_STR))
->set('base_code', $qbUpd->createNamedParameter($row['base_code'], IQueryBuilder::PARAM_STR))
->set('base_name', $qbUpd->createNamedParameter($row['base_name'], IQueryBuilder::PARAM_STR))
->set('is_archived', $qbUpd->createNamedParameter($row['is_archived'], IQueryBuilder::PARAM_INT))
->where($qbUpd->expr()->eq('id', $qbUpd->createNamedParameter((int)$existing['id'], IQueryBuilder::PARAM_INT)))
->executeStatement();
$updated++;
} else {
$qbIns = $this->db->getQueryBuilder();
$qbIns->insert('ul_cgt_members')
->values([
'cogitiel_id' => $qbIns->createNamedParameter($row['cogitiel_id'], IQueryBuilder::PARAM_STR),
'civility' => $qbIns->createNamedParameter($row['civility'], IQueryBuilder::PARAM_STR),
'last_name' => $qbIns->createNamedParameter($row['last_name'], IQueryBuilder::PARAM_STR),
'first_name' => $qbIns->createNamedParameter($row['first_name'], IQueryBuilder::PARAM_STR),
'phone_personal' => $qbIns->createNamedParameter($row['phone_personal'], IQueryBuilder::PARAM_STR),
'phone_pro' => $qbIns->createNamedParameter($row['phone_pro'], IQueryBuilder::PARAM_STR),
'address' => $qbIns->createNamedParameter($row['address'], IQueryBuilder::PARAM_STR),
'postal_code' => $qbIns->createNamedParameter($row['postal_code'], IQueryBuilder::PARAM_STR),
'city' => $qbIns->createNamedParameter($row['city'], IQueryBuilder::PARAM_STR),
'email' => $qbIns->createNamedParameter($row['email'], IQueryBuilder::PARAM_STR),
'base_code' => $qbIns->createNamedParameter($row['base_code'], IQueryBuilder::PARAM_STR),
'base_name' => $qbIns->createNamedParameter($row['base_name'], IQueryBuilder::PARAM_STR),
'default_amount' => $qbIns->createNamedParameter('0.00', IQueryBuilder::PARAM_STR),
'default_method' => $qbIns->createNamedParameter('Virement', IQueryBuilder::PARAM_STR),
'is_archived' => $qbIns->createNamedParameter($row['is_archived'], IQueryBuilder::PARAM_INT),
])
->executeStatement();
$inserted++;
}
}
$this->db->commit();
} catch (Throwable $e) {
$this->db->rollBack();
throw $e;
}
return [
'inserted' => $inserted,
'updated' => $updated,
'total' => $inserted + $updated,
];
}
/**
* Enregistre (Upsert) une cotisation sur un ou plusieurs mois consécutifs.
*
* @return array<int, array<string, mixed>>
*/
public function saveDues(
int $memberId,
int $year,
int $monthStart,
int $monthEnd,
float $amount,
string $paymentMethod,
?string $note = null,
): array {
$monthStart = max(1, min(12, $monthStart));
$monthEnd = max($monthStart, min(12, $monthEnd));
$amount = round(max(0.0, $amount), 2);
$method = $this->sanitizeMethod($paymentMethod);
$cleanNote = $note !== null ? trim($note) : '';
$savedDues = [];
$this->db->beginTransaction();
try {
for ($month = $monthStart; $month <= $monthEnd; $month++) {
$qbFind = $this->db->getQueryBuilder();
$qbFind->select('id')
->from('ul_cgt_dues')
->where($qbFind->expr()->eq('member_id', $qbFind->createNamedParameter($memberId, IQueryBuilder::PARAM_INT)))
->andWhere($qbFind->expr()->eq('year', $qbFind->createNamedParameter($year, IQueryBuilder::PARAM_INT)))
->andWhere($qbFind->expr()->eq('month', $qbFind->createNamedParameter($month, IQueryBuilder::PARAM_INT)));
$res = $qbFind->executeQuery();
$existing = $res->fetchAssociative();
$res->closeCursor();
if ($existing !== false) {
$dueId = (int)$existing['id'];
$qbUpd = $this->db->getQueryBuilder();
$qbUpd->update('ul_cgt_dues')
->set('amount', $qbUpd->createNamedParameter(number_format($amount, 2, '.', ''), IQueryBuilder::PARAM_STR))
->set('payment_method', $qbUpd->createNamedParameter($method, IQueryBuilder::PARAM_STR))
->set('note', $qbUpd->createNamedParameter($cleanNote, IQueryBuilder::PARAM_STR))
->where($qbUpd->expr()->eq('id', $qbUpd->createNamedParameter($dueId, IQueryBuilder::PARAM_INT)))
->executeStatement();
} else {
$qbIns = $this->db->getQueryBuilder();
$qbIns->insert('ul_cgt_dues')
->values([
'member_id' => $qbIns->createNamedParameter($memberId, IQueryBuilder::PARAM_INT),
'year' => $qbIns->createNamedParameter($year, IQueryBuilder::PARAM_INT),
'month' => $qbIns->createNamedParameter($month, IQueryBuilder::PARAM_INT),
'amount' => $qbIns->createNamedParameter(number_format($amount, 2, '.', ''), IQueryBuilder::PARAM_STR),
'payment_method' => $qbIns->createNamedParameter($method, IQueryBuilder::PARAM_STR),
'note' => $qbIns->createNamedParameter($cleanNote, IQueryBuilder::PARAM_STR),
])
->executeStatement();
$dueId = (int)$this->db->lastInsertId('*PREFIX*ul_cgt_dues');
}
$savedDues[$month] = [
'id' => $dueId,
'member_id' => $memberId,
'year' => $year,
'month' => $month,
'amount' => $amount,
'payment_method' => $method,
'note' => $cleanNote,
];
}
// Met à jour le montant et le mode de référence du syndiqué si un montant > 0 est saisi
if ($amount > 0) {
$qbMbr = $this->db->getQueryBuilder();
$qbMbr->update('ul_cgt_members')
->set('default_amount', $qbMbr->createNamedParameter(number_format($amount, 2, '.', ''), IQueryBuilder::PARAM_STR))
->set('default_method', $qbMbr->createNamedParameter($method, IQueryBuilder::PARAM_STR))
->where($qbMbr->expr()->eq('id', $qbMbr->createNamedParameter($memberId, IQueryBuilder::PARAM_INT)))
->executeStatement();
}
$this->db->commit();
} catch (Throwable $e) {
$this->db->rollBack();
throw $e;
}
return $savedDues;
}
/**
* Supprime la cotisation d'un mois donné pour un syndiqué.
*/
public function deleteDue(int $memberId, int $year, int $month): void {
$qb = $this->db->getQueryBuilder();
$qb->delete('ul_cgt_dues')
->where($qb->expr()->eq('member_id', $qb->createNamedParameter($memberId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('year', $qb->createNamedParameter($year, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('month', $qb->createNamedParameter($month, IQueryBuilder::PARAM_INT)))
->executeStatement();
}
/**
* Sauvegarde les modifications d'un modèle de publipostage.
*
* @return array<string, mixed>
*/
public function saveTemplate(string $slug, string $title, string $subject, string $body): array {
$qb = $this->db->getQueryBuilder();
$qb->update('ul_cgt_templates')
->set('title', $qb->createNamedParameter(trim($title), IQueryBuilder::PARAM_STR))
->set('subject', $qb->createNamedParameter(trim($subject), IQueryBuilder::PARAM_STR))
->set('body', $qb->createNamedParameter($body, IQueryBuilder::PARAM_STR))
->where($qb->expr()->eq('slug', $qb->createNamedParameter($slug, IQueryBuilder::PARAM_STR)))
->executeStatement();
$qbGet = $this->db->getQueryBuilder();
$qbGet->select('*')
->from('ul_cgt_templates')
->where($qbGet->expr()->eq('slug', $qbGet->createNamedParameter($slug, IQueryBuilder::PARAM_STR)));
$res = $qbGet->executeQuery();
$row = $res->fetchAssociative();
$res->closeCursor();
if ($row === false) {
return [];
}
return [
'id' => (int)$row['id'],
'slug' => (string)$row['slug'],
'title' => (string)$row['title'],
'subject' => (string)$row['subject'],
'body' => (string)$row['body'],
];
}
/**
* Réinitialise les 3 modèles de publipostage avec leur contenu d'origine.
*
* @return array<int, array<string, mixed>>
*/
public function resetTemplates(): array {
foreach (DefaultTemplates::getAll() as $tpl) {
$this->saveTemplate($tpl['slug'], $tpl['title'], $tpl['subject'], $tpl['body']);
}
return $this->getTemplates();
}
/**
* Envoie par e-mail via OCP\Mail\IMailer les courriers personnalisés aux syndiqués sélectionnés.
*
* @param array<int, int> $memberIds
* @return array{sent: int, skipped_no_email: int, errors: array<int, string>}
*/
public function sendMailingEmails(
int $year,
array $memberIds,
string $subjectTemplate,
string $bodyTemplate,
): array {
$state = $this->getFullState($year);
$membersById = [];
foreach ($state['members'] as $m) {
$membersById[(int)$m['id']] = $m;
}
$duesMap = (array)$state['dues'];
$sent = 0;
$skippedNoEmail = 0;
$errors = [];
foreach ($memberIds as $rawId) {
$memberId = (int)$rawId;
if (!isset($membersById[$memberId])) {
continue;
}
$member = $membersById[$memberId];
$email = trim((string)($member['email'] ?? ''));
if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
$skippedNoEmail++;
continue;
}
$memberDues = $duesMap[$memberId] ?? [];
$renderedSubject = $this->renderTemplateVariables($subjectTemplate, $member, $memberDues, $year);
$renderedBody = $this->renderTemplateVariables($bodyTemplate, $member, $memberDues, $year);
$htmlBody = $this->buildEmailHtml($renderedSubject, $renderedBody);
try {
$recipientName = trim($member['first_name'] . ' ' . $member['last_name']);
$message = $this->mailer->createMessage();
$message->setSubject($renderedSubject);
$message->setPlainBody($renderedBody);
$message->setHtmlBody($htmlBody);
$message->setTo([$email => $recipientName]);
$failedRecipients = $this->mailer->send($message);
if (!empty($failedRecipients)) {
$errors[] = sprintf('%s (%s) : échec SMTP', $recipientName, $email);
} else {
$sent++;
}
} catch (Throwable $e) {
$errors[] = sprintf('%s (%s) : %s', $member['last_name'], $email, $e->getMessage());
}
}
return [
'sent' => $sent,
'skipped_no_email' => $skippedNoEmail,
'errors' => $errors,
];
}
/**
* Remplace toutes les variables dynamiques {{...}} pour un syndiqué et une année donnés.
*
* @param array<string, mixed> $member
* @param array<int, array<string, mixed>> $memberDues
*/
public function renderTemplateVariables(
string $text,
array $member,
array $memberDues,
int $year,
): string {
$currentYear = (int)date('Y');
$currentMonth = (int)date('n');
$currentDay = (int)date('j');
if ($year < $currentYear) {
$maxCheckMonth = 12;
} elseif ($year > $currentYear) {
$maxCheckMonth = 0;
} else {
$maxCheckMonth = ($currentDay > 10) ? $currentMonth : max(0, $currentMonth - 1);
}
$totalPaye = 0.0;
$nbMoisPayes = 0;
$lastPaidAmount = 0.0;
for ($m = 1; $m <= 12; $m++) {
$amt = isset($memberDues[$m]) ? (float)$memberDues[$m]['amount'] : 0.0;
if ($amt > 0.0) {
$totalPaye += $amt;
$nbMoisPayes++;
$lastPaidAmount = $amt;
}
}
$missingMonthsNames = [];
for ($m = 1; $m <= $maxCheckMonth; $m++) {
$amt = isset($memberDues[$m]) ? (float)$memberDues[$m]['amount'] : 0.0;
if ($amt <= 0.0) {
$missingMonthsNames[] = self::MONTH_NAMES_FR[$m];
}
}
$nbMoisManquants = count($missingMonthsNames);
$moisManquantsListe = $nbMoisManquants > 0 ? implode(', ', $missingMonthsNames) : 'Aucun';
$cotisationHabituelle = (float)($member['default_amount'] ?? 0.0);
if ($cotisationHabituelle <= 0.0 && $lastPaidAmount > 0.0) {
$cotisationHabituelle = $lastPaidAmount;
}
$creditImpot = round($totalPaye * 0.66, 2);
$montantEstimeRetard = round($nbMoisManquants * $cotisationHabituelle, 2);
$addressParts = [];
if (trim((string)$member['address']) !== '') {
$addressParts[] = trim((string)$member['address']);
}
$cpCity = trim(trim((string)$member['postal_code']) . ' ' . trim((string)$member['city']));
if ($cpCity !== '') {
$addressParts[] = $cpCity;
}
$adresseComplete = $addressParts !== [] ? implode(' — ', $addressParts) : 'Adresse non renseignée';
$replacements = [
'{{civilite}}' => (string)($member['civility'] ?? ''),
'{{nom}}' => (string)($member['last_name'] ?? ''),
'{{prenom}}' => (string)($member['first_name'] ?? ''),
'{{adresse_complete}}' => $adresseComplete,
'{{code_responsable}}' => (string)($member['cogitiel_id'] ?? ''),
'{{annee}}' => (string)$year,
'{{date_du_jour}}' => date('d/m/Y'),
'{{total_paye}}' => $this->formatEuros($totalPaye),
'{{credit_impot}}' => $this->formatEuros($creditImpot),
'{{nb_mois_payes}}' => (string)$nbMoisPayes,
'{{mois_manquants_liste}}' => $moisManquantsListe,
'{{nb_mois_manquants}}' => (string)$nbMoisManquants,
'{{cotisation_habituelle}}' => $this->formatEuros($cotisationHabituelle),
'{{montant_estime_retard}}' => $this->formatEuros($montantEstimeRetard),
];
return strtr($text, $replacements);
}
private function formatEuros(float $amount): string {
return number_format($amount, 2, ',', ' ') . ' €';
}
private function buildEmailHtml(string $subject, string $plainBody): string {
$escapedSubject = htmlspecialchars($subject, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$escapedBody = nl2br(htmlspecialchars($plainBody, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
return <<<HTML
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>{$escapedSubject}</title>
</head>
<body style="font-family: Arial, Helvetica, sans-serif; color: #1f2937; line-height: 1.6; background-color: #f9fafb; padding: 24px;">
<div style="max-width: 680px; margin: 0 auto; background: #ffffff; border: 1px solid #e5e7eb; border-top: 5px solid #d32f2f; border-radius: 6px; padding: 28px;">
<div style="border-bottom: 2px solid #f3f4f6; padding-bottom: 14px; margin-bottom: 20px;">
<strong style="color: #d32f2f; font-size: 18px;">UNION LOCALE CGT DE FOIX ET DE LA HAUTE-ARIÈGE</strong><br>
<span style="color: #6b7280; font-size: 13px;">Suivi des Cotisations Syndicales — Isolés / Multipros</span>
</div>
<div style="font-size: 14px; color: #111827;">
{$escapedBody}
</div>
</div>
</body>
</html>
HTML;
}
/**
* @return array<string, mixed>
*/
private function getMemberById(int $id): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('ul_cgt_members')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
$res = $qb->executeQuery();
$row = $res->fetchAssociative();
$res->closeCursor();
if ($row === false) {
return [];
}
return $this->normalizeMemberRow($row);
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private function normalizeMemberRow(array $row): array {
return [
'id' => (int)$row['id'],
'cogitiel_id' => (string)$row['cogitiel_id'],
'civility' => (string)($row['civility'] ?? ''),
'last_name' => (string)$row['last_name'],
'first_name' => (string)$row['first_name'],
'phone_personal' => (string)($row['phone_personal'] ?? ''),
'phone_pro' => (string)($row['phone_pro'] ?? ''),
'address' => (string)($row['address'] ?? ''),
'postal_code' => (string)($row['postal_code'] ?? ''),
'city' => (string)($row['city'] ?? ''),
'email' => (string)($row['email'] ?? ''),
'base_code' => (string)($row['base_code'] ?? ''),
'base_name' => (string)($row['base_name'] ?? ''),
'default_amount' => round((float)($row['default_amount'] ?? 0.0), 2),
'default_method' => (string)($row['default_method'] ?? 'Virement'),
'is_archived' => (int)($row['is_archived'] ?? 0),
];
}
private function sanitizeMethod(string $method): string {
$trimmed = trim($method);
if (in_array($trimmed, self::ALLOWED_METHODS, true)) {
return $trimmed;
}
return 'Virement';
}
}

View File

@ -0,0 +1,847 @@
<?php
declare(strict_types=1);
/** @var array $_ */
script('ul_tresorerie', 'app_v2');
style('ul_tresorerie', 'app_v2');
$currentYear = (string)($_['currentYear'] ?? date('Y'));
$canAccess = (bool)($_['canAccess'] ?? false);
$isAdmin = (bool)($_['isAdmin'] ?? false);
$userUid = (string)($_['userUid'] ?? '');
$userDisplayName = (string)($_['userDisplayName'] ?? $userUid);
$logoUrl = image_path('ul_tresorerie', 'cgt_logo.svg');
$bannerUrl = image_path('ul_tresorerie', 'banniere_ul.png');
?>
<div
id="ul-tresorerie-app"
class="ul-app-wrapper"
data-initial-year="<?php p($currentYear); ?>"
data-can-access="<?php p($canAccess ? '1' : '0'); ?>"
data-is-admin="<?php p($isAdmin ? '1' : '0'); ?>"
data-logo-url="<?php p($logoUrl); ?>"
data-banner-url="<?php p($bannerUrl); ?>"
>
<?php if (!$canAccess): ?>
<div class="ul-access-denied-wrapper">
<div class="ul-access-denied-card">
<img
src="<?php p($bannerUrl); ?>"
alt="Union Locale CGT Foix Haute-Ariège"
class="ul-access-denied-banner"
width="320"
height="68"
/>
<div class="ul-access-denied-badge">🔒 Accès restreint — Trésorerie UL CGT</div>
<h2>Vous n'avez pas encore accès à l'application UL Trésorerie</h2>
<p>
Cette application de suivi des cotisations syndicales est réservée aux trésoriers et membres habilités de
l'<strong>Union Locale CGT de Foix et de la Haute-Ariège</strong>.
</p>
<div class="ul-access-denied-userbox">
<span>Compte Nextcloud connecté :</span>
<strong><?php p($userDisplayName); ?> (<code><?php p($userUid); ?></code>)</strong>
</div>
<p class="ul-access-denied-help">
Demandez à un administrateur Nextcloud d'autoriser votre compte ou votre groupe depuis l'onglet
<strong>« Accès &amp; Permissions »</strong> de l'application.
</p>
</div>
</div>
<?php else: ?>
<!-- Barre latérale gauche rétractable -->
<div id="app-navigation" class="ul-no-print">
<div class="ul-brand-header">
<div class="ul-brand-banner-box">
<img
src="<?php p($bannerUrl); ?>"
alt="Union Locale CGT Foix Haute-Ariège"
class="ul-brand-banner-img"
width="210"
height="46"
/>
<button
type="button"
id="btn-close-sidebar"
class="ul-sidebar-close-btn"
data-action="toggle-sidebar"
title="Fermer le volet latéral gauche"
aria-label="Fermer le volet latéral gauche"
>&laquo;</button>
</div>
<div class="ul-brand-subtitle-bar">
<span>Suivi Cotisations — Isolés / Multipros</span>
</div>
</div>
<div class="ul-nav-section-title">Vues principales</div>
<ul class="ul-nav-list">
<li>
<a href="#grid" id="nav-view-grid" class="ul-nav-item active" data-view="grid">
<span class="ul-nav-icon">📊</span>
<span class="ul-nav-label">Grille des cotisations</span>
</a>
</li>
<li>
<a href="#dashboard" id="nav-view-dashboard" class="ul-nav-item" data-view="dashboard">
<span class="ul-nav-icon">📈</span>
<span class="ul-nav-label">Tableau de bord</span>
</a>
</li>
<li>
<a href="#mailing" id="nav-view-mailing" class="ul-nav-item" data-view="mailing">
<span class="ul-nav-icon">✉️</span>
<span class="ul-nav-label">Publipostage &amp; Courriers</span>
</a>
</li>
<?php if ($isAdmin): ?>
<li>
<a href="#permissions" id="nav-view-permissions" class="ul-nav-item" data-view="permissions">
<span class="ul-nav-icon">🔒</span>
<span class="ul-nav-label">Accès &amp; Permissions</span>
</a>
</li>
<?php endif; ?>
</ul>
<div class="ul-nav-section-title">Filtres &amp; Alertes (<span id="nav-year-indicator"><?php p($currentYear); ?></span>)</div>
<ul class="ul-nav-list ul-filter-list">
<li>
<a href="#filter-all" id="filter-btn-all" class="ul-filter-item active" data-filter="all">
<span class="ul-filter-label">👥 Tous les syndiqués</span>
<span id="count-all" class="ul-counter">0</span>
</a>
</li>
<li>
<a href="#filter-uptodate" id="filter-btn-uptodate" class="ul-filter-item" data-filter="uptodate">
<span class="ul-filter-label">🟢 Cotisations à jour</span>
<span id="count-uptodate" class="ul-counter ul-counter-green">0</span>
</a>
</li>
<li>
<a href="#filter-orange" id="filter-btn-orange" class="ul-filter-item" data-filter="orange">
<span class="ul-filter-label">🟠 Cotisations pas à jour (<span id="count-orange-inline">0</span>)</span>
<span id="count-orange" class="ul-counter ul-counter-orange">0</span>
</a>
</li>
<li>
<a href="#filter-red" id="filter-btn-red" class="ul-filter-item" data-filter="red">
<span class="ul-filter-label">🔴 Aucun versement sur l'année (<span id="count-red-inline">0</span>)</span>
<span id="count-red" class="ul-counter ul-counter-red">0</span>
</a>
</li>
</ul>
<div class="ul-nav-summary-card">
<div class="ul-summary-row">
<span>Année active :</span>
<strong id="summary-year"><?php p($currentYear); ?></strong>
</div>
<div class="ul-summary-row">
<span>Syndiqués actifs :</span>
<strong id="summary-active-count">0</strong>
</div>
<div class="ul-summary-row ul-summary-total">
<span>Total perçu :</span>
<strong id="summary-grand-total">0,00 €</strong>
</div>
</div>
<div class="ul-nav-actions">
<button type="button" id="btn-nav-new-member" class="ul-btn ul-btn-secondary ul-btn-block">
+ Nouveau syndiqué
</button>
<button type="button" id="btn-nav-import-asp" class="ul-btn ul-btn-outline ul-btn-block">
📥 Importer fichier .asp
</button>
</div>
</div>
<!-- Zone centrale #app-content -->
<div id="app-content" class="ul-no-print">
<!-- Bannière de notification -->
<div id="ul-toast" class="ul-toast hidden" role="status" aria-live="polite"></div>
<!-- VUE 1 : GRILLE ANNUELLE DES COTISATIONS -->
<section id="view-grid" class="ul-view-section">
<header class="ul-toolbar">
<div class="ul-toolbar-left">
<button
type="button"
class="ul-btn-toggle-sidebar"
data-action="toggle-sidebar"
title="Ouvrir / Fermer le volet latéral gauche"
aria-label="Ouvrir ou fermer le volet latéral gauche"
>☰</button>
<div class="ul-year-selector" aria-label="Sélecteur d'année comptable">
<button type="button" id="btn-prev-year" class="ul-btn-icon" title="Année précédente">&lt;</button>
<span id="label-current-year" class="ul-year-label"><?php p($currentYear); ?></span>
<button type="button" id="btn-next-year" class="ul-btn-icon" title="Année suivante">&gt;</button>
</div>
<div class="ul-search-box">
<input
type="search"
id="input-search"
class="ul-search-input"
placeholder="Rechercher nom, prénom, ville, e-mail, n° CoGétise…"
autocomplete="off"
/>
</div>
<label class="ul-checkbox-label" for="chk-show-archived">
<input type="checkbox" id="chk-show-archived" />
<span>Afficher les syndiqués archivés</span>
</label>
</div>
<div class="ul-toolbar-right">
<button type="button" id="btn-new-member" class="ul-btn ul-btn-primary">
+ Nouveau syndiqué
</button>
<button type="button" id="btn-import-asp" class="ul-btn ul-btn-secondary">
📥 Importer fichier .asp
</button>
<input type="file" id="input-import-file" class="hidden" accept=".asp,.csv,.txt" />
</div>
</header>
<div class="ul-legend-bar">
<span class="ul-legend-item">
<span class="ul-legend-swatch ul-swatch-paid"></span> Case verte : cotisation réglée (clic = modifier)
</span>
<span class="ul-legend-item">
<span class="ul-legend-swatch ul-swatch-empty"></span> Case grise : non cotisé (clic = saisie pré-remplie + Entrée)
</span>
<span class="ul-legend-item">
<strong>V</strong> Virement · <strong>P</strong> Prélèvement · <strong>C</strong> Chèque · <strong>E</strong> Espèces
</span>
<span id="active-filter-indicator" class="ul-active-filter-pill hidden"></span>
</div>
<div class="ul-grid-container">
<table id="dues-table" class="ul-dues-table">
<thead>
<tr>
<th class="col-sticky-left">Syndiqué (Nom, Prénom &amp; Statut)</th>
<th class="col-month">Janv.</th>
<th class="col-month">Févr.</th>
<th class="col-month">Mars</th>
<th class="col-month">Avr.</th>
<th class="col-month">Mai</th>
<th class="col-month">Juin</th>
<th class="col-month">Juil.</th>
<th class="col-month">Août</th>
<th class="col-month">Sept.</th>
<th class="col-month">Oct.</th>
<th class="col-month">Nov.</th>
<th class="col-month">Déc.</th>
<th class="col-sticky-right">Total Annuel</th>
</tr>
</thead>
<tbody id="dues-tbody">
<tr>
<td colspan="14" class="ul-empty-cell">Chargement des données de trésorerie…</td>
</tr>
</tbody>
<tfoot id="dues-tfoot">
<tr class="ul-tfoot-row">
<th class="col-sticky-left">Total Mensuel (<span id="tfoot-members-count">0</span> affichés)</th>
<th id="foot-month-1" class="col-month-total">0,00 €</th>
<th id="foot-month-2" class="col-month-total">0,00 €</th>
<th id="foot-month-3" class="ul-month-total col-month-total">0,00 €</th>
<th id="foot-month-4" class="col-month-total">0,00 €</th>
<th id="foot-month-5" class="col-month-total">0,00 €</th>
<th id="foot-month-6" class="col-month-total">0,00 €</th>
<th id="foot-month-7" class="col-month-total">0,00 €</th>
<th id="foot-month-8" class="col-month-total">0,00 €</th>
<th id="foot-month-9" class="col-month-total">0,00 €</th>
<th id="foot-month-10" class="col-month-total">0,00 €</th>
<th id="foot-month-11" class="col-month-total">0,00 €</th>
<th id="foot-month-12" class="col-month-total">0,00 €</th>
<th id="foot-grand-total" class="col-sticky-right col-grand-total">0,00 €</th>
</tr>
</tfoot>
</table>
</div>
</section>
<!-- VUE 2 : TABLEAU DE BORD & COMPARAISON PLURIANNUELLE (1 SEUL ÉCRAN) -->
<section id="view-dashboard" class="ul-view-section hidden">
<header class="ul-toolbar ul-dashboard-toolbar">
<div class="ul-toolbar-left">
<button
type="button"
class="ul-btn-toggle-sidebar"
data-action="toggle-sidebar"
title="Ouvrir / Fermer le volet latéral gauche"
aria-label="Ouvrir ou fermer le volet latéral gauche"
>☰</button>
<img src="<?php p($logoUrl); ?>" alt="CGT" class="ul-toolbar-cgt-logo" width="28" height="36" />
<div>
<h2 class="ul-section-heading">
Tableau de bord &amp; Évolution des Cotisations
</h2>
<span class="ul-dashboard-subtitle">Vue synthétique annuelle et comparative inter-années — UL CGT Foix Haute-Ariège</span>
</div>
</div>
<div class="ul-toolbar-right">
<div class="ul-dash-control-group">
<label class="ul-field-label-inline">Année analysée :</label>
<div class="ul-year-selector">
<button type="button" id="btn-dash-prev-year" class="ul-btn-icon" title="Année précédente">&lt;</button>
<span id="dash-label-year" class="ul-year-label"><?php p($currentYear); ?></span>
<button type="button" id="btn-dash-next-year" class="ul-btn-icon" title="Année suivante">&gt;</button>
</div>
</div>
<div class="ul-dash-control-group">
<label for="dash-select-compare-year" class="ul-field-label-inline">Comparer avec :</label>
<select id="dash-select-compare-year" class="ul-select ul-select-sm ul-dash-compare-select"></select>
</div>
</div>
</header>
<div class="ul-dashboard-content">
<!-- Ligne 1 : 5 Chiffres Clés (KPIs) -->
<div class="ul-kpi-grid">
<div class="ul-kpi-card ul-kpi-card-primary">
<div class="ul-kpi-top">
<span class="ul-kpi-label">Total encaissé (<span id="kpi-year-label"><?php p($currentYear); ?></span>)</span>
<span id="kpi-total-diff" class="ul-kpi-trend">0 %</span>
</div>
<div id="kpi-total-amount" class="ul-kpi-value">0,00 €</div>
<div id="kpi-total-sub" class="ul-kpi-sub">Année de comparaison : 0,00 €</div>
</div>
<div class="ul-kpi-card">
<div class="ul-kpi-top">
<span class="ul-kpi-label">Moyenne mensuelle</span>
<span class="ul-kpi-icon">📅</span>
</div>
<div id="kpi-monthly-avg" class="ul-kpi-value">0,00 €</div>
<div id="kpi-avg-due" class="ul-kpi-sub">Cotisation moy. : 0,00 € / mois</div>
</div>
<div class="ul-kpi-card">
<div class="ul-kpi-top">
<span class="ul-kpi-label">Syndiqués à jour</span>
<span class="ul-kpi-icon">🟢</span>
</div>
<div id="kpi-recovery-rate" class="ul-kpi-value">0 %</div>
<div id="kpi-recovery-sub" class="ul-kpi-sub">0 à jour sur 0 syndiqués actifs</div>
</div>
<div class="ul-kpi-card">
<div class="ul-kpi-top">
<span class="ul-kpi-label">Retards estimés</span>
<span class="ul-kpi-icon">🟠</span>
</div>
<div id="kpi-estimated-arrears" class="ul-kpi-value ul-kpi-value-orange">0,00 €</div>
<div id="kpi-arrears-sub" class="ul-kpi-sub">0 mois manquant(s) cumulé(s)</div>
</div>
<div class="ul-kpi-card">
<div class="ul-kpi-top">
<span class="ul-kpi-label">Crédit d'impôt 66 %</span>
<span class="ul-kpi-icon">🧾</span>
</div>
<div id="kpi-tax-credit" class="ul-kpi-value">0,00 €</div>
<div id="kpi-dues-count" class="ul-kpi-sub">0 mensualité(s) pointée(s)</div>
</div>
</div>
<!-- Ligne 2 : Graphiques principaux -->
<div class="ul-dash-charts-grid">
<!-- Graphique 1 : Évolution mensuelle & Cumul annuel (Année N vs Année comparée) -->
<div class="ul-card ul-dash-chart-card ul-dash-chart-main">
<div class="ul-card-header-flex">
<div>
<h3 class="ul-card-title">
Évolution mensuelle &amp; Cumul annuel (<span id="chart-main-year-label"><?php p($currentYear); ?></span> vs <span id="chart-main-comp-label">N-1</span>)
</h3>
<span class="ul-chart-subtitle">Barres : encaissements mensuels · Courbes : cumul progressif sur l'année</span>
</div>
<div class="ul-chart-legend">
<span class="ul-chart-legend-item">
<span class="ul-legend-dot ul-dot-red"></span> <span id="legend-year-curr"><?php p($currentYear); ?></span> (Mensuel &amp; Cumul)
</span>
<span class="ul-chart-legend-item">
<span class="ul-legend-dot ul-dot-slate"></span> <span id="legend-year-comp">N-1</span> (Mensuel &amp; Cumul)
</span>
</div>
</div>
<div id="dash-chart-monthly-comparison" class="ul-svg-chart-wrap"></div>
</div>
<!-- Colonne droite : Comparaison multi-années + Modes de paiement & Statuts -->
<div class="ul-dash-side-charts">
<div class="ul-card ul-dash-chart-card">
<div class="ul-card-header-flex">
<h3 class="ul-card-title">Comparaison pluriannuelle</h3>
<span class="ul-chart-subtitle">Cliquer sur une année pour la charger</span>
</div>
<div id="dash-chart-multiyear" class="ul-svg-chart-wrap ul-svg-chart-compact"></div>
</div>
<div class="ul-card ul-dash-chart-card">
<div class="ul-card-header-flex">
<h3 class="ul-card-title">Modes de règlement &amp; État des syndiqués</h3>
</div>
<div id="dash-chart-methods-status" class="ul-methods-status-wrap"></div>
</div>
</div>
</div>
<!-- Ligne 3 : Tableau comparatif mensuel synthétique -->
<div class="ul-card ul-dash-table-card">
<div class="ul-table-responsive">
<table class="ul-dash-comp-table">
<thead>
<tr>
<th>Comparatif Mensuel</th>
<th>Janv.</th>
<th>Févr.</th>
<th>Mars</th>
<th>Avr.</th>
<th>Mai</th>
<th>Juin</th>
<th>Juil.</th>
<th>Août</th>
<th>Sept.</th>
<th>Oct.</th>
<th>Nov.</th>
<th>Déc.</th>
<th>Total Année</th>
</tr>
</thead>
<tbody id="dash-comp-tbody"></tbody>
</table>
</div>
</div>
</div>
</section>
<!-- VUE 3 : OUTIL DE PUBLIPOSTAGE & COURRIERS -->
<section id="view-mailing" class="ul-view-section hidden">
<header class="ul-toolbar">
<div class="ul-toolbar-left">
<button
type="button"
class="ul-btn-toggle-sidebar"
data-action="toggle-sidebar"
title="Ouvrir / Fermer le volet latéral gauche"
aria-label="Ouvrir ou fermer le volet latéral gauche"
>☰</button>
<h2 class="ul-section-heading">
Publipostage &amp; Courriers en série — Année <span id="mailing-year-label"><?php p($currentYear); ?></span>
</h2>
</div>
<div class="ul-toolbar-right">
<button type="button" id="btn-print-mailing" class="ul-btn ul-btn-secondary">
🖨️ Imprimer / Export PDF groupé (<span id="btn-print-count">0</span>)
</button>
<button type="button" id="btn-email-mailing" class="ul-btn ul-btn-primary">
📧 Envoyer par e-mail (<span id="btn-email-count">0</span>)
</button>
</div>
</header>
<div class="ul-mailing-workspace">
<!-- Colonne gauche : Modèle & Destinataires -->
<div class="ul-mailing-sidebar">
<div class="ul-card">
<label for="select-template" class="ul-field-label">1. Choisir le modèle de courrier :</label>
<select id="select-template" class="ul-select">
<option value="tax_receipt">1. Reçu fiscal / Crédit d'impôt 66 %</option>
<option value="missing_months">2. Rappel des mois manquants (Alerte Orange)</option>
<option value="annual_zero_warning">3. Rappel annuel d'absence de versement (Alerte Rouge — 0 €)</option>
</select>
<p id="mailing-auto-rule-hint" class="ul-hint-text">
Sélection automatique : syndiqués actifs ayant versé plus de 0,00 € sur l'année.
</p>
</div>
<div class="ul-card ul-recipients-card">
<div class="ul-recipients-header">
<span class="ul-field-label">2. Destinataires sélectionnés (<strong id="mailing-selected-count">0</strong>)</span>
<div class="ul-mini-actions">
<button type="button" id="btn-mailing-check-all" class="ul-btn-link">Tout cocher</button>
<span>·</span>
<button type="button" id="btn-mailing-uncheck-all" class="ul-btn-link">Tout décocher</button>
</div>
</div>
<div class="ul-recipients-filters">
<label class="ul-checkbox-label" for="chk-mailing-no-email-only">
<input type="checkbox" id="chk-mailing-no-email-only" />
<span>Syndiqués sans adresse e-mail uniquement (courrier papier)</span>
</label>
</div>
<div id="mailing-recipients-list" class="ul-recipients-list"></div>
</div>
</div>
<!-- Colonne droite : Éditeur de modèle & Aperçu A4 -->
<div class="ul-mailing-main">
<div class="ul-card">
<div class="ul-card-header-flex">
<h3 class="ul-card-title">3. Personnaliser le modèle sélectionné</h3>
<div class="ul-card-actions">
<button type="button" id="btn-reset-templates" class="ul-btn ul-btn-outline ul-btn-sm">
Réinitialiser les modèles
</button>
<button type="button" id="btn-save-template" class="ul-btn ul-btn-primary ul-btn-sm">
💾 Enregistrer le modèle
</button>
</div>
</div>
<div class="ul-form-row">
<div class="ul-form-group">
<label for="tpl-title" class="ul-field-label">Intitulé du modèle</label>
<input type="text" id="tpl-title" class="ul-input" />
</div>
<div class="ul-form-group">
<label for="tpl-subject" class="ul-field-label">Objet de l'e-mail / courrier</label>
<input type="text" id="tpl-subject" class="ul-input" />
</div>
</div>
<div class="ul-variables-box">
<span class="ul-variables-title">Variables dynamiques (cliquer pour insérer au curseur) :</span>
<div id="tpl-variables-bar" class="ul-variables-pills">
<button type="button" class="ul-var-pill" data-var="{{civilite}}">{{civilite}}</button>
<button type="button" class="ul-var-pill" data-var="{{nom}}">{{nom}}</button>
<button type="button" class="ul-var-pill" data-var="{{prenom}}">{{prenom}}</button>
<button type="button" class="ul-var-pill" data-var="{{adresse_complete}}">{{adresse_complete}}</button>
<button type="button" class="ul-var-pill" data-var="{{code_responsable}}">{{code_responsable}}</button>
<button type="button" class="ul-var-pill" data-var="{{annee}}">{{annee}}</button>
<button type="button" class="ul-var-pill" data-var="{{date_du_jour}}">{{date_du_jour}}</button>
<button type="button" class="ul-var-pill" data-var="{{total_paye}}">{{total_paye}}</button>
<button type="button" class="ul-var-pill" data-var="{{credit_impot}}">{{credit_impot}}</button>
<button type="button" class="ul-var-pill" data-var="{{nb_mois_payes}}">{{nb_mois_payes}}</button>
<button type="button" class="ul-var-pill" data-var="{{mois_manquants_liste}}">{{mois_manquants_liste}}</button>
<button type="button" class="ul-var-pill" data-var="{{nb_mois_manquants}}">{{nb_mois_manquants}}</button>
<button type="button" class="ul-var-pill" data-var="{{cotisation_habituelle}}">{{cotisation_habituelle}}</button>
<button type="button" class="ul-var-pill" data-var="{{montant_estime_retard}}">{{montant_estime_retard}}</button>
</div>
</div>
<div class="ul-form-group">
<label for="tpl-body" class="ul-field-label">Corps du courrier</label>
<textarea id="tpl-body" class="ul-textarea" rows="12"></textarea>
</div>
</div>
<div class="ul-card">
<div class="ul-card-header-flex">
<h3 class="ul-card-title">4. Aperçu en direct du courrier généré</h3>
<div class="ul-preview-selector-wrap">
<label for="select-preview-member" class="ul-field-label-inline">Syndiqué prévisualisé :</label>
<select id="select-preview-member" class="ul-select ul-select-sm"></select>
</div>
</div>
<div id="mailing-live-preview" class="ul-letter-preview"></div>
</div>
</div>
</div>
</section>
<?php if ($isAdmin): ?>
<!-- VUE 4 : GESTION DES PERMISSIONS D'ACCÈS (ADMINISTRATEURS) -->
<section id="view-permissions" class="ul-view-section hidden">
<header class="ul-toolbar">
<div class="ul-toolbar-left">
<button
type="button"
class="ul-btn-toggle-sidebar"
data-action="toggle-sidebar"
title="Ouvrir / Fermer le volet latéral gauche"
aria-label="Ouvrir ou fermer le volet latéral gauche"
>☰</button>
<h2 class="ul-section-heading">
🔒 Gestion des permissions d'accès à UL Trésorerie
</h2>
</div>
<div class="ul-toolbar-right">
<button type="button" id="btn-save-permissions" class="ul-btn ul-btn-primary">
💾 Enregistrer les permissions
</button>
</div>
</header>
<div class="ul-permissions-workspace">
<div class="ul-card ul-permissions-mode-card">
<h3 class="ul-card-title">1. Politique générale d'accès à l'application</h3>
<p class="ul-hint-text">
Les administrateurs Nextcloud disposent toujours d'un accès complet à l'application et à cette page de configuration.
</p>
<div class="ul-perm-mode-options">
<label class="ul-perm-mode-option" for="perm-mode-restricted">
<input type="radio" name="perm-access-mode" id="perm-mode-restricted" value="restricted" checked />
<div>
<strong>Accès restreint aux groupes et membres sélectionnés (Recommandé)</strong>
<span>Seuls les administrateurs Nextcloud, les membres des groupes cochés ci-dessous et les utilisateurs explicitement autorisés peuvent ouvrir l'application.</span>
</div>
</label>
<label class="ul-perm-mode-option" for="perm-mode-all">
<input type="radio" name="perm-access-mode" id="perm-mode-all" value="all_users" />
<div>
<strong>Tous les utilisateurs connectés à ce Nextcloud</strong>
<span>Tout compte utilisateur actif sur l'instance Nextcloud peut accéder à l'application de trésorerie.</span>
</div>
</label>
</div>
</div>
<div id="perm-selectors-grid" class="ul-permissions-grid">
<!-- Colonne Groupes Nextcloud -->
<div class="ul-card ul-perm-list-card">
<div class="ul-card-header-flex">
<div>
<h3 class="ul-card-title">2. Groupes Nextcloud autorisés (<span id="perm-groups-count">0</span>)</h3>
<span class="ul-chart-subtitle">Tout membre d'un groupe coché aura accès à l'application</span>
</div>
<div class="ul-mini-actions">
<button type="button" id="btn-perm-groups-all" class="ul-btn-link">Tout cocher</button>
<span>·</span>
<button type="button" id="btn-perm-groups-none" class="ul-btn-link">Tout décocher</button>
</div>
</div>
<input
type="search"
id="input-perm-search-groups"
class="ul-input ul-perm-search"
placeholder="Filtrer les groupes Nextcloud…"
autocomplete="off"
/>
<div id="perm-groups-list" class="ul-perm-checklist"></div>
</div>
<!-- Colonne Utilisateurs Nextcloud -->
<div class="ul-card ul-perm-list-card">
<div class="ul-card-header-flex">
<div>
<h3 class="ul-card-title">3. Utilisateurs autorisés individuellement (<span id="perm-users-count">0</span>)</h3>
<span class="ul-chart-subtitle">Autoriser des comptes spécifiques même s'ils ne sont pas dans un groupe</span>
</div>
<div class="ul-mini-actions">
<button type="button" id="btn-perm-users-all" class="ul-btn-link">Tout cocher</button>
<span>·</span>
<button type="button" id="btn-perm-users-none" class="ul-btn-link">Tout décocher</button>
</div>
</div>
<input
type="search"
id="input-perm-search-users"
class="ul-input ul-perm-search"
placeholder="Rechercher un utilisateur par nom, identifiant ou e-mail…"
autocomplete="off"
/>
<div id="perm-users-list" class="ul-perm-checklist"></div>
</div>
</div>
</div>
</section>
<?php endif; ?>
</div>
<!-- MODALE 1 : SAISIE D'UNE COTISATION AU CLIC -->
<div id="due-modal" class="ul-modal-backdrop hidden ul-no-print" role="dialog" aria-modal="true" aria-labelledby="due-modal-title">
<div class="ul-modal-dialog ul-modal-sm">
<header class="ul-modal-header">
<div>
<h3 id="due-modal-title" class="ul-modal-title">Saisie de cotisation</h3>
<p id="due-modal-subtitle" class="ul-modal-subtitle"></p>
</div>
<button type="button" id="btn-close-due-modal" class="ul-modal-close" aria-label="Fermer">&times;</button>
</header>
<form id="due-form" class="ul-modal-body">
<input type="hidden" id="due-member-id" />
<input type="hidden" id="due-year" />
<input type="hidden" id="due-month-start" />
<div id="due-modal-prefill-hint" class="ul-prefill-banner"></div>
<div class="ul-form-row">
<div class="ul-form-group">
<label for="due-input-amount" class="ul-field-label">Montant versé (€) *</label>
<input
type="number"
id="due-input-amount"
class="ul-input ul-input-lg"
step="0.01"
min="0.01"
required
/>
</div>
<div class="ul-form-group">
<label for="due-input-method" class="ul-field-label">Mode de paiement</label>
<select id="due-input-method" class="ul-select ul-input-lg">
<option value="Virement">Virement (V)</option>
<option value="Prélèvement">Prélèvement (P)</option>
<option value="Chèque">Chèque (C)</option>
<option value="Espèces">Espèces (E)</option>
</select>
</div>
</div>
<div class="ul-form-group">
<label for="due-input-month-end" class="ul-field-label">Appliquer jusqu'au mois de :</label>
<select id="due-input-month-end" class="ul-select"></select>
<span class="ul-field-help">Permet de saisir plusieurs mois consécutifs en une seule validation (ex : paiement d'un trimestre ou semestre).</span>
</div>
<div class="ul-form-group">
<label for="due-input-note" class="ul-field-label">Note facultative</label>
<input
type="text"
id="due-input-note"
class="ul-input"
maxlength="255"
placeholder="Ex : Chèque n°4512, régularisation..."
/>
</div>
<footer class="ul-modal-footer">
<button type="button" id="btn-delete-due" class="ul-btn ul-btn-danger hidden">
🗑️ Effacer ce mois
</button>
<div class="ul-modal-footer-right">
<button type="button" id="btn-cancel-due" class="ul-btn ul-btn-outline">
Annuler
</button>
<button type="submit" id="btn-submit-due" class="ul-btn ul-btn-success">
✓ Valider (Entrée)
</button>
</div>
</footer>
</form>
</div>
</div>
<!-- MODALE 2 : CRÉATION / ÉDITION DE FICHE SYNDIQUÉ -->
<div id="member-modal" class="ul-modal-backdrop hidden ul-no-print" role="dialog" aria-modal="true" aria-labelledby="member-modal-title">
<div class="ul-modal-dialog ul-modal-lg">
<header class="ul-modal-header">
<h3 id="member-modal-title" class="ul-modal-title">Fiche du syndiqué</h3>
<button type="button" id="btn-close-member-modal" class="ul-modal-close" aria-label="Fermer">&times;</button>
</header>
<form id="member-form" class="ul-modal-body">
<input type="hidden" id="mbr-id" />
<div class="ul-form-row ul-form-row-3">
<div class="ul-form-group">
<label for="mbr-civility" class="ul-field-label">Civilité</label>
<select id="mbr-civility" class="ul-select">
<option value="M.">M.</option>
<option value="Mme">Mme</option>
<option value="">Non précisé</option>
</select>
</div>
<div class="ul-form-group">
<label for="mbr-last-name" class="ul-field-label">Nom (MAJUSCULES) *</label>
<input type="text" id="mbr-last-name" class="ul-input" required placeholder="Ex : MORGET" />
</div>
<div class="ul-form-group">
<label for="mbr-first-name" class="ul-field-label">Prénom *</label>
<input type="text" id="mbr-first-name" class="ul-input" required placeholder="Ex : FLORIAN" />
</div>
</div>
<div class="ul-form-row ul-form-row-3">
<div class="ul-form-group">
<label for="mbr-cogitiel-id" class="ul-field-label">Code Responsable (CoGétise)</label>
<input type="text" id="mbr-cogitiel-id" class="ul-input" placeholder="Ex : 1962796" />
</div>
<div class="ul-form-group">
<label for="mbr-default-amount" class="ul-field-label">Montant mensuel de réf. (€)</label>
<input type="number" id="mbr-default-amount" class="ul-input" step="0.01" min="0" value="0.00" />
</div>
<div class="ul-form-group">
<label for="mbr-default-method" class="ul-field-label">Mode de paiement habituel</label>
<select id="mbr-default-method" class="ul-select">
<option value="Virement">Virement</option>
<option value="Prélèvement">Prélèvement</option>
<option value="Chèque">Chèque</option>
<option value="Espèces">Espèces</option>
</select>
</div>
</div>
<div class="ul-form-row ul-form-row-3">
<div class="ul-form-group">
<label for="mbr-email" class="ul-field-label">Adresse e-mail</label>
<input type="email" id="mbr-email" class="ul-input" placeholder="exemple@domaine.fr" />
</div>
<div class="ul-form-group">
<label for="mbr-phone-personal" class="ul-field-label">Téléphone personnel</label>
<input type="text" id="mbr-phone-personal" class="ul-input" placeholder="06 00 00 00 00" />
</div>
<div class="ul-form-group">
<label for="mbr-phone-pro" class="ul-field-label">Téléphone professionnel</label>
<input type="text" id="mbr-phone-pro" class="ul-input" />
</div>
</div>
<div class="ul-form-row ul-form-row-3">
<div class="ul-form-group ul-span-2">
<label for="mbr-address" class="ul-field-label">Adresse postale complète</label>
<input type="text" id="mbr-address" class="ul-input" placeholder="Ex : 23 AVENUE DE TOULOUSE" />
</div>
<div class="ul-form-group">
<label for="mbr-postal-code" class="ul-field-label">Code postal</label>
<input type="text" id="mbr-postal-code" class="ul-input" placeholder="Ex : 09100" />
</div>
</div>
<div class="ul-form-row ul-form-row-3">
<div class="ul-form-group">
<label for="mbr-city" class="ul-field-label">Ville</label>
<input type="text" id="mbr-city" class="ul-input" placeholder="Ex : PAMIERS" />
</div>
<div class="ul-form-group">
<label for="mbr-base-code" class="ul-field-label">Code de la base</label>
<input type="text" id="mbr-base-code" class="ul-input" value="90045110520251341" />
</div>
<div class="ul-form-group">
<label for="mbr-base-name" class="ul-field-label">Nom de la base</label>
<input type="text" id="mbr-base-name" class="ul-input" value="INDIVIDUELS UL FOIX HAUTE-ARIEGE" />
</div>
</div>
<div class="ul-form-row ul-checkbox-group-row">
<label class="ul-checkbox-label" for="mbr-is-archived">
<input type="checkbox" id="mbr-is-archived" />
<span>📦 Syndiqué archivé (masqué par défaut dans la grille)</span>
</label>
</div>
<footer class="ul-modal-footer">
<button type="button" id="btn-delete-member" class="ul-btn ul-btn-danger hidden">
🗑️ Supprimer ce syndiqué
</button>
<div class="ul-modal-footer-right">
<button type="button" id="btn-cancel-member" class="ul-btn ul-btn-outline">
Annuler
</button>
<button type="submit" id="btn-submit-member" class="ul-btn ul-btn-primary">
💾 Enregistrer la fiche
</button>
</div>
</footer>
</form>
</div>
</div>
<!-- ZONE D'IMPRESSION A4 MULTI-PAGES (visible uniquement lors de window.print()) -->
<div id="ul-print-container" class="ul-print-only" aria-hidden="true"></div>
<?php endif; ?>
</div>