From a86212de4f86668767ab9a8f2b2ce2c3ab090a1b Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 21 Sep 2024 22:15:59 +0200
Subject: [PATCH 001/121] added passUntilValid

---
 src/Modules/Region/components/MemberList.vue | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 02a2564ad4..b38e497617 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -183,6 +183,13 @@
             year: 'numeric',
           }) }}
         </template>
+        <template #cell(passUntilValid)="row">
+          {{ row.item.lastPassDate === null ? '' : $dateFormatter.format(passUntilValid(row.item.lastPassDate), {
+            day: 'numeric',
+            month: 'numeric',
+            year: 'numeric',
+          }) }}
+        </template>
         <template #cell(lastActivity)="row">
           {{ $dateFormatter.format(row.item.lastActivity, {
             day: 'numeric',
@@ -436,6 +443,12 @@ export default {
           label: this.$i18n('group.member_list.passports.created_at'),
           sortable: true,
           class: 'align-middle',
+        },
+        {
+          key: 'passUntilValid',
+          label: 'Gültig bis',
+          sortable: true,
+          class: 'align-middle',
         })
       }
 
@@ -508,6 +521,11 @@ export default {
     regionStore.fetchMemberList(this.groupId)
   },
   methods: {
+    passUntilValid (creationDate) {
+      const validUntil = new Date(creationDate)
+      validUntil.setFullYear(validUntil.getFullYear() + 3)
+      return validUntil
+    },
     isNullOrEmptyOrWhitespace (str) {
       return (str ?? '').trim().length === 0
     },
-- 
GitLab


From 0e7b8d87495a207bd34c0cb55fb86f4d473a2f5a Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 16:18:01 +0200
Subject: [PATCH 002/121] refactor pdf generation - added booleans createPdf
 and renew

---
 client/src/api/stores.js                      |   1 +
 client/src/api/verification.js                |   5 +-
 .../PassportGeneratorGateway.php              |  24 +-
 .../PassportGeneratorTransaction.php          | 367 ++++++++++--------
 .../Passport/CreateRegionPassportModel.php    |  34 ++
 .../Passport/CreateUserPassportModel.php      |  42 ++
 src/RestApi/VerificationRestController.php    |   5 +-
 7 files changed, 305 insertions(+), 173 deletions(-)
 create mode 100644 src/RestApi/Models/Passport/CreateUserPassportModel.php

diff --git a/client/src/api/stores.js b/client/src/api/stores.js
index a2fbf868c9..e705ba2828 100644
--- a/client/src/api/stores.js
+++ b/client/src/api/stores.js
@@ -16,6 +16,7 @@ export async function getStoreInformation (storeId) {
 
 export async function updateStore (store) {
   const result = await patch(`/stores/${store.id}/information`, store)
+  console.log('store', store)
   return result
 }
 
diff --git a/client/src/api/verification.js b/client/src/api/verification.js
index 4b3b34d23c..8c2f09d6fd 100644
--- a/client/src/api/verification.js
+++ b/client/src/api/verification.js
@@ -20,6 +20,7 @@ export async function createPassportAsUser () {
   return await post('/user/current/passport', {}, { responseType: 'blob' })
 }
 
-export async function createPassportAsAmbassador (regionId, userIds) {
-  return await post(`/region/${regionId}/passport`, { userIds: userIds }, { responseType: 'blob' })
+export async function createPassportAsAmbassador (regionId, userIds, createPdf = true, renew = true) {
+  const options = createPdf ? { responseType: 'blob' } : {}
+  return await post(`/region/${regionId}/passport`, { userIds, createPdf, renew: renew }, options)
 }
diff --git a/src/Modules/PassportGenerator/PassportGeneratorGateway.php b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
index 26c69ca87c..aae34f872c 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorGateway.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
@@ -13,21 +13,29 @@ final class PassportGeneratorGateway extends BaseGateway
         parent::__construct($db);
     }
 
-    public function passGen(int $bot_id, int $fsid): int
+    public function logPassGeneration(int $generatedUserId, array $userIds): int
     {
-        return $this->db->insert('fs_pass_gen', [
-            'foodsaver_id' => $fsid,
-            'date' => $this->db->now(),
-            'bot_id' => $bot_id,
-        ]);
+        $rowsInserted = 0;
+        $now = $this->db->now();
+
+        foreach ($userIds as $userId) {
+            $this->db->insert('fs_pass_gen', [
+                'foodsaver_id' => $userId,
+                'date' => $now,
+                'bot_id' => $generatedUserId,
+            ]);
+            ++$rowsInserted;
+        }
+
+        return $rowsInserted;
     }
 
-    public function updateLastGen(array $foodsaver): int
+    public function updateFoodsaverLastPassDate(array $foodsaver): int
     {
         return $this->db->update('fs_foodsaver', ['last_pass' => $this->db->now()], ['id' => $foodsaver]);
     }
 
-    public function getLastGen(int $fsId): ?\DateTime
+    public function getFoodsaverLastPassDate(int $fsId): ?\DateTime
     {
         $lastPass = $this->db->fetchValueByCriteria('fs_foodsaver', 'last_pass', ['id' => $fsId]);
 
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index c465e398dd..6bf60d23a0 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -2,19 +2,20 @@
 
 namespace Foodsharing\Modules\PassportGenerator;
 
+use DateTime;
 use Foodsharing\Lib\Session;
-use Foodsharing\Modules\Bell\BellGateway;
-use Foodsharing\Modules\Bell\DTO\Bell;
-use Foodsharing\Modules\Core\DBConstants\Bell\BellType;
 use Foodsharing\Modules\Core\DBConstants\Foodsaver\Gender;
 use Foodsharing\Modules\Core\DBConstants\Foodsaver\Role;
 use Foodsharing\Modules\Foodsaver\FoodsaverGateway;
 use Foodsharing\Modules\Profile\ProfileGateway;
 use Foodsharing\Modules\Region\RegionGateway;
 use Foodsharing\Modules\Uploads\UploadsTransactions;
+use Foodsharing\RestApi\Models\Passport\CreateRegionPassportModel;
+use Foodsharing\RestApi\Models\Passport\CreateUserPassportModel;
 use Foodsharing\Utility\FlashMessageHelper;
 use Foodsharing\Utility\TranslationHelper;
 use setasign\Fpdi\Tcpdf\Fpdi;
+use stdClass;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use Symfony\Contracts\Translation\TranslatorInterface;
@@ -28,7 +29,6 @@ class PassportGeneratorTransaction extends AbstractController
         private readonly ProfileGateway $profileGateway,
         private readonly Session $session,
         private readonly UploadsTransactions $uploadsTransactions,
-        private readonly BellGateway $bellGateway,
         protected FlashMessageHelper $flashMessageHelper,
         protected TranslationHelper $translationHelper,
         protected TranslatorInterface $translator,
@@ -37,98 +37,145 @@ class PassportGeneratorTransaction extends AbstractController
     ) {
     }
 
-    public function generate(array $foodsavers, ?\DateTime $passDate = null, bool $cutMarkers = true, bool $protectPDF = false, bool $ambassadorGeneration = false): string
+    private function setupPdfMargins(\TCPDF $pdf, array $userIds): array
     {
-        $tmp = [];
-        foreach ($foodsavers as $foodsaver) {
-            $tmp[$foodsaver] = (int)$foodsaver;
+        $singleUser = (count($userIds) === 1);
+
+        if ($singleUser) {
+            $pdf->AddPage('L', [53.3, 83]);
+        } else {
+            $pdf->AddPage();
         }
-        $foodsavers = $tmp;
-        $is_generated = [];
 
-        $pdf = new Fpdi();
+        $pdf->SetAutoPageBreak(false, 0);
+        $pdf->SetMargins(0, 0, 0, true);
+
+        $backgroundMarginX = $singleUser ? 0 : 10;
+        $backgroundMarginY = $singleUser ? 0 : 10;
+        $cellMarginX = 40; // Gleich in beiden Fällen
+        $cellMarginY = $singleUser ? 3.2 : 13.2;
+        $idLabelMarginX = $singleUser ? 40 : 50;
+        $idLabelMarginY = 5; // Gleich in beiden Fällen
+        $logoMarginX = $singleUser ? 3.5 : 13.5;
+        $logoMarginY = $singleUser ? 3.6 : 13.6;
+        $photoMarginX = $singleUser ? 4 : 14;
+        $photoMarginY = $singleUser ? 19.7 : 31;
+        $nameMaxWidthMarginX = $singleUser ? 31 : 41;
+        $nameMaxWidthMarginY = $singleUser ? 20 : 30;
+        $nameLabelMarginX = $singleUser ? 31 : 41;
+        $nameLabelMarginY = $singleUser ? 20 : 28;
+        $nameMarginX = $singleUser ? 31 : 41;
+        $nameMarginY = $singleUser ? 22 : 30.2;
+        $roleLabelMarginX = $singleUser ? 31 : 41;
+        $roleLabelMarginY = $singleUser ? 27 : 37;
+        $roleMarginX = $singleUser ? 31 : 41;
+        $roleMarginY = $singleUser ? 29 : 39;
+        $validTillLabelMarginX = $singleUser ? 31 : 41;
+        $validTillLabelMarginY = $singleUser ? 45 : 55;
+        $validTillMarginX = $singleUser ? 31 : 41;
+        $validTillMarginY = $singleUser ? 47 : 57;
+        $validDownLabelMarginX = $singleUser ? 31 : 41;
+        $validDownLabelMarginY = $singleUser ? 36 : 46;
+        $validDownMarginX = $singleUser ? 31 : 41;
+        $validDownMarginY = $singleUser ? 38 : 48;
+        $qrCodeMarginX = $singleUser ? 60 : 70.5;
+        $qrCodeMarginY = $singleUser ? 33 : 43;
+
+        return [
+            'backgroundMarginX' => $backgroundMarginX,
+            'backgroundMarginY' => $backgroundMarginY,
+            'cellMarginX' => $cellMarginX,
+            'cellMarginY' => $cellMarginY,
+            'idLabelMarginX' => $idLabelMarginX,
+            'idLabelMarginY' => $idLabelMarginY,
+            'logoMarginX' => $logoMarginX,
+            'logoMarginY' => $logoMarginY,
+            'photoMarginX' => $photoMarginX,
+            'photoMarginY' => $photoMarginY,
+            'nameMaxWidthMarginX' => $nameMaxWidthMarginX,
+            'nameMaxWidthMarginY' => $nameMaxWidthMarginY,
+            'nameLabelMarginX' => $nameLabelMarginX,
+            'nameLabelMarginY' => $nameLabelMarginY,
+            'nameMarginX' => $nameMarginX,
+            'nameMarginY' => $nameMarginY,
+            'roleLabelMarginX' => $roleLabelMarginX,
+            'roleLabelMarginY' => $roleLabelMarginY,
+            'roleMarginX' => $roleMarginX,
+            'roleMarginY' => $roleMarginY,
+            'validTillLabelMarginX' => $validTillLabelMarginX,
+            'validTillLabelMarginY' => $validTillLabelMarginY,
+            'validTillMarginX' => $validTillMarginX,
+            'validTillMarginY' => $validTillMarginY,
+            'validDownLabelMarginX' => $validDownLabelMarginX,
+            'validDownLabelMarginY' => $validDownLabelMarginY,
+            'validDownMarginX' => $validDownMarginX,
+            'validDownMarginY' => $validDownMarginY,
+            'qrCodeMarginX' => $qrCodeMarginX,
+            'qrCodeMarginY' => $qrCodeMarginY,
+        ];
+    }
 
-        if ($protectPDF) {
-            $pdf->SetProtection(['print', 'copy', 'modify', 'assemble'], '', null, 0, null);
+    private function addUserPhotoToPdf(\TCPDF $pdf, int $userId, float $x, float $y, array $margins): void
+    {
+        $photo = $this->foodsaverGateway->getPhotoFileName($userId);
+        if (!$photo) {
+            // Wenn kein Foto vorhanden ist, nichts tun.
+            return;
         }
 
-        $generationUntilDate = '+3 years';
-        if ($ambassadorGeneration) {
-            $untilFrom = (new \DateTime())->format('d. m. Y');
-            $validUntil = (new \DateTime())->modify($generationUntilDate)->format('d. m. Y');
+        $imagePath = null;
+        $imageWidth = null;
+
+        if (str_starts_with($photo, '/api/uploads')) {
+            // Verarbeitet Fotos, die über die API hochgeladen wurden.
+            $uuid = substr($photo, strlen('/api/uploads/'));
+            $filename = $this->uploadsTransactions->generateFilePath($uuid, 200, 257, 0);
+
+            if (!file_exists($filename)) {
+                $originalFilename = $this->uploadsTransactions->generateFilePath($uuid);
+                $this->uploadsTransactions->resizeImage($originalFilename, $filename, 200, 257, 0);
+            }
+
+            $imagePath = $filename;
+            $imageWidth = 24;
         } else {
-            $untilFrom = $passDate->format('d. m. Y');
-            $validUntil = $passDate->modify($generationUntilDate)->format('d. m. Y');
+            // Verarbeitet Fotos aus dem lokalen Images-Verzeichnis.
+            $croppedImagePath = 'images/crop_' . $photo;
+            $originalImagePath = 'images/' . $photo;
+
+            if (file_exists($croppedImagePath)) {
+                $imagePath = $croppedImagePath;
+                $imageWidth = 24;
+            } elseif (file_exists($originalImagePath)) {
+                $imagePath = $originalImagePath;
+                $imageWidth = 22;
+            }
         }
 
-        if (count($tmp) === 1) {
-            $pdf->AddPage('L', [53.3, 83]);
-            $pdf->SetAutoPageBreak(false, 0);
-            $pdf->SetMargins(0, 0, 0, true);
-            $backgroundMarginX = 0;
-            $backgroundMarginY = 0;
-            $cellMarginX = 40;
-            $cellMarginY = 3.2;
-            $idLabelMarginX = 40;
-            $idLabelMarginY = 5;
-            $logoMarginX = 3.5;
-            $logoMarginY = 3.6;
-            $photoMarginX = 4;
-            $photoMarginY = 19.7;
-            $nameMaxWidthMarginX = 31;
-            $nameMaxWidthMarginY = 20;
-            $nameLabelMarginX = 31;
-            $nameLabelMarginY = 20;
-            $nameMarginX = 31;
-            $nameMarginY = 22;
-            $roleLabelMarginX = 31;
-            $roleLabelMarginY = 27;
-            $roleMarginX = 31;
-            $roleMarginY = 29;
-            $validTillLabelMarginX = 31;
-            $validTillLabelMarginY = 45;
-            $validDownLabelMarginX = 31;
-            $validDownLabelMarginY = 36;
-            $validDownMarginX = 31;
-            $validDownMarginY = 38;
-            $validTillMarginX = 31;
-            $validTillMarginY = 47;
-            $qrCodeMarginX = 60;
-            $qrCodeMarginY = 33;
-        } else {
-            $pdf->AddPage();
-            $backgroundMarginX = 10;
-            $backgroundMarginY = 10;
-            $cellMarginX = 40;
-            $cellMarginY = 13.2;
-            $idLabelMarginX = 50;
-            $idLabelMarginY = 5;
-            $logoMarginX = 13.5;
-            $logoMarginY = 13.6;
-            $photoMarginX = 14;
-            $photoMarginY = 31;
-            $nameMaxWidthMarginX = 41;
-            $nameMaxWidthMarginY = 30;
-            $nameLabelMarginX = 41;
-            $nameLabelMarginY = 28;
-            $roleLabelMarginX = 41;
-            $roleLabelMarginY = 37;
-            $roleMarginX = 41;
-            $roleMarginY = 39;
-            $nameMarginX = 41;
-            $nameMarginY = 30.2;
-            $validTillLabelMarginX = 41;
-            $validTillLabelMarginY = 55;
-            $validTillMarginX = 41;
-            $validTillMarginY = 57;
-            $validDownLabelMarginX = 41;
-            $validDownLabelMarginY = 46;
-            $validDownMarginX = 41;
-            $validDownMarginY = 48;
-            $qrCodeMarginX = 70.5;
-            $qrCodeMarginY = 43;
+        if ($imagePath) {
+            // Fügt das Bild dem PDF hinzu.
+            $pdf->Image(
+                $imagePath,
+                $margins['photoMarginX'] + $x,
+                $margins['photoMarginY'] + $y,
+                $imageWidth
+            );
+        }
+    }
+
+    private function generatePdf(array $userIds, bool $ambassadorGeneration, $validDates): stdClass
+    {
+        $protectPDF = !$ambassadorGeneration;
+        $cutMarkers = $ambassadorGeneration;
+
+        $pdf = new Fpdi();
+
+        if ($protectPDF) {
+            $pdf->SetProtection(['print', 'copy', 'modify', 'assemble'], '', null, 0, null);
         }
 
+        $margins = $this->setupPdfMargins($pdf, $userIds);
+
         $pdf->SetTextColor(0, 0, 0);
         $pdf->AddFont('Ubuntu-L', '', $this->projectDir . '/lib/font/ubuntul.php', true);
         $pdf->AddFont('AcmeFont Regular', '', $this->projectDir . '/lib/font/acmefont.php', true);
@@ -137,83 +184,63 @@ class PassportGeneratorTransaction extends AbstractController
         $y = 0.0;
         $card = 0;
 
-        $noPhoto = [];
-
-        end($foodsavers);
+        end($userIds);
 
         $pdf->setSourceFile($this->projectDir . '/img/foodsharing_logo.pdf');
         $fs_logo = $pdf->importPage(1);
+        $pdfGeneratedUser = [];
 
-        foreach ($foodsavers as $fs_id) {
-            if ($foodsaver = $this->foodsaverGateway->getFoodsaverDetails($fs_id)) {
-                if (empty($foodsaver['photo'])) {
-                    $noPhoto[] = $foodsaver['name'] . ' ' . $foodsaver['nachname'];
-
-                    $bellData = Bell::create(
-                        'passgen_failed_title',
-                        'passgen_failed',
-                        'fas fa-camera',
-                        ['href' => '/user/current/settings'],
-                        ['user' => $this->session->user('name')],
-                        BellType::createIdentifier(BellType::PASS_CREATION_FAILED, $foodsaver['id'])
-                    );
-                    $this->bellGateway->addBell($foodsaver['id'], $bellData);
-                    //continue;
-                }
-
+        foreach ($userIds as $userId) {
+            if ($user = $this->foodsaverGateway->getFoodsaverDetails($userId)) {
                 $pdf->SetTextColor(0, 0, 0);
 
                 ++$card;
 
-                $this->passportGeneratorGateway->passGen($this->session->id(), $foodsaver['id']);
+                $backgroundFile = $this->projectDir . '/img/pass_bg' . ($cutMarkers ? '' : '_cut') . '.png';
 
-                if ($cutMarkers) {
-                    $backgroundFile = $this->projectDir . '/img/pass_bg.png';
-                } else {
-                    $backgroundFile = $this->projectDir . '/img/pass_bg_cut.png';
-                }
-                $pdf->Image($backgroundFile, $backgroundMarginX + $x, $backgroundMarginY + $y, 83, 55);
+                $pdf->Image($backgroundFile, $margins['backgroundMarginX'] + $x, $margins['backgroundMarginY'] + $y, 83, 55);
 
-                $name = $foodsaver['name'] . ' ' . $foodsaver['nachname'];
+                $name = $user['name'] . ' ' . $user['nachname'];
                 $fontSize = 10;
                 $maxWidth = 49;
                 $pdf->SetFont('Ubuntu-L', '', $fontSize);
                 $maxFontSize = min($maxWidth / $pdf->GetStringWidth($name) * $fontSize, $fontSize);
                 if ($maxFontSize >= 8) {
                     $pdf->SetFont('Ubuntu-L', '', $maxFontSize);
-                    $pdf->Text($nameMarginX + $x, $nameMarginY + $y - 0.2, $name);
+                    $pdf->Text($margins['nameMarginX'] + $x, $margins['nameMarginY'] + $y - 0.2, $name);
                 } else {
                     // Require line break after first name
                     $fontSize = min(
-                        $maxWidth / $pdf->GetStringWidth($foodsaver['name']) * $fontSize,
-                        $maxWidth / $pdf->GetStringWidth($foodsaver['nachname']) * $fontSize,
+                        $maxWidth / $pdf->GetStringWidth($user['name']) * $fontSize,
+                        $maxWidth / $pdf->GetStringWidth($user['nachname']) * $fontSize,
                         8
                     );
                     $pdf->SetFont('Ubuntu-L', '', $fontSize);
-                    $lineHeight = $pdf->getStringHeight(0, $foodsaver['name']) * 0.7;
-                    $pdf->Text($nameMarginX + $x, $nameMarginY + $y - 0.2, $foodsaver['name']);
-                    $pdf->Text($nameMarginX + $x, $nameMarginY + $y + $lineHeight - 0.2, $foodsaver['nachname']);
+                    $lineHeight = $pdf->getStringHeight(0, $user['name']) * 0.7;
+                    $pdf->Text($margins['nameMarginX'] + $x, $margins['nameMarginY'] + $y - 0.2, $user['name']);
+                    $pdf->Text($margins['nameMarginX'] + $x, $margins['nameMarginY'] + $y + $lineHeight - 0.2, $user['nachname']);
                 }
 
                 $pdf->SetFont('Ubuntu-L', '', 10);
-                $pdf->Text($roleMarginX + $x, $roleMarginY + $y, $this->getRole($foodsaver['geschlecht'], $foodsaver['rolle']));
-                $pdf->Text($validDownMarginX + $x, $validDownMarginY + $y, $untilFrom);
-                $pdf->Text($validTillMarginX + $x, $validTillMarginY + $y, $validUntil);
+                $pdf->Text($margins['roleMarginX'] + $x, $margins['roleMarginY'] + $y, $this->getRole($user['geschlecht'], $user['rolle']));
+                $pdf->Text($margins['validDownMarginX'] + $x, $margins['validDownMarginY'] + $y, $validDates->untilFrom);
+                $pdf->Text($margins['validTillMarginX'] + $x, $margins['validTillMarginY'] + $y, $validDates->validUntil);
+
                 $pdf->SetFont('Ubuntu-L', '', 6);
-                $pdf->Text($nameLabelMarginX + $x, $nameLabelMarginY + $y, 'Name');
-                $pdf->Text($roleLabelMarginX + $x, $roleLabelMarginY + $y, 'Rolle');
-                $pdf->Text($validDownLabelMarginX + $x, $validDownLabelMarginY + $y, 'Gültig ab');
-                $pdf->Text($validTillLabelMarginX + $x, $validTillLabelMarginY + $y, 'Gültig bis');
+                $pdf->Text($margins['nameLabelMarginX'] + $x, $margins['nameLabelMarginY'] + $y, 'Name');
+                $pdf->Text($margins['roleLabelMarginX'] + $x, $margins['roleLabelMarginY'] + $y, 'Rolle');
+                $pdf->Text($margins['validDownLabelMarginX'] + $x, $margins['validDownLabelMarginY'] + $y, 'Gültig ab');
+                $pdf->Text($margins['validTillLabelMarginX'] + $x, $margins['validTillLabelMarginY'] + $y, 'Gültig bis');
 
                 $pdf->SetFont('Ubuntu-L', '', 9);
                 $pdf->SetTextColor(255, 255, 255);
-                $pdf->SetXY($cellMarginX + $x, $cellMarginY + $y);
-                $pdf->Cell($idLabelMarginX, $idLabelMarginY, 'ID ' . $fs_id, 0, 0, 'R');
+                $pdf->SetXY($margins['cellMarginX'] + $x, $margins['cellMarginY'] + $y);
+                $pdf->Cell($margins['idLabelMarginX'], $margins['idLabelMarginY'], 'ID ' . $userId, 0, 0, 'R');
 
                 $pdf->SetFont('AcmeFont Regular', '', 5.3);
                 $pdf->Text(12.8 + $x, 18.6 + $y, $this->translator->trans('pass.claim'));
 
-                $pdf->useTemplate($fs_logo, $logoMarginX + $x, $logoMarginY + $y, 29.8);
+                $pdf->useTemplate($fs_logo, $margins['logoMarginX'] + $x, $margins['logoMarginY'] + $y, 29.8);
 
                 $style = [
                     'vpadding' => 'auto',
@@ -226,26 +253,10 @@ class PassportGeneratorTransaction extends AbstractController
 
                 // FIXME Do we really always want fs.de here?!
                 // QRCODE,L : QR-CODE Low error correction
-                $pdf->write2DBarcode('https://foodsharing.de/profile/' . $fs_id, 'QRCODE,L', $qrCodeMarginX + $x, $qrCodeMarginY + $y, 20, 20, $style, 'N', true);
-
-                if ($photo = $this->foodsaverGateway->getPhotoFileName($fs_id)) {
-                    if (str_starts_with($photo, '/api/uploads')) {
-                        // get the UUID and create a resized file
-                        $uuid = substr($photo, strlen('/api/uploads/'));
-                        $filename = $this->uploadsTransactions->generateFilePath($uuid, 200, 257, 0);
-                        if (!file_exists($filename)) {
-                            $originalFilename = $this->uploadsTransactions->generateFilePath($uuid);
-                            $this->uploadsTransactions->resizeImage($originalFilename, $filename, 200, 257, 0);
-                        }
-                        $pdf->Image($filename, $photoMarginX + $x, $photoMarginY + $y, 24);
-                    } else {
-                        if (file_exists('images/crop_' . $photo)) {
-                            $pdf->Image('images/crop_' . $photo, $photoMarginX + $x, $photoMarginY + $y, 24);
-                        } elseif (file_exists('images/' . $photo)) {
-                            $pdf->Image('images/' . $photo, $photoMarginX + $x, $photoMarginY + $y, 22);
-                        }
-                    }
-                }
+                $pdf->write2DBarcode('https://foodsharing.de/profile/' . $userId, 'QRCODE,L', $margins['qrCodeMarginX'] + $x, $margins['qrCodeMarginY'] + $y, 20, 20, $style, 'N', true);
+
+                // Aufruf der privaten Funktion zum Hinzufügen des Benutzerfotos zum PDF
+                $this->addUserPhotoToPdf($pdf, $userId, $x, $y, $margins);
 
                 if ($x == 0) {
                     $x += 95;
@@ -261,22 +272,56 @@ class PassportGeneratorTransaction extends AbstractController
                     $y = 0;
                 }
 
-                $is_generated[] = $foodsaver['id'];
+                $pdfGeneratedUser[] = $user['id'];
             }
         }
-        if (!empty($noPhoto)) {
-            $this->flashMessageHelper->info(
-                $this->translator->trans('pass.noPhoto')
-                . join(', ', $noPhoto)
-                . $this->translator->trans('pass.notGenerated')
-            );
+
+        $result = new stdClass();
+        $result->pdf = $pdf;
+        $result->pdfGeneratedUserIds = $pdfGeneratedUser;
+
+        return $result;
+    }
+
+    private function calculateValidDates(?DateTime $passDate, bool $isAmbassador): stdClass
+    {
+        $generationUntilDate = '+3 years';
+        $fromDate = $isAmbassador ? new DateTime() : clone $passDate;
+
+        $untilFrom = $fromDate->format('d. m. Y');
+        $validUntil = (clone $fromDate)->modify($generationUntilDate)->format('d. m. Y');
+
+        $result = new stdClass();
+        $result->untilFrom = $untilFrom;
+        $result->validUntil = $validUntil;
+
+        return $result;
+    }
+
+    public function generatePassportAsUser(CreateUserPassportModel $userPassportModel)
+    {
+        $this->generatePdf();
+    }
+
+    public function generatePassportAsAmbassador(CreateRegionPassportModel $regionPassportModel): mixed
+    {
+        $isAmbassador = true;
+        $result = new stdClass();
+        $validDates = $this->calculateValidDates($regionPassportModel->passDate, $isAmbassador);
+
+        if ($regionPassportModel->createPdf) {
+            $result = $this->generatePdf($regionPassportModel->userIds, $isAmbassador, $validDates);
         }
 
-        if ($ambassadorGeneration) {
-            $this->passportGeneratorGateway->updateLastGen($is_generated);
+        $generatedUserId = $this->session->id();
+
+        $userIds = $result->pdfGeneratedUserIds ? $regionPassportModel->userIds : [];
+        if ($regionPassportModel->renew) {
+            $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
+            $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
         }
 
-        return $pdf->Output('', 'S');
+        return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : $regionPassportModel->userIds;
     }
 
     public function getRole(int $gender_id, int $role_id): string
@@ -308,9 +353,9 @@ class PassportGeneratorTransaction extends AbstractController
         return $roles[$role_id];
     }
 
-    public function getPassDate(int $userId): \DateTime
+    public function getPassDate(int $userId): DateTime
     {
-        $date = $this->passportGeneratorGateway->getLastGen($userId);
+        $date = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
 
         if (empty($date)) {
             $verifyHistory = $this->profileGateway->getVerifyHistory($userId);
diff --git a/src/RestApi/Models/Passport/CreateRegionPassportModel.php b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
index 8615fd5031..d5bb14dd07 100644
--- a/src/RestApi/Models/Passport/CreateRegionPassportModel.php
+++ b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
@@ -2,6 +2,7 @@
 
 namespace Foodsharing\RestApi\Models\Passport;
 
+use DateTime;
 use JMS\Serializer\Annotation\Type;
 use OpenApi\Annotations as OA;
 use Symfony\Component\Validator\Constraints as Assert;
@@ -22,4 +23,37 @@ class CreateRegionPassportModel
     #[Assert\All(new Assert\Positive())]
     #[Type('array<int>')]
     public array $userIds = [];
+
+    /**
+     * @OA\Property(
+     *     type="boolean",
+     *     description="Flag to create PDF"
+     * )
+     * @Assert\NotNull()
+     * @Assert\Type("boolean")
+     */
+    public bool $createPdf;
+
+    /**
+     * @OA\Property(
+     *     type="boolean",
+     *     description="Flag to renew the passport"
+     * )
+     * @Assert\NotNull()
+     * @Assert\Type("boolean")
+     */
+    public bool $renew;
+
+    /**
+     * Datum des Passes im ISO 8601 Format.
+     *
+     * @OA\Property(
+     *     type="string",
+     *     format="date-time",
+     *     description="Datum des Passes im ISO 8601 Format",
+     *     nullable=true
+     * )
+     * @Assert\DateTime(format="Y-m-d\TH:i:sP")
+     */
+    public ?DateTime $passDate = null;
 }
diff --git a/src/RestApi/Models/Passport/CreateUserPassportModel.php b/src/RestApi/Models/Passport/CreateUserPassportModel.php
new file mode 100644
index 0000000000..dff685f8ec
--- /dev/null
+++ b/src/RestApi/Models/Passport/CreateUserPassportModel.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Foodsharing\RestApi\Models\Passport;
+
+use JMS\Serializer\Annotation\Type;
+use OpenApi\Annotations as OA;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Class that represents the data for creating a region passport.
+ *
+ * This class contains the user IDs for which a region passport should be generated.
+ * The data is provided in a format in which it is sent to the client.
+ */
+class CreateUserPassportModel
+{
+    /**
+     * Users for passport generation as array.
+     *
+     * @OA\Property(
+     *     type="array",
+     *     description="UserId for passport generation",
+     *     @OA\Items(type="integer")
+     * )
+     */
+    #[Assert\NotBlank]
+    #[Assert\All(new Assert\Positive())]
+    public int $userId;
+
+    /**
+     * Datum des Passes im ISO 8601 Format.
+     *
+     * @OA\Property(
+     *     type="string",
+     *     format="date-time",
+     *     description="Datum des Passes im ISO 8601 Format"
+     * )
+     * @Assert\NotBlank
+     * @Assert\DateTime(format="Y-m-d\TH:i:sP")
+     */
+    public string $passDate;
+}
diff --git a/src/RestApi/VerificationRestController.php b/src/RestApi/VerificationRestController.php
index d53a2f0b2a..a1c5e235af 100644
--- a/src/RestApi/VerificationRestController.php
+++ b/src/RestApi/VerificationRestController.php
@@ -242,7 +242,7 @@ class VerificationRestController extends AbstractFoodsharingRestController
 
         $passDate = $this->passportGeneratorTransaction->getPassDate($sessionId);
 
-        $pdf = $this->passportGeneratorTransaction->generate([$sessionId], $passDate, false, true);
+        $pdf = $this->passportGeneratorTransaction->generatePassport([$sessionId], $passDate, false);
 
         $response = new Response($pdf);
         $response->headers->set('Content-Type', 'application/pdf');
@@ -280,6 +280,7 @@ class VerificationRestController extends AbstractFoodsharingRestController
             throw new AccessDeniedHttpException();
         }
 
+
         $this->assertThereAreNoValidationErrors($validator, $regionPassportModel);
 
         $areUsersInRegion = $this->passportGeneratorTransaction->areUsersInRegion($regionPassportModel->userIds, $regionId);
@@ -288,7 +289,7 @@ class VerificationRestController extends AbstractFoodsharingRestController
         }
 
         try {
-            $pdf = $this->passportGeneratorTransaction->generate($regionPassportModel->userIds, null, true, false, true);
+            $pdf = $this->passportGeneratorTransaction->generatePassportAsAmbassador($regionPassportModel, null, true);
 
             $response = new Response($pdf);
             $response->headers->set('Content-Type', 'application/pdf');
-- 
GitLab


From 777c547343f346e77dc1da129f809718860b000f Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 17:13:50 +0200
Subject: [PATCH 003/121] added toggle buttons in memberlist

---
 .../PassportGeneratorTransaction.php          |  4 ++--
 src/Modules/Region/components/MemberList.vue  | 19 ++++++++++++++++++-
 src/RestApi/VerificationRestController.php    |  8 +++++---
 3 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 6bf60d23a0..e2250b1b9e 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -315,13 +315,13 @@ class PassportGeneratorTransaction extends AbstractController
 
         $generatedUserId = $this->session->id();
 
-        $userIds = $result->pdfGeneratedUserIds ? $regionPassportModel->userIds : [];
+        $userIds = $result->pdfGeneratedUserIds ?? $regionPassportModel->userIds;
         if ($regionPassportModel->renew) {
             $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
             $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
         }
 
-        return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : $regionPassportModel->userIds;
+        return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : implode(',', $regionPassportModel->userIds);
     }
 
     public function getRole(int $gender_id, int $role_id): string
diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index b38e497617..4b8eec47de 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -88,6 +88,12 @@
             >
               {{ $i18n('group.member_list.passports.filter_selection') }}
             </b-form-checkbox>
+            <b-form-checkbox-group
+              v-model="selectedPassportGenerationOption"
+              :options="passportGenerationOptions"
+              switches
+              size="sm"
+            />
           </div>
         </div>
       </b-tab>
@@ -324,9 +330,20 @@ export default {
       filterPassportMember: false,
       activeTab: null,
       sortBy: '',
+      passportGenerationOptions: [
+        { text: 'PDF erstellen', value: 'createPdf' },
+        { text: 'Ausweis erneuern', value: 'renew' },
+      ],
+      selectedPassportGenerationOption: ['createPdf', 'renew'],
     }
   },
   computed: {
+    createPdf () {
+      return this.selectedPassportGenerationOption.includes('createPdf')
+    },
+    renewPassport () {
+      return this.selectedPassportGenerationOption.includes('renew')
+    },
     getAdminButton () {
       return (item) => {
         if (this.mayRemoveAdminOrAmbassador && this.rowItemIsAdminOrAmbassadorOfRegion(item)) {
@@ -680,7 +697,7 @@ export default {
     async createPassports () {
       showLoader()
       try {
-        const blob = await createPassportAsAmbassador(this.regionId, this.passportMember)
+        const blob = await createPassportAsAmbassador(this.regionId, this.passportMember, this.createPdf, this.renewPassport)
         const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
         this.downloadFile(blob, filename)
       } catch (e) {
diff --git a/src/RestApi/VerificationRestController.php b/src/RestApi/VerificationRestController.php
index a1c5e235af..a0d922b7fa 100644
--- a/src/RestApi/VerificationRestController.php
+++ b/src/RestApi/VerificationRestController.php
@@ -289,10 +289,12 @@ class VerificationRestController extends AbstractFoodsharingRestController
         }
 
         try {
-            $pdf = $this->passportGeneratorTransaction->generatePassportAsAmbassador($regionPassportModel, null, true);
+            $result = $this->passportGeneratorTransaction->generatePassportAsAmbassador($regionPassportModel, null, true);
 
-            $response = new Response($pdf);
-            $response->headers->set('Content-Type', 'application/pdf');
+            $response = new Response($result);
+            if ($regionPassportModel->createPdf) {
+                $response->headers->set('Content-Type', 'application/pdf');
+            }
         } catch (\Exception $ex) {
             throw new BadRequestException($ex->getMessage());
         }
-- 
GitLab


From bb05eb24484b7f5d8c4bac340081147597a4b162 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 17:51:19 +0200
Subject: [PATCH 004/121] code style

---
 src/RestApi/VerificationRestController.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/RestApi/VerificationRestController.php b/src/RestApi/VerificationRestController.php
index a0d922b7fa..e64d78103a 100644
--- a/src/RestApi/VerificationRestController.php
+++ b/src/RestApi/VerificationRestController.php
@@ -280,7 +280,6 @@ class VerificationRestController extends AbstractFoodsharingRestController
             throw new AccessDeniedHttpException();
         }
 
-
         $this->assertThereAreNoValidationErrors($validator, $regionPassportModel);
 
         $areUsersInRegion = $this->passportGeneratorTransaction->areUsersInRegion($regionPassportModel->userIds, $regionId);
-- 
GitLab


From 4dbe90a03ec6f7ee37ee3470cf8cf389d578548c Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 18:33:24 +0200
Subject: [PATCH 005/121] added generatePassportAsUser

---
 .../PassportGeneratorTransaction.php          | 13 ++++--
 .../Passport/CreateRegionPassportModel.php    | 14 -------
 .../Passport/CreateUserPassportModel.php      | 42 -------------------
 src/RestApi/VerificationRestController.php    |  6 +--
 4 files changed, 11 insertions(+), 64 deletions(-)
 delete mode 100644 src/RestApi/Models/Passport/CreateUserPassportModel.php

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index e2250b1b9e..b7e9f09645 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -11,7 +11,6 @@ use Foodsharing\Modules\Profile\ProfileGateway;
 use Foodsharing\Modules\Region\RegionGateway;
 use Foodsharing\Modules\Uploads\UploadsTransactions;
 use Foodsharing\RestApi\Models\Passport\CreateRegionPassportModel;
-use Foodsharing\RestApi\Models\Passport\CreateUserPassportModel;
 use Foodsharing\Utility\FlashMessageHelper;
 use Foodsharing\Utility\TranslationHelper;
 use setasign\Fpdi\Tcpdf\Fpdi;
@@ -298,16 +297,22 @@ class PassportGeneratorTransaction extends AbstractController
         return $result;
     }
 
-    public function generatePassportAsUser(CreateUserPassportModel $userPassportModel)
+    public function generatePassportAsUser(int $userId): string
     {
-        $this->generatePdf();
+        $isAmbassador = false;
+        $passDate = $this->getPassDate($userId);
+        $validDates = $this->calculateValidDates($passDate, $isAmbassador);
+
+        $result = $this->generatePdf([$userId], $isAmbassador, $validDates);
+
+        return $result->pdf->Output('', 'S');
     }
 
     public function generatePassportAsAmbassador(CreateRegionPassportModel $regionPassportModel): mixed
     {
         $isAmbassador = true;
         $result = new stdClass();
-        $validDates = $this->calculateValidDates($regionPassportModel->passDate, $isAmbassador);
+        $validDates = $this->calculateValidDates(null, $isAmbassador);
 
         if ($regionPassportModel->createPdf) {
             $result = $this->generatePdf($regionPassportModel->userIds, $isAmbassador, $validDates);
diff --git a/src/RestApi/Models/Passport/CreateRegionPassportModel.php b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
index d5bb14dd07..61eae01236 100644
--- a/src/RestApi/Models/Passport/CreateRegionPassportModel.php
+++ b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
@@ -2,7 +2,6 @@
 
 namespace Foodsharing\RestApi\Models\Passport;
 
-use DateTime;
 use JMS\Serializer\Annotation\Type;
 use OpenApi\Annotations as OA;
 use Symfony\Component\Validator\Constraints as Assert;
@@ -43,17 +42,4 @@ class CreateRegionPassportModel
      * @Assert\Type("boolean")
      */
     public bool $renew;
-
-    /**
-     * Datum des Passes im ISO 8601 Format.
-     *
-     * @OA\Property(
-     *     type="string",
-     *     format="date-time",
-     *     description="Datum des Passes im ISO 8601 Format",
-     *     nullable=true
-     * )
-     * @Assert\DateTime(format="Y-m-d\TH:i:sP")
-     */
-    public ?DateTime $passDate = null;
 }
diff --git a/src/RestApi/Models/Passport/CreateUserPassportModel.php b/src/RestApi/Models/Passport/CreateUserPassportModel.php
deleted file mode 100644
index dff685f8ec..0000000000
--- a/src/RestApi/Models/Passport/CreateUserPassportModel.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-namespace Foodsharing\RestApi\Models\Passport;
-
-use JMS\Serializer\Annotation\Type;
-use OpenApi\Annotations as OA;
-use Symfony\Component\Validator\Constraints as Assert;
-
-/**
- * Class that represents the data for creating a region passport.
- *
- * This class contains the user IDs for which a region passport should be generated.
- * The data is provided in a format in which it is sent to the client.
- */
-class CreateUserPassportModel
-{
-    /**
-     * Users for passport generation as array.
-     *
-     * @OA\Property(
-     *     type="array",
-     *     description="UserId for passport generation",
-     *     @OA\Items(type="integer")
-     * )
-     */
-    #[Assert\NotBlank]
-    #[Assert\All(new Assert\Positive())]
-    public int $userId;
-
-    /**
-     * Datum des Passes im ISO 8601 Format.
-     *
-     * @OA\Property(
-     *     type="string",
-     *     format="date-time",
-     *     description="Datum des Passes im ISO 8601 Format"
-     * )
-     * @Assert\NotBlank
-     * @Assert\DateTime(format="Y-m-d\TH:i:sP")
-     */
-    public string $passDate;
-}
diff --git a/src/RestApi/VerificationRestController.php b/src/RestApi/VerificationRestController.php
index e64d78103a..3f48a5b0b0 100644
--- a/src/RestApi/VerificationRestController.php
+++ b/src/RestApi/VerificationRestController.php
@@ -240,9 +240,7 @@ class VerificationRestController extends AbstractFoodsharingRestController
             throw new AccessDeniedHttpException();
         }
 
-        $passDate = $this->passportGeneratorTransaction->getPassDate($sessionId);
-
-        $pdf = $this->passportGeneratorTransaction->generatePassport([$sessionId], $passDate, false);
+        $pdf = $this->passportGeneratorTransaction->generatePassportAsUser($sessionId);
 
         $response = new Response($pdf);
         $response->headers->set('Content-Type', 'application/pdf');
@@ -288,7 +286,7 @@ class VerificationRestController extends AbstractFoodsharingRestController
         }
 
         try {
-            $result = $this->passportGeneratorTransaction->generatePassportAsAmbassador($regionPassportModel, null, true);
+            $result = $this->passportGeneratorTransaction->generatePassportAsAmbassador($regionPassportModel);
 
             $response = new Response($result);
             if ($regionPassportModel->createPdf) {
-- 
GitLab


From be778aebbc7396fd3f6141718c5d9d4903795739 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 18:53:15 +0200
Subject: [PATCH 006/121] changed implode to json

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index b7e9f09645..a653a7848c 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -326,7 +326,7 @@ class PassportGeneratorTransaction extends AbstractController
             $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
         }
 
-        return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : implode(',', $regionPassportModel->userIds);
+        return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : json_encode(['userIds' => $regionPassportModel->userIds]);
     }
 
     public function getRole(int $gender_id, int $role_id): string
-- 
GitLab


From d075b6d59484e80459491d941676f68e6a827498 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 18:54:15 +0200
Subject: [PATCH 007/121] translation

---
 translations/messages.de.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index d1490c5214..ed626c7d0b 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -572,7 +572,7 @@ group:
       title: "Standard"
       lastname: "Nachname"
     passports:
-      title: "Ausweise"
+      title: "Ausweise & Verifizierung"
       created_at: "Ausweis erstellt am"
       generate_button: "Ausweis/e für markierte erstellen"
       clear_selection: "Alle markierte zurücksetzen"
-- 
GitLab


From 2f293d23534b08b84ae06ee7acd58b0dc5909059 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 18:56:20 +0200
Subject: [PATCH 008/121] removed role

---
 .../PassportGeneratorTransaction.php          | 31 -------------------
 1 file changed, 31 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index a653a7848c..28ea309bff 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -221,13 +221,11 @@ class PassportGeneratorTransaction extends AbstractController
                 }
 
                 $pdf->SetFont('Ubuntu-L', '', 10);
-                $pdf->Text($margins['roleMarginX'] + $x, $margins['roleMarginY'] + $y, $this->getRole($user['geschlecht'], $user['rolle']));
                 $pdf->Text($margins['validDownMarginX'] + $x, $margins['validDownMarginY'] + $y, $validDates->untilFrom);
                 $pdf->Text($margins['validTillMarginX'] + $x, $margins['validTillMarginY'] + $y, $validDates->validUntil);
 
                 $pdf->SetFont('Ubuntu-L', '', 6);
                 $pdf->Text($margins['nameLabelMarginX'] + $x, $margins['nameLabelMarginY'] + $y, 'Name');
-                $pdf->Text($margins['roleLabelMarginX'] + $x, $margins['roleLabelMarginY'] + $y, 'Rolle');
                 $pdf->Text($margins['validDownLabelMarginX'] + $x, $margins['validDownLabelMarginY'] + $y, 'Gültig ab');
                 $pdf->Text($margins['validTillLabelMarginX'] + $x, $margins['validTillLabelMarginY'] + $y, 'Gültig bis');
 
@@ -329,35 +327,6 @@ class PassportGeneratorTransaction extends AbstractController
         return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : json_encode(['userIds' => $regionPassportModel->userIds]);
     }
 
-    public function getRole(int $gender_id, int $role_id): string
-    {
-        $roles = match ($gender_id) {
-            Gender::MALE => [
-                Role::FOODSHARER->value => $this->translator->trans('terminology.foodsharer.m'),
-                Role::FOODSAVER->value => $this->translator->trans('terminology.foodsaver.m'),
-                Role::STORE_MANAGER->value => $this->translator->trans('terminology.storemanager.m'),
-                Role::AMBASSADOR->value => $this->translator->trans('terminology.ambassador.m'),
-                Role::ORGA->value => $this->translator->trans('terminology.ambassador.m'),
-            ],
-            Gender::FEMALE => [
-                Role::FOODSHARER->value => $this->translator->trans('terminology.foodsharer.f'),
-                Role::FOODSAVER->value => $this->translator->trans('terminology.foodsaver.f'),
-                Role::STORE_MANAGER->value => $this->translator->trans('terminology.storemanager.f'),
-                Role::AMBASSADOR->value => $this->translator->trans('terminology.ambassador.f'),
-                Role::ORGA->value => $this->translator->trans('terminology.ambassador.f'),
-            ],
-            default => [
-                Role::FOODSHARER->value => $this->translator->trans('terminology.foodsharer.d'),
-                Role::FOODSAVER->value => $this->translator->trans('terminology.foodsaver.d'),
-                Role::STORE_MANAGER->value => $this->translator->trans('terminology.storemanager.d'),
-                Role::AMBASSADOR->value => $this->translator->trans('terminology.ambassador.d'),
-                Role::ORGA->value => $this->translator->trans('terminology.ambassador.d'),
-            ],
-        };
-
-        return $roles[$role_id];
-    }
-
     public function getPassDate(int $userId): DateTime
     {
         $date = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
-- 
GitLab


From a10465ff3c6e119be16857c5924c6ad0013af33f Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 18:56:43 +0200
Subject: [PATCH 009/121] removed role

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 28ea309bff..d02006dfb5 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -4,8 +4,6 @@ namespace Foodsharing\Modules\PassportGenerator;
 
 use DateTime;
 use Foodsharing\Lib\Session;
-use Foodsharing\Modules\Core\DBConstants\Foodsaver\Gender;
-use Foodsharing\Modules\Core\DBConstants\Foodsaver\Role;
 use Foodsharing\Modules\Foodsaver\FoodsaverGateway;
 use Foodsharing\Modules\Profile\ProfileGateway;
 use Foodsharing\Modules\Region\RegionGateway;
-- 
GitLab


From a70de506dc775389ebf7eee5e4271df6c0d26f4c Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:12:34 +0200
Subject: [PATCH 010/121] fixes

---
 .../PassportGeneratorTransaction.php          | 25 ++++++++-----------
 1 file changed, 10 insertions(+), 15 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index d02006dfb5..44f69af926 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -38,21 +38,20 @@ class PassportGeneratorTransaction extends AbstractController
     {
         $singleUser = (count($userIds) === 1);
 
-        if ($singleUser) {
-            $pdf->AddPage('L', [53.3, 83]);
-        } else {
-            $pdf->AddPage();
-        }
+        $pdf->AddPage(
+            $singleUser ? 'L' : 'P',
+            $singleUser ? [53.3, 83] : 'A4'
+        );
 
         $pdf->SetAutoPageBreak(false, 0);
         $pdf->SetMargins(0, 0, 0, true);
 
         $backgroundMarginX = $singleUser ? 0 : 10;
         $backgroundMarginY = $singleUser ? 0 : 10;
-        $cellMarginX = 40; // Gleich in beiden Fällen
+        $cellMarginX = 40;
         $cellMarginY = $singleUser ? 3.2 : 13.2;
         $idLabelMarginX = $singleUser ? 40 : 50;
-        $idLabelMarginY = 5; // Gleich in beiden Fällen
+        $idLabelMarginY = 5;
         $logoMarginX = $singleUser ? 3.5 : 13.5;
         $logoMarginY = $singleUser ? 3.6 : 13.6;
         $photoMarginX = $singleUser ? 4 : 14;
@@ -116,7 +115,6 @@ class PassportGeneratorTransaction extends AbstractController
     {
         $photo = $this->foodsaverGateway->getPhotoFileName($userId);
         if (!$photo) {
-            // Wenn kein Foto vorhanden ist, nichts tun.
             return;
         }
 
@@ -124,7 +122,6 @@ class PassportGeneratorTransaction extends AbstractController
         $imageWidth = null;
 
         if (str_starts_with($photo, '/api/uploads')) {
-            // Verarbeitet Fotos, die über die API hochgeladen wurden.
             $uuid = substr($photo, strlen('/api/uploads/'));
             $filename = $this->uploadsTransactions->generateFilePath($uuid, 200, 257, 0);
 
@@ -136,7 +133,6 @@ class PassportGeneratorTransaction extends AbstractController
             $imagePath = $filename;
             $imageWidth = 24;
         } else {
-            // Verarbeitet Fotos aus dem lokalen Images-Verzeichnis.
             $croppedImagePath = 'images/crop_' . $photo;
             $originalImagePath = 'images/' . $photo;
 
@@ -150,7 +146,6 @@ class PassportGeneratorTransaction extends AbstractController
         }
 
         if ($imagePath) {
-            // Fügt das Bild dem PDF hinzu.
             $pdf->Image(
                 $imagePath,
                 $margins['photoMarginX'] + $x,
@@ -246,11 +241,11 @@ class PassportGeneratorTransaction extends AbstractController
                     'module_height' => 1 // height of a single module in points
                 ];
 
-                // FIXME Do we really always want fs.de here?!
-                // QRCODE,L : QR-CODE Low error correction
-                $pdf->write2DBarcode('https://foodsharing.de/profile/' . $userId, 'QRCODE,L', $margins['qrCodeMarginX'] + $x, $margins['qrCodeMarginY'] + $y, 20, 20, $style, 'N', true);
+                $baseUrl = getenv('FS_ENV') === 'dev'? 'https://foodsharing.de' : BASE_URL;
+                $profileUrl = $baseUrl . '/user/' . $userId . '/profile';
+
+                $pdf->write2DBarcode($profileUrl, 'QRCODE,H', $margins['qrCodeMarginX'] + $x, $margins['qrCodeMarginY'] + $y, 20, 20, $style, 'N', true);
 
-                // Aufruf der privaten Funktion zum Hinzufügen des Benutzerfotos zum PDF
                 $this->addUserPhotoToPdf($pdf, $userId, $x, $y, $margins);
 
                 if ($x == 0) {
-- 
GitLab


From 3460e603812b58172775cdf25b724ace33f25b6a Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:15:44 +0200
Subject: [PATCH 011/121] removed font

---
 lib/font/acmefont.ctg.z                         | Bin 509 -> 0 bytes
 lib/font/acmefont.php                           |  15 ---------------
 lib/font/acmefont.z                             | Bin 27789 -> 0 bytes
 lib/font/acmefontregular.ttf                    | Bin 66536 -> 0 bytes
 .../PassportGeneratorTransaction.php            |   4 ----
 5 files changed, 19 deletions(-)
 delete mode 100644 lib/font/acmefont.ctg.z
 delete mode 100644 lib/font/acmefont.php
 delete mode 100644 lib/font/acmefont.z
 delete mode 100644 lib/font/acmefontregular.ttf

diff --git a/lib/font/acmefont.ctg.z b/lib/font/acmefont.ctg.z
deleted file mode 100644
index 7738d189a5859f92d5547a0fa96a09b78e16343c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 509
zcmb=J^Y%jfXYnGL;~$qRb8#wa8>hCXOx=4+GPVDNmP+)^)ha3ND#}atUwXgmUG??Y
zbEZEEZRN`6|K4Nw+V1`N<=-y+lijdZ{$iu-l@o5I1?@b&{JACVrppdnFH)Ai9VmO(
zu=n+izSlqeZq)rcve0bbWUHOQR$DJy?d6<%@rYV!T6$^O^OC&hrFD~cFW}z2LNoTx
z6}_7ao4j|gIlX&PrtkYx>t4ND_wv@d*T3>!9Lsz4EX`zd<k>vG;@Q{#<lVgN{{F+i
z?N!^Jw_Q5jyfeIf*XOB@Rk;)Hy_swG{_?%I*YaK;&3k`Z>`P?)7vK1=rLTW)ZOwgg
zd*AE5?=tqsy=vc=pIrW}y8eq(d3pHvlKAgs_3OVobXRR?uiD`rb!V~dwS3!qljdb)
z`hR_Lp*Mfan&s8Yp5|s|&wq1t{*Bn#zdq)d?YX~df6VWd^UGguD{cPbJ-hs+%7J@Z
zi*|4`pn!kN!@eK<wq;xYwy)X03O4-uV-Ujf=W^Jal5cP8cXBb@5U;Vh!-}GV#+*Op
Le>m@^S)2p_l7{6a

diff --git a/lib/font/acmefont.php b/lib/font/acmefont.php
deleted file mode 100644
index 56133dd0ee..0000000000
--- a/lib/font/acmefont.php
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-// TCPDF FONT FILE DESCRIPTION
-$type='TrueTypeUnicode';
-$name='AcmeFont';
-$up=-127;
-$ut=19;
-$dw=810;
-$diff='';
-$originalsize=66536;
-$enc='';
-$file='acmefont.z';
-$ctg='acmefont.ctg.z';
-$desc=array('Flags'=>32,'FontBBox'=>'[-50 -221 950 1014]','ItalicAngle'=>0,'Ascent'=>1014,'Descent'=>-221,'Leading'=>0,'CapHeight'=>759,'XHeight'=>555,'StemV'=>1,'StemH'=>0,'AvgWidth'=>497,'MaxWidth'=>988,'MissingWidth'=>810);
-$cw=array(0=>810,32=>271,33=>324,36=>670,37=>814,38=>667,39=>247,40=>405,41=>407,42=>252,44=>241,45=>353,46=>236,47=>674,48=>647,49=>382,50=>650,51=>652,52=>655,53=>616,54=>638,55=>647,56=>692,57=>642,58=>241,59=>242,63=>633,65=>733,66=>690,67=>619,68=>688,69=>665,70=>617,71=>622,72=>690,73=>388,74=>405,75=>698,76=>641,77=>839,78=>718,79=>669,80=>673,81=>659,82=>681,83=>653,84=>723,85=>665,86=>695,87=>969,88=>687,89=>643,90=>611,97=>610,98=>610,99=>566,100=>604,101=>594,102=>356,103=>605,104=>629,105=>329,106=>337,107=>613,108=>326,109=>897,110=>610,111=>570,112=>590,113=>594,114=>415,115=>564,116=>373,117=>636,118=>617,119=>888,120=>585,121=>610,122=>570,160=>271,161=>324,162=>638,163=>762,165=>643,170=>502,181=>636,186=>471,191=>633,192=>733,193=>733,194=>733,195=>733,196=>733,197=>733,198=>988,199=>619,200=>665,201=>665,202=>665,203=>665,204=>388,205=>388,206=>388,207=>388,208=>688,209=>718,210=>669,211=>669,212=>669,213=>669,214=>669,215=>0,216=>669,217=>665,218=>665,219=>665,220=>665,221=>643,222=>590,223=>897,224=>610,225=>610,226=>610,227=>610,228=>610,229=>610,230=>898,231=>566,232=>594,233=>594,234=>594,235=>594,236=>329,237=>329,238=>329,239=>405,240=>570,241=>610,242=>570,243=>570,244=>570,245=>570,246=>570,248=>570,249=>636,250=>636,251=>636,252=>636,253=>610,254=>610,255=>610,321=>662,322=>501,338=>964,339=>877,352=>653,353=>564,376=>643,381=>611,382=>570,960=>415,8211=>378,8212=>682,8216=>248,8217=>247,8220=>498,8221=>510,8226=>310,8719=>681,8730=>566,9674=>619);
-// --- EOF ---
diff --git a/lib/font/acmefont.z b/lib/font/acmefont.z
deleted file mode 100644
index d52ee2f8f965c28e1d5ab5561396670997883fad..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 27789
zcmb=Jb966zg7nE!1DnmF+zA(@9fcxvJQ{czIEB_IaEh)ff9bbt(PRZ7p~@udNm<ux
z_C2yb`SjfL?&>)fg}ySk3%ehmFPl>{J#GH(wHAHZN$TmxRp-kd?@ONefA&q*4GfNQ
zs`EcN-+K7&&6Mx=_EdhpcV>2Ei=b24mClGSEC03!zif}+^&zn3-Y<<K@1*CSJior?
zkN0oEDq)*V%a5Jc*L?MMVf(|q%u3Vz&d;A&`P}~ASE-gAeiid9jB8(hIUoQ0kCxLP
z6Q}j_=FOS0v%<7?TI-j1#}i4jbe^c+`}=GDo5n5s8&{VLDILk*w7*;6UE}j*Cuc70
z&ac_OpK;-KAB7`(p64VcetWU|vn<En=h7UWb1xS^uAEZw)?WF`t@Q_ge?R|3|6H<q
zT*IH^3j2*_&)57h-JdDvhti(^AO75V6!fLtH(H?Tu&_#A-9m%<2M_A&#UIqNUHZ?g
zwBvB9)cwj&Ew;b8JNTV=s-Dl*-@E)r`O}?K8GhHUn<~IyXu^K>@YZ!K3R52bEq?R;
z>yJ<G`!mnTFHY`QexAo@p^VYtyKzzam9LNbX#Ndb@~T|I^1_<B^9s^cKKs)zDt;;a
zJI|Sm@8x$Jse{MARx?_Dke`+iXzG!f;k5i~HCyKk$G`Jj*S;|QWy@Zf`^&cfP=(8W
z@d=#!IfIh?E<L>R&eURSHLu^>xaQ#9*B?N*T+gTN{OQddoUS%~%DTqJb1W=(Pi@<I
zbLrWiH#f_kI(PG<%B`kn7i@O5h+Pn|YF4`_6civX)lffIW8wW$=R0N|=G^(_i*5ec
zc3Uarso&{c{AAh3nM|6ZxlPL@Lo}AzhFBEmS?y;yAhzH|afZ|;y_{KbQG2#{ESzDz
zdBXhpIyxG6PxdSl@eb^|Y3A6bx`ktduGFjf&5OhqEU0YQ%23YbFx^G+$Lf&l{DxCQ
z*RM<b`z5<^mfzRqY`gLq4w!f?tlQDBBwLVILgDylnbQmUJ}000ee>DIj5;HMxi&jZ
zF9ud!lM{WqSbP4f^1qpU-@i>>{{8u{*!Rcxsu*2tyubhLo=vhvd8hrgj(Z%-`2Txm
z+N1>+>Z6s?gRgoVdHKYm<<2|9Kd1U1O+9|D-#3r9Mu1u4t$a$xiAxinduUv!p4g@)
zrF^m4m{-?NC@NFq@^&++{N>;Iq~0%D*nX@c^TlGrIw`h0Dr*)ePT040`FGwgxypO_
zFa9v#6&2zakp4e;-}FYm>nG%z>J%0|jM;l)rN@KNGsS|_1Y0@Wc#i25G0l1&T+3)O
zOZM!ekZ)1bVont5a{u3LWuM?wQ@!2zQP1J2iSy3KRL`*eGW+|XyIUEqT-@@B`_8dr
z6Iib<{5R+RQLj9459WjFHzUoIA}2mLWViQE;GA90cD&lbm1?~%ZR2HAhNJAYHP!LA
zo`2;CG@bVHW$EPViM^XYT)6&QJ$LWnx4F%W*B*|(nOnSjN7z^WV%gliJNvHLExRzc
zv|^fn@*k`1)f)^QRQyk$YM*g^^YIHDQs-5zZYg(NRf=|9ZZ+#gz-`+iqr}O(o6j(G
zt!k)fNtZNjl3ep3ft7XB^*@ov&1LVuX}Y~SvccRnqjAzn6~9aJ4YpEGZdc};T%P^1
zbJfQPo6B#%{@8W-0|&pzmwopVc_!A$|7G{AP6`aIW@BLuJW&6!euK?5+0c5S!d9l^
z_YdE@dVkN-|C8sa@RgXF{#6oqVf}GQ)h7FGJ1=et-Pk$LA@)Poy{F-`%3FTTxYy2o
zSeKFiUD2uwUC$C?@3wEAcfljSc6I)f)NbWmzlk2#rcI6CD4WAwv`C|CvD}NKpD|kZ
zWcp3bJ+t<2ja^&5GWJtPSA%*R+vLuU`^#IDZFOfG3C@n`Z!qV{U0&SQ=<K$BVz)qB
z0NaX9l2Vh|i+GY19LjIK&py(i6(@VScX6H*f5oDZ!^ha4U7h_{HdeQ}J^ISQ7hC5`
z$Q8Wbd|I~t`-a0ZC0=Rl|GMguxy<-%X7W`@zZP15N37(5eu%C7@3ZAI&U=O3m!G{x
zEc2)IQR_?YQ3@5tvt4U5%(b5E`c<{d`gL=eal2J8@7I`!ul!$DXWX7~>sr<=%Uzb{
zR&yQ%Z)}UWaz1GRU-%<w1^$9<^9xRAOe>RB;4SDsXJ~c(&BGtx!q&X{!?<VCkpge|
zU6)%tIV5u=te6Wf3oLtc<!z(5!140sirfN1O37~{R6S=@PZxhTXNv9YiiMk*H?Lc4
z{oMcm%rBE0;=fdGym>YA(}Vi2x6d{{%{Ps-6iYt1=9>90|F`G2KU;J|$?#mQtr+*?
zxv%o>U;2{a@X*QdVjdS;>huG*UVmJ@;rKV6^WJ_hclb`oVfHGP|FY5GpV=?b+ubo5
zbBwLes?D1i`DSm&xqdb`LA|6`W}n~Y8Tw7=N%h#Tmizu|%}bYTFPl#H+w4-WXE=BJ
z?$%v2LGICvX+9=X?m1O|`1?chTdu(|=a*@Xu4XTHMEHBPf7<$JVTtvWKF+x{b%w_R
zvL2;x<NcPm`~Sk1CGUmb%sZMcU;XIjQswR$KOaOt*NaoxbdXuqK1AncnqlFJw3B8H
zv&#bAdiot+A9>_&dvW%@-w%Ip|N8B1%KfsBkM2a<Z)uv=r(^s3i17R!4yRI&T|9q7
z=F{TN7$=bl?5EXqqBp!bzG-gqhF3+mB5vvw+UYyH>L1_4S1s{-^^$eX9kX=2er~?r
zk>OG>^`A2Djt2RL&)*x@eEj~?b$xt~`%#fTrkTbcUE1&DzFPa<=V$14^*0l4*PIl&
z{Xn?tS8Lm|j;)$cqz?RT+>zAHbmrYureA^?er;wY4|YZ6zKFluR-c@4cv`40$I{;)
zGizl2i}zSBoHH}EgDERXlHYZ!O9o$?X!#N8kB@Ut%OC##Rb-*ci-P-(@eLb~%ok4l
z;2?HzWv>*6QN+?yjgJ&-Gk!2h@|<w>X7HUGy5L#DIhH?s%nFZnu6k#sHWoVkcpxw6
zct~siPriwa+W$}9InQ)7P(rmxZ}yquxX`SCz#F0B6CxG}iPndAo+ysdR$5RnYt3nk
z(v#0hn^KZJ`NbZD31@HLU-tdr?r-)c`~KfK^5HG7U4HE1>sJ$GPwkbF-&4P2x$XBO
zpW5$PUO6)7o0&<~^yM~RdF3v#H*^H&C?B{yX|wekvCSGEWQwd3zLZIIMC7PVpPQfm
zt!2Y~>5~k{t*R$k%<FtGw@F9;|J)qWH%4K037dA`JT8~KH||eh_u2ei{?fYPGr~7=
zUO2t|xl`7Lb6JMorHuVjO@$X_KJ4~M^6uC=WA2S4f#UGZD_8F-Yw%)LVqAQdDM4W2
zQr?=0i+Y%gOp+utN|i1yJpcM&;IY*#I_KnLtKY|NyKjF{p6kzo=jZlrx7qRgMeJF9
z`;TG=ALXq0{V@Fa_8I)8U*BYL=I_20`uy!_+mgEYis<zA(B^;gGu>}!NV&KdANcFL
zu;K8zgx$TKY5`HXhO6azLZ-d+R&|~I#!=TU$Gj&s{Gt7#yYHl~y2-c&^4{QK47_`h
zXX?%^p4krqWKQ*RIml0x{CH#Qw{sUAZ0-bv|8KtcxSId&=I={x-qht<?Du)2bN{_f
zk0bv3H!})sPMR&x|F_R}gLRDj+V{Utha9+kvHbftkNgHx?=`oC?JP}iy#7`mclTMZ
z_cgiWD^ADAY@U~N_yl|MuL!*-g;sCOR;MsA_9yZ3uJU*4TOq{P^z6+B>r%#I)7h=N
zbrxhvOw?GWdqMEGk;Nkxu~*+FCAy|BnWy2sfyGYlSftO&3r)-xuZkjk690d?Uoj``
zBS(2VPi@!lC&xeDp6-82@8#DW>-zTo5m<dH>`tlt`RJNHweIK-zcai4&amX$Yuw8B
zG|7ei;||?t%QU{cm}YT(Ucl_vA-flC>gBcTiO63gdDHVe^Mk)j-Y@lVj%7|+Cnnk_
z#5FhX#Z`uv9D11&qFy%*LiJe;U-eWh_LNcOzwxLihfh}S#mV>e3E^eGFO)BiD^I@g
zzI^t-shR&3UzguFt7dMzQ#oT3Uz(d;b-32EZ{KS&nU1l2pWJJ=TKNA{bK9>x-nW@Q
z?2`Y`)!4RGU%a<f%2#RCWS`tsTB~EGzcN1PFyFvCZ+Wxxs?|&Kh3#0*tVoeudgEYj
z<k2$%oHv87@N~s^wEfL;F|Ktp*{gfJ{C@dq>8koi{3aC}_nuq(Yx(c(ayN?~s&4oD
zx9H|`PS(F)1=GGe#k*&JvR&y`?;LwY>Big{xqfs0-L>9rB~;5gZ9%2~;`5IkXW1ml
zvHX$InEaV({Yi-_S#etp7dYSSlzgv~dCq0~`Dss>6dq3LJoflquPf`KzfSs1W(WLr
z{=a#hv-pXr<^Rr|Q!R6!74G%5YAv4bSY*EbPsh*XZR|P!{VqIqFJ4%1AzOIoj8`&2
zf&H^SeEYt2<67m|xj9!Nr<^^Y@K1@C*QzSe>HCzax_g<k|C+b#Rh<|cVz*b+zv7>6
zxy-AVZGnp)&*k6$^2WSt7Kcm9-q~Bpt^0g^wZrL0@#o*IeLS;f%0tz4KQn%RJpL_b
zpLVIj0gtq0doL7cT`qez#a`wkf0c7r`OG`Aise`8eyJ7<9-9|-!J)A{O1Ue=i8qb2
zs@eBx)fKDk_~`KEN8gn0o)vknb87ELzx__fTC^@+@h&}Hp_6quaIx1rQSE{>J)wU$
z>pF}kgdS=NNm;n}x%{5DC7rGFnE8r+Yj9hxnbPiJd{{uu>gMcsC;g;mbT~fwCHI2C
zVB2Ad*V|U<Il6x92=Ch9QD(XEdV=zy3V#75u72@d4qFRX+_#T1;r+V5XZZ>ao%%IP
zgdEm}Tu+n<DCT%t<ha~7ZK)Mw*7wRKZqu&mUys$1vT|X*bT-85PQ2=_8Oy9&YC{jK
z3G0npcGxI6q*&H9W8Gibi_?6Z(zmVA6@2-TJ8f3fg|#lT++R0uni_E-&h*V1?i(p9
zuX61Qm?3e=k$K{#loMVT`IgVT{7ZwUEORsC2|EE91{IFIQO**17a8k5{f;*<x43&y
zKIMC#h~67(n-{ibA)B_a`@M<G*v@EjFn7Mx2dj2Be}CS}-AZeZZhSFWK{_fx>g{<`
z*U<gzO(zDp9()wISNDXPxp}4S%WZ8{S46&Uytm2m<@L6uArm`0cZSH!W$4dSC^O!=
z^xzf64NT>eK2FmR+q?LNEaNk+l{N-`KY!IUM9KVL*O}~dC($n<V)E3-K6gI(tHxQh
zFPA^2Q#NDz|EQApT*i9aUnHJ+<F1n?_F3lH8L@8OkdSwx`p;F(H<!Hqy6C`_&gt=e
zcf)F4ez%bmE1vU4p?C(%{r&?tRxgO#A})J+tLYD!unP@_x3&hmp7mX2YQ4lM{&a8d
z%{ks>9B&0Pd|4$+HJZ#!MS@+OHwJyZ!QK^OzJT@2($_^VuWGU1UM$$w5yIcOyleVy
z^|(GUuZx<VX%UOq|J{AJd+j^M#`{moOPA!^3vq1mlsWwASzp6ZmB_cfxh}Ekmf7yj
zIdwh7pVd5r#pNw_zINDh|MU#iWh=M;y|HkfV%iGB#77kqt@9qG_^tS^^S@h)W41-N
zj$C=S(E0W)g8$nO+`g;f^lbi=4fmgKxScC_bj_~Ft#Kks7sLZ?U+Zxe`X2V1`Zi6W
zooh}*Z<J}qbU&qor)HV+^#8cHsO;~b2errJ|6a2$voCNj%l~$NhV{xUTj!A1?9;=m
zlGoR;guGv<w><yBXV-s!l{3y)T~IE6sub6MZN`yT`MH5>U!D`0&5+6^wQ=7r!ObNX
zR%SUYvbvUGe=E;VqKu&+Da>l~j`iwcSvu<{n}wB2&02jWV!ga&#QIa`ejVF(yG>Sz
zzv#xxtj$Ij91`3=$*${SD>5~^y87*D{;zk%I<D0^@K}6QxbgPGF}<Qqcem8s)b+H9
zmN^^GY+ELA_KDHq$QPR?9%DNcde+xj&fH5aMD%3XRqLKBZzkM6`aSiMTBCSX(%C>3
ziw7P@=d{NgN16J`=|*k5YW2*cMSaGtf|-%-5|{b}mFLcy8)#KK?TA!l&C)AzH6clo
zKc4f<sNwtAe*NAW;ZDon@-oR{d(yVg7HHeNN9?`%=CaSRn-UT&Hs8;D9se(nb>W9k
z|F<-J%r~AH(YT$fTIJrcqWmi_i@4UTQ`u#Fecp?V&eej4HcC{gPM-Cl@@m8Wh4&Jl
zr<{4<A@rg}sY|$g&8A0xRf{ZEEw2xfbzoa`a%zCe-s|p$SJ)GL7fp?9xBM!jUDz<+
z`|$gz>o;-K{r<GP`v<>KQ1@-U&tlUi``(eup73<uq}R`6cz+lw-`chJyYn)Jy&G9&
z&zT-~^!-{H{YWk>^la9J*MWBvesbx0eAwaYnA)s&Cu7TWug}7%vsd16kCqPq8mQH2
zB)D9G>uT&*yIY~P_t?%@+C<3Rv6@i-N%F-8_xA66e*)fb5WbMOw_2v>!?CBwf7_f3
zxACf8d;0!&b62^4#u=;MzE~KeH#H|)wbsnq<OCnfYo;~#Z_no3*!$|4@8V6zpHA6r
zrF?t4;KiJYxkW0g%v@8ib!~IL-llgqM6Ul&m2lnUKkqL+Px;clVtUAFlW$*SeQT;!
znBTg5XbwB=vPZG^DC@?#o=K|`FGjGvNII-!k|*?I-}3ABM+6q~WqRs-uG?!leSc2j
z=1J={H8yNYiT;-zX~wyrQT0riV0US*-9&LM*3Y%yIeX_diyh28Jjb@?0{5$Lwclsj
z=zadgH~H>S+iw!@H+YLb5h%N%@~uf__mATxzaC01h-XkOUe_>x%O17w4(C`etny^z
zy}c{6=xnxf#l*xNDqD`3Owdj=VOa9<q5ntG6}Bz6iuwYQV-Gwp;jn2-4`A&)8XjnR
zagWTAeT%1dhP13&aOGQ2a5Ot>d}CGgqV_$8(Y4+7!Rkc}JhxX|{j=)yO*!M!UC)lU
zEHTZP?rIbHPWXUl{rNTrDY;WyQ;coj<_4Nxkzn}j{~*=$;vE(*@A6wmx@>&|Us%=6
zKQYNub8fWBZspRoO(DBZ1T&`9cPPrvT4A`0P59UcA9c4b6A_b*FTQ_mjAK6kAwxac
z{PNZ#o_Tpc<@p*u9ptphF4XzI*ZJ6#Gk+BhS#&lZtmbi-nKX57%E$MAJ(x<Lo?3nX
zOp#668gX4E^_z0lXPzZg1b0TSS#-W;wdfZ~w~7x}!+n~(E0!lr?w+!PVeO(txjO?^
zY+QJc|18s_1M7@VXhkcRMJF@Q3^?$0wYuI*gN!$l7ps!$*XX|DytSb2-366hz5fqw
z?H9Am*nBkQe~;QeTe;(}r^&B+qI&VsrROJZ#4_aS?^3;f>*~#Gw}kpNc)uFdZ5G=d
z`KUtg(T3~n3%@4FY@cD6FR_<5_V21|TeXjt$Qx9toI2*J|McyO#IUn(-xt5mS+D!r
zrNiy6`_s8n;vcV0u5@Jh_rLe3c0|gaeBtxG4|NaBHj`_c{dBcW_M^_e)I;nn|Gsb0
zJH__)$N95){T25=?Qfp&{ng#0xlbkd><d|&Z%nT_zd=XrtL1Kg=6i)kuSK)jJ{~i<
z(!N&AeZ#rzX%T(B7IW<MdtwSN<R)vpIC9oi^PT?scb4m>{Z~Hx%h%i3VD0+d{XB;?
z*K8M(UbuuI<#EVOwJPqJ34f|}I3>KpayB_^*z>1FA!_YuF@1)dPsyzRl~3DqYAxce
z5V&=u?rnL&`UCdw3P1NJc72XkU2ghfhyUiref!_>ta-nEMq_PSej!`gXI)p>jdh=I
ztUhJD;r+cU=jIrnIbby9R6hG!gPu(p`Fc`(jNXlN8y*>6y>oqa)t$K~cQH(t=+P20
z=Ca-xvZT;c_ey93*U`F&ODq}(yfs(cP7D#g@P%RjbzjlLURUqUzc#HYiF0+1U`oxe
z$<7O>&HMcQR>Z-aO;K$d?z?}fXj|3F$X=)Vdh^5nA8#jpxDfJe;qPO4$*kFDQbcvn
zoc@sSGJWUz)~t8(+qe`&3$w4hx_0Y<>KW~YyiO~a-#q!%_G`iElN)PRFIlxO?5fhv
zq_eV#^4eG8uk@x^ePuAaZMtOU30D(gzZbt<mi;_xG56a-?L|+`nl5hK)H}oVji21#
z4WVV0ZyMbvCqHhNYkMa2fAftf?wrXvUNM&1O<O$rVvn)EKfTQJaIOCJE1mOAP3EjS
zzh5*;ueoetjo!_hV%cg%jVAXlERedrF?Q?is=c$#f8LoB&T*~N$)@d9;*O`MWnDM>
zY@U^&F+20>;fL?itX_JgGQ1Vqxar!em4!aJzjgCWE}6X5zk51nn|H#hDAsc+TPq!o
zwj5vY88&fi^apD`otw6qjrr5}+KZiWymW2G)1`d-d86MK{Mvc^-_^i;(TGMtAHSCh
zofSRBRz5%Y=a%jNaH{%ye*RnDO_Mr#t(yK{deeE~*nC4v%Y~Cq{Jn6heCG6n7p}R5
zF-A$hx@&X&hQfz5QP+_DIxSL;jh~&xkGwmzQaxAp|ExkIu@#rzUHg^N%UT%w*LYpC
z^@oks<tdl<_w0~*-M9a2(e+$wxt%*|cYJ1Vbh!U<`s(u5*MC>)%as3oyzwn>tke6)
z>*e0^{Sumg>lx?U6IZtXXw%pKzr~`cmM<kq>h7#=ZkyxVRwk~O+IIR{!Y<*JXA>T}
zOgYw;pdu4=sN?w@ixAV6z=aW)zG=1ily~%`q<Zq_dIbh8HgwEBw8GelN8IY5#;U-x
z3l7_}>Hg&|<37)!@K*BTlf<LthVLt;pQ!S&QBHiue6A|s;zpf<H10a}64$;pk3Vnn
z6^iRqpLYLGODnTs2HV5@$vYN(`^S@Tlku$x+ijC1!_P4eu5taV_c?CUdf35~EbpY3
zvu?tVTYtD5PgyvqpHaOjssCE>h|txjUXM!Mpv${Hd~B_C*OkfOTjhITj?jk|iS4Y{
zKhDW8lE3--^(*Vn`;QAb)1G}iUOBULhD%=MC3&vO53_B14?XU0KAZLR)am1X-@d=l
z{Z_b*Q8Pm${P}lJHAhkDfLI3+rn(DTSIl~-y){s<clC-pI)X;p7rZ01y}ktS+~q#D
zaz<Y_@9(?+O631>>rA#(xRcSk|Hq|IySGFt*r-^|>^YJ6OfK@$vf7+I3USqKGd2AF
zSmd6rKgw-YzA-+b+2GN)r%YF(zTI$)Woy-0b&WM=Ps|*K@7d0oOZ6@$seZn1(6A|R
zngkb%l@`~b9p~@9{nr2Jp3(`W-Kmw86P}yJS|9(QwBm2`0!D+<rp!0ZyIB<*BqI-r
zb<aMS*%tXv$T)E$XW9jRhpVEp@`8>lW9ud#s6WA@b>;N@B<6ET=PRr;w%abaYqorM
z)YXpr>E@k2&9hc=YdoBn*ZAJA*^PUd&J5qw+xO<1e&SQwyGLlPPm31UORiqip6*xc
zG;SPRbhe?*&THA6;2k#%DyBVWRco1eSfz8bPPM0i<I%r0!rXs^{%UsG9N*;4ba=OA
z%=t4Om(HD3_g&yw>$Kr!i~gyy3;F4Nk%rUw&tCp<e$tQqEp<%#e&U98k_mTu+=?7k
z-?VO<<a1;b=dap@#|k<$TGTFPCTNE=N*~pGws6ti1#9_}%}xri{E@nJcZ;CwAIoVQ
zZ%?y#zw+sN!J^A8PtA_080gxo{YgBwQPneH?zZV!UO$-b=~?FfICa$0bJou9p28i8
z@rAPA>{eC2Kkphk|MpQQc8+T+1vf9TI;<!VR~CKbWr5{HsmSZm+>)w=Q`WprjQw}0
z!r3il*N;WNr(H;mN?*cyePe=j;j@ARhxbjtx$BSonv<?!Y4^+D`p-KMFLdC^6UjG)
zm41&Nn(I7&a5JsY%tyDrr)S&8sq+opXIj5}b#!9PnypJUv+`YMPKoDT9<ohZ*=+8I
z9j}EB?&8Q;xkqJ}SNDd<vx`?OX}bJ$@9JYq7uQ*DTvy%U(7S4v*hS|Ym9ya;E1vGU
z<i4fkg2kU-XP6`JS}Sjy$<}^<*LF?ihyUiCzk1W^|GxKsCK}J?Zz|jOM|Jn)hl}S;
zxcz9$iPBB}JH5Ajmn(dm@%?7>?Db2OE`E!iUhlj0bN-B`Zy6Vaw|?~aF0!LHG(doN
z!P=bxu?Jq4MV@i0pP%*GOSQS<eZb*`39_4Z79>pd@{m5UK%JMZ;k3=&y{dOZgQw1M
zx!nBlvTf7JU8@Uc1<b#^^m2=ZPQ;FP`<UlCi^qk&IFq{Z>c`W~LjA6jFE5>v#u+NU
z+4<3i;9To%d*<xFo8)iEIa}f95BBxC9gNS7CwUjKe6c*VJ}bE8mQ}8Gv3M4f%;AH(
zmT4?+(^++0Ws%zMUe}2$UZuz?-g|whG<E6$|Gm#jcx6R-yPjDY8H%i*v-d04twSN1
zj9orlveVbt#V@$`cE$gg`w`z8xp(gUU0nWN#%9V-nSZwH-+VCD`+w`<w;TO?*Wb>~
z_CEjbMTu70_p<$t*;D_g@j0({InZ@~es)$42ZN=h*_90Am9IP^WM5`v_gYsTPk7&O
z-+j)l8&bK!D^F&q*___~JCu0_L%o_`>G!<emx~Ie-x>c%&6Mr^Xm-Hb<HOp=+rO%8
z>;G0HbG~baXzqT7XTPUg)vq(oNRHSrSt9$ut4)V1qy+LUKfXSk>+8s7d})IBhQjIV
zZG9yT629&0dwN#yrM}D}R{_h}_tT1ItotFGSNpV5yqKxvn}o-Y9oLhmoiOPN56yL)
z`n~dM#Er!*&N<0v-Y3t_<(pR?dPD2iuFSQXdo3ii&ra1{`un}>7uV9T;<pLMe)03(
zV|rU!@OjUPdsR=~d{TdPcipii>ObzBzC14}`r)*9&hqw;)_u3OzHfKoRWa|Y%>m_`
z19PwLS#Eq)NZprRZSCBOMI1I+%$$q9vFzIEA-rkE*G$bvM<=W;N;c^8sWUq6HRr{j
zqc^Yk@E`EE4y|&TJ^8iVk=m5dxmBzIx3f7O-&XRIoZR@|eoffPM6=HquPxrjX1T&?
z{{&mH?N?<F)@PP;p09qjW4hg$n|GPM`-|OsH+h<lS@z#`*Zj`RKDSwIvTe%q7^!$O
zL-v`m8jRacxU4h2c#G*(`kFYYfb#0AcNOkf?mqQ($!@ppdG}0SJ&R52wp5xK^+@aZ
z&cYp6e4chbom8~ST->PR_ua&R7d_hhQZqHCMNi!FV8u1&y=Q-w)O08=cI@<-(mwN`
z%YAMCZMl+v_V^Z6%@x|H7v#L#`nSE1x7p(Z<*IQ}|J;qw-;OCda3pzyZLVYZ^~P&k
zJm2jwXVO|{zk$0X{7rV&?f<9kTN8J`GmP70`BrWIYscL$_^d1@avG?;YxbMmt7@dq
z$Ff`cR{@*s3DeZ7T~@OfrAVz?ta^<nZH263`>_=}Ia3=ZE@XGt&NaOo;k+X1@2lQJ
z5=k*RidK0S9TppR_~ouzJlkT|6q}HTrf~=MWNdEMINIYL_pg)nitCvfPyVwn3(U`}
zuHJsUt$b#!82bu)DZTSQYu+UHZ%R9;{J_4vXp(2upUTel{oCZC3a@SKe|b$)jzMSc
z1<&6_|Jc@FesE8eV})M$#pB$PyOTNR7PPJwHxa+MYS-gmRr<2>g&jNv|CLWR#r|7=
z^vE{d&T|<x&)K)ER6F<huijj@-&NInmR^?6>E0S-$NR|9YSqz;3G3GF)Jrft+>sj?
zt)x|w>aHT<^3_9UR^9ayHJyX*S7J6Ft37)1W=Ua@Sse2r$LPaz_hjW|d^ER6otP?f
z$$xrR!z%YFb3|R1ex2jK?)y#EKT_Q%e*fT(*JfUmUHMoiviYw2j-|#kGx9r+&-!cE
z9y!UU=T?SLb*SnY$EJ#&jBQhbz4~0H<^&yCsPA_yxmIqPMuqFrX<WY^iLBeM5}G8r
zeOYYkuLB1XA_D~L^R{x{mUt$1@$mn*HqDk{TOy|aKh2cHTCDK*W3{Wqbi>`hKR7pK
z>}uXRYiIchqvH4Bk!O;1To3#y{>*Q8z$$fhhkc@`kJ`*?f!C9a7=Kg+<e%d|^Qf%x
zQr_1E9XVfjaGp*G?=96Zl46dF6Hk=-pEd2cUUx#=Ki`R)>whTn&zKb|6SJj3E|G81
z-3-m^v!(CaSTzMK(3II!x_t7?dH+I;j4tp?txA!S3`@ygpeK;orE!OE-STM8=GrGm
z4u|gDn$){A$xbNo?(NX)^51xzXNP<_tuZrL({E;=ctgg~q*e1yU9erj9X)@UjuEfW
z=I0k8yiX<nUif1A!gA(!Ww-eawIg3nzPih4_qqLDO}=a7SG~^KZg_Tw?yagBWl4Lq
z#a4?SJ#bxj&x?Z}GQQ4STerE_<!9`gh3%Ct601|^uPA<Pm}dWqNp@#sZ;RAjz2%2r
z7ag0uXhBS~WztJ0!PQIqm;=vp`MuGY%pD%E{L}%KUrS_fy=hq=q!hxv*-NOh(BXmV
z?@#r?KJ_U^vwyj7-&PiNHr1%+Mhg4A>NlN2R&^8k&9}=(7Vg&Gc<tEEctg$u{b%kq
zg={`~bZ7J-E*teiHvgYOZbv=E4%_5Cm1|s8dFR<q-QE2;lJ!@wHGX`PvG=m%n&R8i
zKC&<WobM}hxtv>FU!@q>Z_lT-DEa4XzU{a93R=UX9IdSwwms0eH8*(n;f2T8jJEc~
zC=~9#I>-NP<AW8CnQks*G-f}$C|Wf;qebMJjQgBV8WaDvEIs8{aqA!J-pPiNGFL;(
zF0j_NF`9L<cM55+muqjo@43}^vqDtPY5VO-j1?C}wrr3&9l;bADYTQJPFD1eqh`VD
z*bw_COBLJuch}53K4<^wzWWD1G75he53|>g^5xZ=?IG$c`~7W0wMeeh_GeK|rGci8
zd{@q%`|8=YX*=$!Y)U(6cHw&V_K&uLN6ehk%cozd?0x^o?U?bT=A17plxx&0zHaO|
z)BW}3G^yO<aj_R4boMEVl=f_zB@@~?>0(di@;!fwXP-z4R9Kwletz!u<wkNjKhJ#n
ze#YjREMN42Rhg&u9AJxOywEHwH^1ViQMz!9vygnxHrbzF=N@Mg60T5wAr@Yf)_(k4
z=bo#1zq%(Kca~^cKGXWZKDm>uSFJYi#rSP{;JL?yxy~&#q;>k*T~(1AFCM?}A<*by
z$TjiRzochMEfHd~KkJ=$bm6TfPvbAV`<m@o!kKq8Q|v?b#+<d;)jLmTOLWQ?Bp*B4
zp{Dfou(o}A6yrI+Mf~bEO!8SF%xR)4vldvU2p?MLk=(4|(9iYZM62!UvS%XS!(z34
zSsCuc&O60<bDwEiO6#LARin_}+uL~Gy!=(u{AP>QH~T#6f|ScI@4l?9jbBr~^G}48
zh4{U_$*XgUx9?NT{ygKU!*qAiXur8HbGBJb?%#T%v~GK6xAot1*FH|Rjorc;R@Zrc
zli7wKx8koGkFjkilXW@%JmN{wD!y~)B9A;?<ut1y=hy<Rf3MU!HZOVq^43v<cZ)wu
z?0U@dLGiBI;<F+tZaIFD31`F4zH98coc7tXZ1wKdiV3?-#g6&2J1tY4SUO9+-Tr{J
ze97;B*Y3U%Idi7c)8}YeU`GAHJBM>zWmB&87;pz(V6j`Ireb>W-n9Vnj^j$QD;CR|
zaQm6P$=6=HAyxa$10g2P_`urkG_Sp`@w`mko9_5`sO!5J{&v0iSy*ZQ_IExhZzesx
zK4WY3S{DcF*!>mjuf7%2+tHcu&Z0+k`qh^A(f>F^&+0L{d@jG^xb4aF`RvvzE-T&_
zHSIc36{6Q+lh;?eyJFhoj>-EsKdZ?$i=I=+YkjgOTZgO0<(byWl-1kZJl=L2On!HG
zy5;Vl#}~=o-~Z>o9FuAL)Q!!`uc!Gmr5n^uKl|^->4@zcYDD(?CR85$_hRvdt*)Qj
zBi5}*wK?#{W$guT>613u&C<VCSiVTTx^0Ejhv-Uf6Q-LRR-E@&I=(gZSKaKHI(L_C
zkiGmeESGh%L-?GWt5a(it>`*)exKo?P0siCoUmkh^K8R`88;Say!k!1b`D3G?qToy
z=AAta|GN_ZaqYjY__+Ojy21IVr`0Ts*3<uYKR?ICbbhvcRg39HzNDiov`?QgOXEqs
zcrqrbV8Lp;7lkub-2&TxpIx;wFry=_-6tv7L?~NMYD$Fg^*dJ%TnpE+om#O%rDF5c
z%j;7V1EZJuzMi!zYEFadtyN~}o+>(Xo0evasm!(GUA?fMJ)x&jEpVUOObhPn8L9pq
zVwJ3#I;TIKjejCJPq0B(P4d|ihDq1I75~wRoR_ixt6<~S;&jW&CnA3y*I88XF=D<#
z+5JMkFVSZ{Hhs{@-nfZ>hDD?ByZdWD#u*1ElsUxTnjL&Q`7C2V{98lQhfiImPES<$
zR{59d{4a~pLg{UnLpLp$$go8Epv0T>tvkOceeQj=hSSShJ43kb%Dr?W+xtIbjn$L;
zk9E#H(7ASsFYC+ib3dMAHj@8je(Ir>_OblWGmcE(H|5il=!kb)&3-HQZ&Lfdbt<Fk
zxk&LfdGm8FZrv%8uw=pEojYzPraP`$y{#dhmwQiEc1}l&bC|cwM#Zpgw=GoP9N83T
z{yp69<}@*}le5~+Z2!KqdgU~?cF9d!4o<sk@p9X&ZO<+HwrcB1H`lj2o_B7(c}Q>a
zSKjOYo{L&2HeJhl^yG=ST*N=F^N!6k*4(?Y{bXJBtL3L#SImFMajW&+xzg-?yLi7c
zCo^Z2UrfEOrhGPeJL}tyoV?e)Dek*x&RVy-fo<--tCpeV53X*#y6D1<+T^@FYYsMB
zChgd7_x;}X*XPduXsIin`)}VF>F#;AQ|munof32EtFV_^)#kMy=3m(CaXN5owS%m-
zwPSu?O1Vo__Qc6nd!^?r+`3oqilWu(oeRXL--wr)_3P!kz+G0-yX|I0l?H7y-)3fR
zvc)C8Wy#*1->&UjmQkCWy=C7*-^oIXuhjR*&OQA#zrW3SlW0iuynpKg7W<xC@OJj^
z?O)BEr{4=)zUsu&Am6`P)6d_&k&vEyc%qr`ZBdQ$FNLC}#dK<V3+?R?nsn*fgEb2*
zKW=n!T=gN~%#{Ljv4^Q0AE!OH_~o?Yw!8VX39cTO1Pb>&{%9^D+wcAE#NW;BYxFi7
zznCbg`QTI~^WE5gQyvwypIwu(aaQHTjVJbn?_cs{YR#HMi96Nr^|*%bZ8qM%^R3wK
z?aW^icehVJqvw~jF+uB>&7QVw*TV~5-=BUu?^JKd&aZCn8!v7)w_d`xnCtvK-!mV%
zuEzx&x>oQ`c5jre?$W@*&-t$(z7}J=%v=4_=>OfWtJ1|^Q~T~3ecSTx@#oBM)*5^z
z)scB7*X}sYSsC>^cxH}!_vE)9+dL0PMHIWY&zc&PRiA6Wy6)hM<j~uPyWUM%e&xz;
zBdz?`y02a;Uw>G2_4#rgk6yt|-ZMAkmj7y%*|cx#)%xcV1&8#1-)Y)&+tf~=ORlgq
z=Zf#^#|QuUtiO6NT6wK=pTxxGBk9~v6gK|49VmZmYH(-a+`g4uH~z0NW1P^r<Q3CJ
zkL>nC3h|Xf4Se#Eo6at)=yC8o`{sT_@by`Toqf~S?_VXycJ1rGr1uf3Un0+bZ&IEY
z_%}m))`_Ji|G8fkUbMZnR*gw(Reph>KF_?@U#nu~D!jH<-pkB+>_z{?rA2Y&#+v=J
z`EqxyvzcyRw)dQ5_QmAd<-Jei!-Jny-Lcp)@9JXZ)oVF6x4-E6AfWYRl5_LlUz<KS
z#M;J6J`309<$w5CH2d(ZKPw|-<*t5D3Y{5p^M2CA8C`RD^+on~cFf^jFZOu8fM&37
z|H_!>2@|FM{%w(Set1hn@`i+~$t(-4@PGBvvo=V^-LdeMJD&Brbi$K|%a5<BOp)+?
zX0_Q>Oj2vpi3L`BW~m+4_Yrp2wNt$Gb$ha~;l1sJi)yxh-1|QF&zWN<Kds#u&9T0~
zDt24&t{lhT7jyI${|<g575KZ<jxlS?wjB1%^NaR&F7)_&??jo?TwmU=(Z36qHn-Yc
z_j{6)>Da*cV%8De%4mjKzCUI<o2PxbX>e38{PaRz;eO346Rt_gs(KyYyCCpe;Ja7t
zcH5Ina{S9yJMyJowO#zGPs{GM;=1Q69x1G<ntAq=a^=as+uv^+M892Fdw9v8&9j>S
z9DBJkgzvNIjv~YB_eHMGt>NI@zUr^?t94pJdHp%Jq$M}yXXQ#tb>*x|^VxK8nhry0
z=I+0%sy4XYxy14;|M0ALTXW{|UcMZd`aVWJ{o1CIUn}y@>TQ|u`nKp}OOegDf3;Qp
zy0~uR!nw}6XXQdyg$HiV_IS(r<L$!<&0F1-x_YM;Pi#5z-9#!pXr1KVJS|_@kh4>E
zuNIG7F*o?Mak}#~K6ASX+Ey<lY|6JS`~Fcs<;{*D+vVNwtYfdPv)v}Xp)BI;b5=M0
zN1rdX`8^TJU7LD*y4dVhRyj2RPm>d`KG*%dZ_C5SZ1=^jihl6+{kn2xogMR%+QoVX
zE)yee^FCYO@o2+3eI7eUwuaChcNy<oPAYk|vNQ5Do560!Jsn0H%;MA&Kh!gBInfZ`
z8Opv(dwNEEZ^-Iffvw@ke^snK)Z@bw=iYp9!alRC{R^Fc{5tXWdRuN5r{JG$!GBK7
zxo|T^Kf!E5z++M0XJ5a}TI$zRrP+5gXUX&mor2tn=iXVYD>~B@H+NT<+@{lM6`6g#
zbMiMnEQ?8QiJQ%^e3ko?ubG9VS2fNkupYUamQkwyY0*BJkXJ@lyLasUvE$h*C$XBK
ztHoRmbI%8#zP_~o#Tz&A>1vmDueuw(?!vT4*5>1}jLp~oHW}>5`gL1C_9n++qc1I?
z3Ig}i!}jqD+;La@u=rrifpEr#<BNa&b(6@l`pZ(2Ezp#))XktTc->mICvkKAZ%)l;
z*16AOSGvee;%xb|DO~RtFKc+!<*szkqxM8=r?=-(JNvf*eOIGr-F}tLxP6)9uG<Xn
zxK^cY%DP~<?qK+p*-Tq@>ur&}JNvc%uizzn@6Kj-+y1prq5HID(DmD^^P~gJ!*@pH
zh{fMHddSiD%ak&Q)X2T7Z+x$IjuX#bQKtW*`{UJFT6f!QO+K`GP3^n-$5g|=El}j`
zN!LZIk|rxN>M2!*b{g-<bNN`<{NC;2JuUythnG^c0+wbn=gC}ovhL+6L+4)~t^UcJ
zS$0Vxv!%{mZw=F%E};jj68hJ0?@lv&-0SSe`*_xbdWow4yBfsZ4;tM$n^CTJiN!^n
zdCn@caL0Rl?5?g)Je=f^lkB3tXp!27_X~`YZ9nYhc`S1<p*r(ee*QYo`EM^TIDg}b
z$oZ|)p8x4_Pmy~5r|0sbFFiN^^_m=rGFqw@C%<-@?(2m=E=CzgN^KK5YSx^0dh3l8
z)k~88Urb7;ROqfNGh1nUwCuG=ww2Y(Zp-UhPdd|Xf5}_&HLJUKR)l?EaLAhWUrAH<
z9?!VPe_*-n3gNluRD)C5JI=W<9bIwV*=SLjXIA)4@kPS&<`zyOk=x#IIQs2bIdA19
zi5;)4YLy<jT#vi1|Hx*digZz5Me8=9fZjr*!~>DXcWjn={DjSL-6zw%j6JhE#rEkP
zJ6+z$aZBP@h_+$E9Jbu8t4}u+Z0a!BQn-Cd+43tTk}tn-EBKy&x3abTqs#Kw*%^x#
zukWsy=Cz(Tv2MTGu?X4kZ-2%7%UQSl@cYG96AlC)*{%F)#*)5?k$=B!YdC!~T0G-L
z&+#tdUpHQSSgh%`bnfE5M<-|St=b))$(wxLvtjF$72*$CFJGOM@BHN8n=HwWi*W)R
z>@Oo7j~Bf?EaSFTm$BZ3bIXy}HyoC%y?Xvvm2ucLp_eb;Z7ST9<2qCB@yZjCwebO!
z^A^1c@=QzZG*-2AT)TXGN!+&WHF0IVCpqud@88ii<7WHYuk)sD^xxF?kZ<;71L238
z)Y;ZPQr}!yKAHLa*A}D3e||^xEq6q3pQXy3%sKmR=wyDm%}NuSd2g;|uhCnrd}r_U
zfCnZ44=(30Sv-wdXK#IFuAkbis7Gg?<S0y@A#;xF;e?a#gQxmt&DU5lNz=rrXWK3r
zo=0yN9<!S!BRFM6VM0*hY2S(2VLulKw|NS*m(<pptg2$Y%NTpx(69EvuIslR1Z|wq
zE1wYgd39WB;)CN>%-5{W9kH!{`~7RKUb$SPMcwIn-|DkJ{B-)^E|i~s^Lmolq<NRZ
z!cI!g+xP#P*`7O5w>}i*CGFU6DDnS{VZL+owr)ZGM{a7JIwz(JtBBof75F0X_fBk4
z#ri^{cXw@f&b7UpynE)8dH2@&?ykN(`^B9b=WkBmsCCQVFX*22HdFIivNA7TzYJr&
zGPmr8?JCP<?|4*iGxwED@6GGHvsHLzY{iuwbG_9wKc?)-5|5j^h270G{=XFa4ypcv
z<vJP6v&<iqmI?DFwk8~txwn6T+>MQ;8!we_(zmvmapR)ViI7~~U)eeHe(KakUlA<W
z>Ober%T;B|Z7#}j9EmlX{C3}myZ-rc+2;eFEq%4<9rJ-Y`KnbdeT&SWeUq~@vA7_z
zb;d@OmA)ODqpmdmQ_~MHxVx0k%zS0w)Zpzo+hhXR&GT<<-MoK2Z%XWng?=lhuf4gj
znO&vkN)Ml`OcMLt9|32XfB!kE>XZ9yW6hM<z`f3@th+rvC$~2|dph~owKCR@b#L7b
zdKPbuiTm(yQZrX^`PTIZva>BjeZs4E8~Ccqi~e~wtByJQht!JIwecO_UPS)1`z&JJ
z;IqKKY=M*BRl!i_IDhwQ=N;#sJ$gLJvT5P<Eh{-^N3m-yIGU3*F*qqgCTmUdW4qeN
z)Av8=ez)UW#ieg+-kRUpyn33<i!0)mQ9f7HcRa4GI-cHr`h{J6{C0^$C0l3syzC16
zb7^u);h!g-n|%H~y%GCcKFj*5;Jh1e*3Q0B^Z#1sj#?r6&u2Iy1^+L<zU9)>!YQXO
zhZg4>lv?drJa^rk->?53i@hHEA}DXiYGIQVecvLCZyvfDAiswD$H||5CoYM8*zEDv
zhxMjaw$a4{*H+8ERor$_<yJ1A_086McS1HAMoXKzWkxtVNkwR%ZLV0k(|PvWZ*F@F
ze)7!T`FqjKcRnVc?>Mh6&)NUw!I2{@CQZFLubxQ?%gvv0;CJESRkgQI+e_xkiB`T*
zn7x}dOz2_Y=aRgo`wmN%7+s&J*rKqG>4+|atH|zKx0f{8C8}97D}6nfF!#Wx7^97+
zJ{RiBKN32$w?z6-)U4*qD^fe+4zFuDd-YY*Ixm;#)6oL|7Hn(VP*Wb*Uog|D&6eY6
zu;KkvTQ3EN^DS6<=fJ(Yq3Y|8BzMf)yUG1-yj5N5f4K)NffgJ3E%&|RnD%+`%}Lyo
z%DA|v8ZI`KJ<Pvvh5N<{#j$^D&C3){d)t4O4DhVKogDe$v%fv>E3JvIpZpGb&)^%&
zD)s)n>+Hp=zv-J8u3GiMH*1e7+pV3Uv%Su+ozT*stFV4;^cJ)H3~OikrAMu1OW8;r
zdYza1V?iHp_oEk=f83w(Ug?k~^O53B>$h)RD*9DLb>nus6+-bH)0Uj#`?9K*`R{_8
zJC!e*xaEW|(Y&>Au42Nw_n+5C&AMu~dWyf2x8zjqkZb1bZyUaF>#Jrpcbn@qXnf4n
z?rgEM$V<9avnl$PI2YHBy?@`zdH?xt{Z7XE&SuX?zV@rre^$94Nc?^7lbVf+m)}ao
z3%WBNGZ$GsF)(NGU$xI6<Hz}inl~TKPwiuPypdHWo{_cl%RK+b``K1y7)RYM4$S=@
zqOoel#0}oje^+z6EYdn(G%2$8hxR^eC)uvQyWBsnD9baNd7)^%;ralZXEPUk*?hZt
zQ}nNIcQ4PrbARTyn``XWpMCZJ^3y^ap`hdcG^E#cw;8>+k|}!}*L^mv@|@D?XWJQ0
zl=S{R{wDPqi&DI!Rg>KL#!b^+a&FlzGwJ1pc^4=4EK-ZE2=nDz6<uw-s>7x_Bs*T8
z<LAW0GmDthI`8gZz0vWmXsGg8-;;iI*;hL`pPD|rq*%RJ^tD~zYSs6aYYX!^U-Wv5
z*(}n(a>{N~_3EsxODc>Ub>cR5ocWV@N!X@%<+rZF<L5jo`m>&ScdOr5_121Q{vQzM
zbnN>D*GW&+v)9Q+=ZLi@I=1R;?l?c4f3Cu;BkO1Qo~c{qxn=R^o%_m6g_tJKc_p|p
z?9Ii{&akb`tAq?!c_pm!W=LD$S74}~xBiabJN;iqQ$)FMZ{GSxvSEYl+pVG8b9b)D
z3cSPSZrc6V{bbCQi;GjjUimL_-`1nvZ&I!HYtiGIE6=@Ml`ORTec{BD%dej|zch2j
zea{@V%fVsQf4@acI};E&H!e3mv0-Lf*`>)F%bzOUI(efs>-MU{8(H;aG;%rPYb~~Y
ztazhQ7<`rQ&9dVM%B#1==1f%&esO=@fy=LD`g`SXocMX;tTy8vze0{l?~`|`UVE&z
zyxKZqzl-8_Hm`0$)BXLS?-gQq@))jeeEx0sOOJ2tOZihYeSe8>I@J(c9lXHq^@j)M
zkDhA&m5#hMQ}C_9B9nz?CDrS6@A>%s$S79u<NYXhRXR9YoBvlv_QB)4?zI`r#V6<a
zPhD<yTITY;i^29=7M9K3Y=3VBum0<~3{Ca^(s2=YjrUKjdiH(M<DK7p{4*yz>&Mk<
zRPq<sOx3oVdwG@B)S4|@7CSnxf6pfS;Uw3gQp?->8CS2p{yIVaYPj@^op0_h+;XE)
ztY~kGz+KyDw&L5rql6cK<^5Xg+Vo}R?WK;T-ZS;0TcWH*v^tUm4$oQ{KIh6ZmRps{
zEK$kJe*gMbb7c<W{y3$3L8jX0Ua5!FxySR~nAt55pI&S0P+opbj9H~9C3Sg%#ipW=
z`Tyfxm)+ytVYS|I@~pmd{MU}u*5;?*U-h&8z?51(o$E5U+TWe!<!=&L#XsqB%PXtk
zue-0F2`gpsnN>3Va#mpCoy-;C&7~_-QtmI4y*>L%$Mv@YF*4SN(rPLWRMc(R|Kvl=
zmB+lt&xQOfd?U~BbK|+V##_QHx59P1*w#nvto^h5$NlT`#Ut{%W%4erSd}BKGieI%
z^?NQGvWyO>iJ#2*yXm8g*Q>vW^qQ7kY5Uc<Z1c{#N53Bx&X&I78##6B>Qb@Iv!p+$
zTouz}UL18UE76^u{mz=z`}b=5n#50g{yOIA!?h+SLw~(Gnc-=w^_!nT)O_N`yjtDk
z3jM1&uC2)q`oVmWS$3J$*JZyQRJQnE6kA~B=H9j1zk)?XRqb^_=u6{Gmvvrx2Rt%<
zx^;QI-dyGu|Hn6b>-5(w{j+z2SdIB-xj)h;u1}cU_0#4l^ODm$e~3N1nEn0neVd~K
zeB}?*Kl<OSdg63eWAbMc);&+SGrsov@w*)twT!S5j@j<Cei}cAn)W(hhii8B8zpP+
zecGt@@oJz&_79#D+k57-REB>|zdUL6{>8@>%>TKw?Ftf|bMBX$Yntti>Ww=#HaM)5
zjf`SUf2;rD>W>AtpC7xu`v~WrrOMJ=|Fn0?AD=B$a9(DQ#O-uLPTRvWm#<wHd3W=g
zr}B|^K`ayQbkBFSUwB2Q&YN$A27BPo%~fBbi~JhWOgr1IEj{J?@0P;i+WAZ-Z5$EH
zs*5hFh5mM9wJh-e?Z(Re*X6&T>z<jGe)~cKLmIsLuC$#G%}9Tk^P*XF*Q?FP(zWk7
ze4Y2zF0OHZ@TOHS<!)c<66^W;_FF`8aeBT_^iieg+`o24msNIGtP2;hV*a=0{O+B`
zmv&nQ&6o9faBA=NkOLCeUdoop>&<LComIT&Xlsm*xaPWi<9AW3e@Ne1BE359&aDgU
zV!suvwY5z-H%VFDwQONxN&33AkG6PJ1*N#xw@WeqHJl{HW$I@1QX*Ms?$T!QO{&w`
zYG;4D8hUw3N^kbzbkB89?{D}gIr;SWHA*MB=Ua8!2VQ=BaDn~z#lbsM;v;L{X}&x!
zxpn^<t#A1*t=Fc!SmW5uwu|S^g1!|$H^of%`%(Ww>*Lo~4S9FpAKkIPh`UO|d{^bw
zZ>M}szlA@$qAMD$vwh9>qn83>94?wire(j`#4;<lR?sX`=v8jT?4{fG)iwXtDHjR9
z6Mi#w?@XcOuU9sluKHj8*67vbvRIzdnH4i)wkL*tiC=ujWuL`H;k!-io*XmD)B1EN
z?7<S|1<C7gt(DGx_ww`?fjqDLtehMBG&lV%NO`|baM`EXHcNHej{UVuy?5!kOZd*h
zd%vRV7o_XH+?p5{{qLHVV_9;|_n%ic<zAJ0dFRC|zduaXOWgnezpvbTlIPGd&R?=?
zw%vOnmwDE|qfVV|g_r}c#_ZMEE7q_66X7FZZg6}n|FH|j)me+~?l@PT9Py(1>is^$
z$@jKCV^6tJ{whea?#EV<Z34F@WoN8mSgWWJ{rdIQfD;_^oTK?xZk!{Q7bETBn-<1=
z(^K{Ft$=;&o|OKTIq);{#e2ppEmkKrHKwZYY;V81D`m-2#YJY<4H<O8;xD|9Dpn4@
z-d4HxUPSNFPt#ZZzB5araN(_eH{AX$$lmMrM5^G!_Q(8(KCW)~$MxZ>)t`m$jjn!M
zzWvo|E$5w9ub!UC_B*%cU+zSW;yGuoyr|vz<XQ~dx=iI_lN5%PmdeFl8$Rt4HaPSB
zi{T}PTw}}SD?6E_dtV$_m1W$xy;5{fN!rO5oC3Ecj=1oz;QahP@LZhyi}fNqcXyp?
z-q|2^k-=QRGN}24DEqIRmJ=o4wx}JL&s0{yDE0bM&$~zYOY7!sf08tJrOQWSt`g;X
zA?f|gE^s<b-~LWKQNPYB@yZWXgT3}~&zIGnGJTzYfnV_3&Uv;!->7$`M~fKxF1}s7
zHg;-9S&&a~)~=SEs@RmyO`)Coh0piYm2jS3+rMF3^V3}h|Ac!*!?)&XMYfi>zE;aG
zdwpr)FV?>JAG`+dzHZffn7rqcsl>l|Dbj(`7ngR<_O-YYA^)XHpl<`u;ixmKcKbev
z;Io|5*KzymofezC^q1LnqVZZS3!i>mvh7EBXTJ5%Me*_5U#xxGs_OPz%kD{OQQD*K
z(EFc))YpsHKW|<Cw({)$_5YS!KX|}X#;opYcha?^S_ZRxo&<e8=)vs7JIOL>Ys4D8
z<((&3%s0)DN=TU)5v&r?9+@KK=v(}#E!1v#U;ZTCW4r3yCO(;`sy$am!P{*v+pFqT
zvK`M@eCD<(7jZV_FG_zjZ%)Ow5|4MXlPf+>zqN74>HoW)p8BNE^Ze<(*~(U&f2Z+%
zNRa;UVC~L`MSFi$TjjLRy0m}gWzUKYe?n#NK6L%Pq0emNmRAnH*1i6{gKxF3;o9Gw
zb2Y6lZfWFyTaa^7y>(vjNjWB&>Wg=Gu6w6f6qcIryu$s#7h&zoS#F`sHBVPg-D~?}
z$HO~bvWbT;PF;NcOa7k6r((m8Y}q|ASm&$eHr_d@l^sEei`AnxIrfU&l63m+SS|d1
z&gPx#o?W_b9W^E3%(DyoA3iKvq-GlTZ-Tpbk7e9P)qbv^DK@gJPA-_ds5JTh*8?Iy
zzwUe2Sns$o?z^)5oA2Ih{m$=g4pYCsbN=bnMS@k|Vs)-uNSWBJ{-AN1(uY-dkA=!K
zi)SA?u6^#H_FBbk4&jom##&w$(o>p$xcMEuydm~?j?JIQuGh2NpKL5*o%f{sfZzs^
z<;8nvZ*sQ1`!?6*%&LZG3AF*a{i}3NuQ=<gP}elkpd(7oQu0lyrS+|g7ZQG!{5o~`
zrK$U|SNwLbmnlZBR$SNhl)vW653da#GUC~_K@sK(A0xuUKfXS&?-P6TfA`SU`_=dS
zVGz9dBVRqv)&FE^SiSxacH#ORzcwG5b>-T=-%j7$_6nU`A$oWI>y_L)PKurBxcj>E
zNUq^-+iODoTW;m7{V1{en8oH>FD40YiFMiPF0<`%@3~3r)6G6jvsd!mlCQMn{~vE}
z+5H<DmMq(+c|Pl(rl-)9^E#QE!_RI`k2u6~chUU1ma8vv9rgxwoes&b{<7_O*%tZ#
zMT@QG&7F07Q}y!`C-=5neQ4*|y-2D5-nSEdyZ613%u{}+k@EJ{v~SOB*-PIk2<?+J
zxIejj%eEt7{?f+{e_jjFE{^$qA;9y!Y^<&0n&eFvEt8V;_8+MEu=h*K7G595eC>02
zm##ABe!nccGwecF_wf?7pEGWy#@H$>FD-lYSoAcj>fM!lToQg5xE4Oxwa=AlkyL(x
z;{$umUDA;(3Huhl?9@`&%U!}_e#_fNp#1%<oyo;{D%LmnnLWcN)wnINd%?f@_lZ2d
z(5(qes&?!>kYe+`@W#_v>p+`lM?PMaXOh3VyndD4w9uQEen0y1T4hnzal5ts+38Kk
zFYkT2`ABkMzNhfZlPMn!CacN^8~o|cetw|wU8_0!Nf{M2^|s$GY8D#r@^(J%oA+eh
zi)j;9?EG@5nlXA~j_}9&q-T@&+*O_ZbMm6wbtmTE{j=}ju?1I+QZC=EWKR3P?%6uN
zo4P-A^iJwUu^Q(;5;|ZoL%lsOxGr<)eTS~axeL-KpI_JJoG0ERe<EM{_Nr^FvC20W
zHdam6U<;OgyQpEx2I<eHFQ)Ddvz26Kx#efG^Q*o>Wv{$D(;<$FJ1$P&S^1Y=C08-t
zMXOF(bF$5%vM(>~&K>aSjEeW#75&@j()BKRpI`Ovk5~0_iq+0#_Wi0K`FOVHt^7r=
zen}QcF}oEBJKb4ZP|KlskL`M9ON&B`_`@pum<fI?JC*806elxX-!3r8LD<Z`bwa9;
z;%mMgc1;uhn6>1rcQ~PU;45E?OhEf*4ySePf<?}<a`I01RGjSCW8&R47)n{<`U7P5
za68p}kmk5r&ua3@-f2alz(Ro|t^b4-RoSk8=5V!ZpYY4AWzlVcET$c^AH;s%EzmXB
zNu!Qq;k3FP3bC*4m9KpJ&9T+KCE$3qfLDWrC(|F#6~<pPTdWxGty359Vt#Yiq3zv$
zg(-#yS$BMkb6mlH>Sg=$f8Ui%HZ)v#u$Y6Fv365fOWVWu90$y~zUFW#ZuzO#vh9si
z#%m#`<OzAw4>rr5y7^JNrHpaT?)i=x=R;e*)iErr4R~I+;`4!K4q28T{7%1|9lD)A
z{PSSY`%!&*wZN{g3{LLu0#?_$FXS?Of9tT^x}mH*&+WyJu$FhTTDf0Yd{l1v^j*2+
zRbI=nh5tBzL|2F{yxj0!a>3~*b2)tf@;a5*b1mE)aNT5z;a_=<vkd!+s|9AYe|ldj
z>BK#u?4dP>E)!4pgfh-s^_-_H*(be_c~HstVb6`u3#tol?`=3-yWaH$<AS5j4Ck8<
zaO*c3$u*f(WJoTY&a_v)t*oBqL)DY(9JarNTjZGIDkUHMx47`BGnpf{ZA<t!#V<uG
z{)=u`{^IpuVz{HUJ?95o)-SbRx0qFTT{u;HqB-mDzZpL3oV3IWJzUgO6pc42X!m?C
zd*a~tgz4o=$G9iXbs`R>9X0aH7$@pitzN--?3jc{yO_(H1!WT9LK>Yx2D?fYni#n7
zpK7>!q5aoI1}PV3jqbLKVJ2*sQmk(^mMx0SFu%pTsZ#z{Ki`7p7Xns2eG8&5tbL(j
z)t2YHeX;e0xi2iNT8}N9E_xxegw1!6W{J5~_p*yTvs{Za<nA_$T~La0dVZm@gfZ4-
z`h~qO>`Lsb7;HP@n&h1HUHcauzwr2lQ5Dx-sl8%*`SuF#<@~Ex#r>=HOV}6o7yK7*
zU!1?-`=aj)zc03Tm3NSLk#~}JlXt9d`zKS&^H=`M_80Fj++Xa!`22$B7t$|2zwrE`
z_yzHc(=Uc!2)`Km<@}5I3*Rq%zqo%<y|cZ;f4={9Ep<H$)=tpVE)c#j{g0G@rTH8i
zWgmO<mHX9S&R7-JIQ_|rk~jh1Ns_aix80QWn?5^dCeIbt*O{5U3pWS_F7`dUvBEn$
zy4qc7=Bqbe<=<1Pwi^BV#m?5g>9F$RoxZcT=g8jPR(t$h?C$Sx?r#6jP+R-q!b9cl
zcdEYJ$lZRw@cGBxesj*umzu9J$IjTY^!v@1GjHy!e2~$*?o0VQ8&3TOncXZ`SRdK_
z5ORwAl`LQP<l^zTZ$E^muix`W^u^TmRhJr$2d}UDWqSV4x$OAwzswoV-v6^Zf8~)$
z$qVwC+BFVT+g&(xyTRRU;nnv`b3eIX;F5R#-x~HItw!ScN7p|Jzcr68zM|RNwd&T@
zgwM;v!xZ*2)!bnGX|~X2ONQb7_yzF~eSZkpasHHRsL4}ZU=s3V_dj{Ih|c>*HGh2P
z`6c~;{ja>-5$g|nd)VJM*RwykuUNx!f7Rc`a}FrVafbdq{jJGXr{~1<cOfEs7tS+d
zGC9c@YPMMS^%pC#Dvp<q;tS5lw8VWD`eINeRdq^vi~YQ2xt}~=PKGbOzTo=e`wRCz
ze|y3Ce#<?Ey)Ob^tdyv_8+~E+LVH=)_b%_*>UQ$}diVB1{`-b`zZrIM&vj8>km~2i
zFSoq*hwO#!<qPER^Va^*`EoJx#mOb|`?;gug}(T{fLGpGf3fXn&j-!h8O?3_<~iuQ
zuWyj6VYT=q^x>}d^XCm~+8MGh>Gb|8WntUwzfkO(MpmL-U(|y1){na%zL!_#pVn~f
z`UGixrm6L<HfJ^$>=nMZSL)I~!<c%e>%SjX$*kLcfc5>1dWW-T<Qd+scf6PSB<&tU
zYIE3;8qsyy1+|>(`VW3(iCKT;(Z|;hnyhB#ANm^RQP2P9uVl>pme>CeeO26H$9%my
zu;b^_dH-Kec(?!Id+~?AoOj5zy<V!p7tUSGv99{ztoEYwhwuM>P{nriWA29<u6OdS
z`?ov&`+mtrm*-%uRI6gGL_NnpxrY7z2kx66W-Zv1-~4^~#8Wm)ma6^;n6vK129d<S
z5u9yTuPgG(J!bu&ack?R+CT56=1)&px$oYZ!$EuQHGOMptCZ@`v?wrId*At`!#(D;
z4E*fL>~re5GVif}m2driLF~f4vn<nu_!!K8&o7u*zQlH3^XaCT9{F2yo#PJii=1ya
zvAS#{ul-+DJa_+fM*Y2cPc4i5Ge3GV`*vNQzqY1#<3ZP3f*C%y->f(%?9O=1{>tl_
zY*Ea*Hw<>q@7}ogjNUVg-?G<w>W}X0{i*khiO)dq+f*N++M3kVr|Q~;HEi|8PjA1t
ztbK4QQ(f6pMwy70hnvs*IMwcOZVyw%cKN;B`R?}}&M&e*%4x&0SC6+gi~Y;d^o9HP
zaHkxzy&!GR^NIC~-7%h`cJ~=S&6sD2RjvIdUeEkB;doV3-sLZsluqz1{dqa&HP;KH
z1Xg?h_z5xZr>h8ViA_3bD8p{vqp+drU$R8UwK^Sj4z;&pi&PqG9GL5L^f#<IFzbZv
zac8$8^{kmcKJ}+Y`t82#mEgOCrSY+)u!w_^hnkq&LZ^9kKbXBzyAwVft+(N4n0;2w
zW!W(wr30spWR$t)cw~!c?os-w-yx&Um2m2XeX$^~-+~WWTPLd*xqt3Bsv;6S?LqXf
znGq*_xSv*bPH3N^QK;nqKr|z6MgQY3rz&_QJ~3D5ig7-8>f)Kg7&k3haqYBoDncec
zN{p9}6siRpz5J-gy;nFRNMriqt)E!}ji>Nb9udlOT3Ob+Wedx-oP`rOY?eItVC$Ya
zY1Y+K>l>yE-EmpT@F8k#8q@ldIZ2+o3)h@HGMi({hvbEKPsmO==)5FSX`(>trdcHw
z7EPTeGPia&2=8PTQ|>(SiC^n^snP`@i|(m^Sl?}MV4QF;W$OpE#zWkZipCFRCx2V~
zcGX1Q2UAnIn;xljyF7dv*?B-aa0f$NgP@nO=Y=A*ZkM_bIwwVnCN6TD)3)YI+!Iru
zhNBrylUH_FG+qyByM87zHEM-M1m~W#%_`9irVAWTMxW$4rus+at$X6s48Ml!Y04iy
zvBWi=ao98=%sYKr!~^45(MF$}A~#*K{&YU=8sj5PHJeuXU$ayq8GTPU8+)p*@N~Z{
z-Tp^sPL8sxqx9(jhLZ}>m76&}yfO_r;LYLZ;qZ)S;@v#e53|;$y$~!oJo9YROb1T;
zKI7;UvYJv#sob3{8k^@H5%l^ziBJDe!|?@iOnt6OolAm>Jw)PqIkvFGZJWIR!X={#
zhf+4q$la8nwnug1nh7&M?27yy7-70eaN)BFoO{G>9um}4C=lB`?}F2VtxkC-@)-S?
zD^Bt+(kbKG=@R(3(%An+u9#}YCdZu@R5P45?*xTLOxyKSw`G<DWF&i>cV_Fps}{j=
zf5LXL6^#e7FV;<Q)Y-C>F>d+gwgo2LXPtgq9mrPXT<EsCwYko*Xtr)x;FhSIfWB6n
z=#%a{Uid4_n8E6$JAqfssdDqwHM>P0UY*pKaG}$o&M3cp(xL@R-v%(8e6WH0fY+1m
zpe<~-8cQowgC<QCn0w%L(M`@hVY@{iJY|eq9lGU`)Y`BUdeORH7%k3bO<(&&Jvvpr
zQ`v&EMM&(R<2y&KXdS+eHa(usHBq@DPE6~U@~rnQ*ukO55XasaK5eaEd&X3bJu2EM
zY9FGu&UZ=JIw@@{!=5LTn;6#TOs~A8@8Y-Z73V_k4`I?z+TLu+n6#E}HjjPBq*#$6
z4TmietOvC$PVPO)BsteXW;U;nMWnM|^Q<1hTe7{IIsMEvMWUoGIp4mOGJWzUBmaAo
z&alKq{tjfDnCZ>DN3Os5(CZ5ipQhe)Sujaaod543H-~Mv8ZM`(H%_0DRy^6lsZ!8D
zaP6Y*JIs4FO})UoYXf7PkbvW|gWif8xb^roxgL~8rZ5_v$eVsLZP^E=r*9aT|8#vg
z)xstgBzNHTS1Z9S)6Qltu8n<?Yc`ozfK7S&q3r1=oA0XJOr3t$t*&hLUFSz0yE|Ki
zHhHXIeamLLLvhlRjh96>@tI1xl%y%yx4-yys9L%}ZHc&I^I7KyTOC@#L3C@2{1@qr
zkXx4+)}Mc_^x;-)Swr{(-#vGisc*Sm@#6Rd=}os)Pv#X{zFDHfvA2vjE`v+Dae9C^
zTScx}(er`^M}CLw50e@WdDm}AS^nbVMJL@fevyPyi#KuGPv$XJFmkh>^hxQA{ViD0
z_+*J&!}a&&!4-GQCQNYIUv$IMsX=JhnuKyIjuU3>Dwc2Zmd`z6^_EY*vw0C`mIM1m
z?{{a79<Y2ptx%*e@tTAgm!hPGNmVdM%%bc5T}<b`oGKGEx=|~4((zYQZ%S#{fo#<c
zRw+Sp2c$W62o_8~{8*sljv*&s`xAziw|wf2*I9QyJg7NyZdw0j$2!0E%kLFf9T@|b
zolG$o%({8z$w_Wr1|@bO%PqHZtoEeMooP~0%h=Ag{(PFbBU}4@ZOb(b`jxK>7<oPv
zXV|}ZUX~E<boh}E!=xmR$L<H2%xBJHm%h8}lSM>V*oC{d75}hrxUg1xe$)1AQ>Qn#
z<;DJ#-f+p&hwaew19scj{bATr_WG3hK{56G@0SYfdpVUWpT3?D9`{px!>^}PR|hol
z?0h7!PxjPA#Z>;@%!P?;hx```wESmLJ*~Z;Av-?qr>29_`WoIW#qWVpX-tRY9b%U}
zQE9o~BIW60zR7XsTY);>4I9?hh!;)#ZPFso#&PmCPfI+LPp&q<Bd5wT;X}(8xJggh
zFMlZC$@1XT^A47d8uq*o?Hy(*99Zo1BT_?=qrmJ{{%gghTo0cs)c!rSU149{)9MNP
z>Yl1k*fcL_P2JOOhsI-5_qWb)+t6sG@JI2&azO<j=O5w@lifVlJN;k@Quw2jaEn9L
z-04TD#=^p<$rs{!wdb>UnL6pt71+mLdeJB~%E`JV-etOCjdeso?}eu6fgBlfPKhg)
z3Le_u9Cb+he=na?g{W-P2g@%8yz&bTZ3XHiy_8?-U;ZM=QQtLhQU3g84@H-(S#uzo
z^<JEt)`4OU*G%2OhR=x{o^e9T&Oh`6jCR^{JS-02+nL|;;c|e~&iy^J7O+Sj4ZFwl
z-LT=0QP*Xo+lsYPhxD0v-a5=|S5(=$<?^T7nU-%IKD)oz%kyyi0<oh1BEG`yA8t1Y
zYCg=As+TNfcsD(;>6tG>Ybpol)JX!uZY!tCJO9)?p}p8o<E=o8eB&qUg`!akHS$yL
z@B9>|G5?79r5WvtHT(q$&rjJq>CQbMs=I=FLw3uD^$K=-KNV(7H#xAt;YX^5BJWH2
zwhwCqysMrH2P8{vsOUPj-QkDbiP>>KnOB(a{bbe9*y4YAyW$mJ&C9-@%bf2u3+!VL
zDhpn5qE@+OyTjhQ2a*dPNQu3AwM?~q{T}X9FWF9{UfKLf?ZB?|r_B@gv2Vz0|KJ@^
z#CGqP;vaJjOXi)o6#pnq@qaYG>4UyvZQ4_rDf1ieU156odO^*nu*MIo84}qFDrX&w
zaof9P|M#_fcz0~%uWICasLrs=?!@ecicKFrUl9MbV6AoLEH{%4pG3FtAIf()!|-%I
z<0;GLDcx)fOXU`R@BOlmotN>6B!@lQQ~tGk<R8c{+FQh-v4Q{4`-Q#&b=<FHnSKbi
zESaYG;HtXfAC)UcE=y%jZe>ng_&&bvL-hnTZbN@(j~@jhFBUlc@Sjj8Qsu(t6!c7E
z$^FJ{7h<9nuk&&Im-M>LRrxrB$1AVp!)u1a5=;D@e<XTXvVADOwB4xXyt8HU0n6VR
zv(z(d*qoX!N_@#<QEaTVp0aq6tiyaij(Wz@w+tS7&OdB564?tf)o*crEpzyxn=w6h
zir4N2X?`b<j2+w>A541v7vJ5I{<dwGK+Au|wVp|34nKl5a?Ot>nNPXK`S3Ht|GWoX
zD!%tQ9^PjvdlJ_=;mZ!&1C86Af8>_P96Il8S<SHOw7{gW+s;pUJTG%R%)Xc_V8{Q~
zBWYgS2iq5ZTf~#*HGL?Zkf#6Pd7;#1=0o+3d#V-BI&$3S+Iiw{D#v;!OBG-BmitY&
zPX4_tP{+c%uvWmBO;OVH+eM*o&Nb#O@oam(9lq@=P{(`frvJnH{8K!gf7COs|9fG2
zD93)T32)dv`W$~~YoyyJOmF$He&M~pB6UG&>Mj3SHoRT_x5WOfDLdo+JPuE7`6XBE
zIG&~}{Lwoi&vbG^+k`!;iPpTvCU4x1FPLWg!t9jj_Je=p{{Abo>bt{f^4{$@<0l>~
z#<dgLCLEd1Ff09nC&z9@m5=J0;!hh7Uly1YQ~B$g$eraERi@nGefa!gUDq@NR>RB^
z^D9%`E`NDzQsByVcFi9ljk3O=_l{O!c0LblW+b^BD&z2!ubi>0l0{=e+k~EH{Q+;}
z7ViBm=&_SMw(++i$A31r=h|D|u4CB0QsAG=lH;NeKJfHrwtR>UxFsrad9q2k_1UfR
zPKO?bGjwaT{AZbUK=c;-dIsM^Tjq#y+~*Z~$+e}3XD?Gh=AuOFs;)ynMI0`1DlR-9
zyk+UUMNjR0qn~6-eKu+RpLt5ue|=5(<>t*3*Z$rvv-9nN8AdxzjMvKSNc}u-ZnTK$
zL@(L5_a=Ge+HMN9-1hjyJzKBvO~IC%Cd`kW6E$nr?6dyMgOY!TxXz7<nkDbER4-=7
zggqDL&WW~|Y*nODI&sQXSEJY4J5`;}Jvnlz#fqt{-O~H@9t&gJ-4-#6m#k}SWL(>j
z^vS7?>D?yd)}?!#E~#^UpEKLaeofxY*|Az@^i=y|X6dguwd0$~8a+`xy`>)i6jN)A
z_HT%YklOe3#5R}xtCn~edIx)7Zk@QcSUhd{^H0L3mVKOg$n3-_8Lc^%=5yl%UN}45
z+!mmb@%`wMn^UGuWpZttdG@?W+ttoPkA57wV&iz3;pi>VIcogNJwGmgE;eOx@Mhs+
zaj~g?-n48ox1Sf%CpP8epPx-kJLXvjH!v+<&i!~vLF&6Llj3JsvvdBwBs+KRlGJme
z9G^@+|ND7Jr}jekyx6EW3``=&&Yznb9hCM=jEifEqk`px`RDq@Cp|7Ue&wO6mCij~
z?~JaGj?{-0Z2OZ%on%)V%(+k)KR3*I-=BGhcJ1miU8H_(Q6}$#tngUnT|7Q3e+aQ!
z_iXoec&qA_@XLbzi<!+iF};xVg>S7V%dhX-wZL@ev3u9=GF;uNBh|frQI6{z!-tC$
zm-6{--0*Vc&0=wFuXSDykF|mr`$Y9mpMU)G<;#`E+&_Pw^j4iVXZCEpd*Y2J=Fieo
zH0C^0lgPN_*qfP2SMGdi37TPRY`fJWvS-%(InlGC=ggU{@22B7cjj6BxzQR^gD=KB
z`FZKj95zqW>-P)T`vfc*`9Ck^W3ByClGxZVuaWPTWW37UvfB$c8qR&?U7V`d{QKmg
zFMBqbaEi=kvO0VA(h~;02SE`GGY$3`Y~SG_JZI)S>(Br$@#o@P-Q7!SI_5+L924jI
zxpd{{VDIS*rcP6vI<xrk<f!=hGpzVs4|Hec9Dn{yU)N{u%ow8wUPs=)<Z{W-%XqT+
zlv83=1gn?6PH(f!OeuAjSuq-O<idiNe|~<lScu7Yht@G2@9E39yC=6Yg}B^u6hHI0
zyL;)Mg88#f`k$S*AanAUt3HWITKqd1Hy>J~w`uD$!)Zn3|7zm8uU1W5G^f4IvQVwQ
z-)PO>o&W2e_C0of!dvNcZhrKfoSn*bOm5A-j~^?zOn)Tn^=F!IXUThq{CKOmekDiy
zx3BZ#cfRrCCi73@*3ZGM+)-SUmOcHlGg50xbkx+}9xZGKX4-_<8=BYUv7MN;q&HY%
zw%&y0({`}s?<wV&q^g<Dr~OeY=h2tcx{sIMSnN^Nu~{;eZ(}#N$~slQwgX2G?l8((
zkyVvv<A1>OoW8^U2194b&@U}3uRMBG5+iwK-qwv~_f~Du{`h3(&Yi}WTV3SZo_l)-
zKki=gFgW1y-R_SIje@$}=A09mR?;G79&a_telvf?s-9c2bC0)AIr^lgj#22%?AaPB
z%hSZUxxHs|D}3kDP5*Sk-&D!|!mULYgUr0Qyj0^_{`vW`=?fY^aX+4{e(w45^MXt(
z+h1-LI(_~O|J1fepI+>l;iGnS;nkyam^7mzEEuOSG;$wxQTLlzud;GlK@s1t<F8U4
z&wUbac{9_YW~1Atk51+9UDkGanj3{x&-$Z1HT>(g?;-W+;rkg^{8<#HyvKI#{5YKq
z(F`UF%jFOG)@#k+7rJmq>ghIL(Pipc+iyJcwpNdN^j(1OO4Rm0lT$V;RtLTAm%0{R
zl94@WnuFfjbCz*)m-m>T-6*z5)b;GyX+a?mv^_6fdU7QxS0Q}6_}UGd-1f#he!22e
zENa@hyu(|6ZirZ$;<I|is|kwRgg$E3EM@ojaUhGw-*<lW4m+0XIjy%`mRe|qdaTOI
z?a5NwZ#F4=>$_Pu&YhVXtuvu6oI90inINldyO&4yq?FR|dmIu5Gxn|sSaa7v)k1E8
zPu!YdOLKe1$IPu>>;Jo1Z`SkRw3ZIMD-nMEMBtNL-h|AB3|&`qR^E2VF<aVMbNIy?
z-Y5f)g9ql#Y8N!NpLoq(qQcm4DStpp$HjZg-o^PehE=_qG>yYewlt-5r}C~9S%xaC
z45bq@dX^~Htu`yVpDt2x>$mXFPT|LwgB^A`y2_qce|CPZ*OJ{<>_*IgC4V%^ygR=_
z_E=48<f7^G6fVr0p*Kh0$62tuc?wU}CiW8&X)TsZrB@eJXXQPbRK~t)hLI`btkZY?
za4srpJQf_GP}Ln$6)Nc4$!F?u(?e<D1fhk;b@ZN!AJFsdx#0RK$nEV~D<`Q<B9^x0
zX?#CEEaDQAUtm)wW@1yz!MJ$o(FGhKnYAk`*aIgn^FJQ#8SPMM$`yBN{>4|5#h&Rs
zU9PQr<;_ag3pY=dv7OzwYr)MwKZOq*eg36s)6Yd-EBfBdn5~%-{PUH#!yXo0!AUZj
ztNxl^YLpH0+U~#oB+p9@SMScnuQ)<g`Wif)E@ymMtMR_%pPWYK?PbkdwPwf9u$nKw
zf69#c%v$kYD^HzZ)OE9TNY0oUTA2~tudjMekYjrW|0&(5ms6XIcyIOkKNlBoj9s?y
zyj0YRn&byF4!vPH=x+4s=F8yD)}1?-23<>1*M7Y@L7-`)i7DeH$Lp)!tEJt&8^XP2
z?LXb1z5GGK4=n?A7cW>5<~mtHr8h2MX&#T4;vNq_!ANt{g{qgo#tNu8Y>Z6U@FerW
zrQqV?^vRoBmxd&MJM!)9%A=i|70efj`_G>>Q|?_?;N<kYvL7{8H`h)|Vt8k|HBff_
zHO0@&x~`1eW{vAkABx?$@5tTm0RKH(OIKXEdN$4LCG(QU8Yw;uJn5VlI}fgonW59b
zI&D6Cs*DB8hb1*%nbLf=h6Y~R|NOaGO30b>Ym-9`oaS2GB*r&Us@Hp(Q=Ocfd8~|Z
zA%jp=-LE^3?&LJyVlh9cGGqQ?HOGre2I0XoIO1D7f|?n03XP|pd9pO>oz<~CCl<ZA
z28$1E*i<cfS$9sulnd(1($2kLzHx;|Wbcec`VQab$9dhet#=FkbU8t1wZRV`9bdlH
zdlOEGb*S2|NxKqseNMDOQp`fDLmR9Nw_4cG-EXqu43l7|<?8Z1TOBU?7BOt|k_b_|
ze7sQMVNaB<*_SI%WFsznPcC*?X?%q1eDFP{H(TzU{CTo6v{5nX<DHc=H60c?$xB8p
zj*0qq<;@>UW#!es?i{k!$`$_+%ra$}P2sF5D~0Yceod-yHk%tHwfj-3pIpc)lMfaF
zCCLd{9%77FUzT$zmCV>OBgi=Gq$1<o<`upVMHD~2DzD;Rf4gq7`m)3=;qFPBmy11b
zH=CqvZ)-nOedjs7bypvCfBt!M;#uxuaWQVTBtEC<O8QyPFSakO=<1u?89ZHR%c5X~
zMDwMOlxJu0c<aw8Hhyxa#@TYa!#Q2^DaG1P?o>Hj9#?pNgZJqjHp}gI1J1pjQ2d+s
z=^aMP@;d?N=1wSn&inKZt7ZA!fOB^z6o2P^dWYHa`<;MudnXjX=Y4vI-SYe0fOCH*
z6#wUaYQtd3e<$Ev^Mub2c%Ry^Sn}TuICpr$=MTJ3ZI~?k?*yESR(P&!KIL=RiOSc!
zPi?p?`|kyuOP=ug2Jh27yhZoQoGtU)=WM?pa8Bb%&$Tm=CIZ*CScRolEPS}usyR)3
zV$7~}E>~9_Y`phOu~kEq;pvr`Hyw9{F-qmsB_(BjJCkt!&(TF$K}IilvMeN)@A_4n
z`EE{;qWx}{>HfENPYHfLZQ22s*jd`EUb#$N<EhW=vE0!(@{;3)+|U)D&n#Fcs@)y@
z-AQXxT4CO@9cwmjEA`C?Kec;W(8jJe37dWW=dwN(+_Jf5QOxnAjguBdx~$Sz|NhlW
z$Debo*-y=F`^UNK%fkIyubSH~iK{<9P}6qDrJBuop$P9QYX_%ghpyb(qA9&<(iXEw
z^JAV@FRoVBn({rkxZ##h?%&wz^5D=PO3z$%&z-Pmvy)@rx^IKQk~2@l&-t8HJFr3G
z4SSrPU3SAIjnXG-zN<3TL=7|3xW)aRsGDSSz1cWvQslXnSwBKAKDhGb&X35yQ;#_d
zbLIx+R-Comdg@B|<>Cq3mT#V26SKuS_V26}S1LUw*l#y+S73i|YO~0**K=5E793ij
zvrX7z-XvDe*k(W3_$8MMMT27x7#<a8arm{1#iLSa`j;8g!Zw%8-_}xG=zF??ThVZ9
z@70qlJ39~MOy_!V(I?nB?UDEeBi5PL?Nc;WSI7AHiKm^{@jVn=U0rDQ;E`<b@vkS-
z_c|V6yEH51#Gi)9ss@8Z!6QNT3vcN7y)@e(=&JjPWv2e6^g}C-F34dt{&42bp^(Il
zHXB3cW-i;XgF|L}U#RW6sYRT}np`){THiNCrC#&r&P?BwkZy(*1yxx~ik^6haQl?z
z=PqwJ^k8X8{?!Jx<_I&^nb%C%&BMMeyIFYX+*7VE9pbyiCInnQ=5^w8cg%68e^;*T
zSjhM5PttY6V$H&el?*0ENy#0x6JJNqIdxu2OQ~N!k9YR+X=$H6cQOZybF=mIY}7BR
z<yKh5BRcW8`1Us4RQWkYwrwZGcW-N5x^rh_vaiV_*L6z`_NF|^ef!gOU2+j4%XOc+
zivhlduUyw<<}G8A{Pbhdbp!vlU0q%oXS!p~?`+Lfo(?u^Eo;E}j`P6_V|*>&wZ6I}
z`s1Z$?u4K=@8rV3%Yj|1_%42H3rWp&Nbs<p8gEqKZn#XU<I=W!;%n1por^H+{Sm*S
zJTotGr9oieX0g)Cui3utSd{duW^=aeY5lMgskcAVuJ6+Iw-GtDHS4_j{%bcsdLQ$8
z_;J~F%dH77jy-vqc6~=}Ugjl@?5)pd$Gvv?_vPivrJBxrtInKVzcavMwuOm#a>#6+
ziRQci$u3Ag6RRP@vvvBSyt=!p{z=JiQ@=l&9czChG)3-Az)rd1%h~gyBW4(%&;?ss
zyk~-ijdl6Ln0&)$%dYR4GT-c57FZ&^`ygLy`|P;;cV_N9681_qZ^d?*%FVCuN6(m{
zHOG9`+M_dO$A3P$vT!lWW#zo8y~V<<tjSxy{CsxZ%6hxSe0hBd?Q4tks_s4)>y_=a
zyZ6TGyTAXI{><Cfr%aufEOP&sf2F>nrYJ9O*QP4ncgwEV<*h85a6vh*Y;QZ4H+S*V
zsF#m@*B9sg$$9kQ(j39NMR(4w|9j_6%f*fhAj_VMX)j;<rDJyN`-gwtB>jGP=+Kpy
zMORh)78aWC{Z<%I&MT#F)7(~nIeV?s_j=~wO|K`dnBwNWQKo!z_I^|Q*cq{_CMYaj
zF+1+N2+u#^Hn(dad)>L03%yK9-x+87=-GAeH~%A(_nIV~t=g=)=}@38_ts-viJ6*P
zUufqP)Lfo6v+Y!0>8$WySKdUHJ~7x6Z075q{dM}Z#OuWp)53Q0NsG@rY2}}oR`>D}
z=cZemBWCD7yRwaWO&qV5UT^k8t8CX4#!t`I<X&3pvuTf~_`6TS>SA0IikCUC{wZBw
z?00sOqJ`u2Yi%a`Ti%^J_p{siR@TP^{mKf#YmS%a#@fdIt)1}7HSUdxS$m`Es$22-
z=eu$_8k!65ZZ;{7s?)hY_3tUg$DN&rg6gtvZJ4!6)L*DqX6Bjm^Oo94rYsHq-l%T&
zW9QKnL9yhQ#{&LOp8G~B@Y?<D^L-s>2?bc$a2JbT5v=w6d}+a^u>0#~{J$irYG+$*
zW*1~D>ULLi>Tl7z8-tkdUs(QlRys?f%iQu`*0b9msUCQgdSF386SJf)oA>RXysTl%
z@0mE>sQmwC!KV6}%y%L^dN1ekvF=!rJ=Z7tEAP?fQ~RzgE4}n~rj-7fK4yjOXXddk
zt}U~*$eK6H;oAwOidb7)=Gs%2MD9v0&^f2M{4;k|q=`yTVK6JB$ok6Yo(xe@b1Usz
zyG>ad_XK)gxO_W*3X1~Alh>13igW`n+`fGM_I>jSFW+c)F+G-B@micMRC;nrlgF)p
zPm{up`&``4Pg&>ps@-W@Ux0nl+J5~rC8Dw`zP2x_|M}<6qKgOTd|UI#?R&_?$XS<X
zmzH^EGA&~9{91NpQN_%2^R;}!e@S1ATk7ofj@|h4tTmTZW?p<6E&Z%p;JlA-#=Vzc
z+`j+PYr3|Md0Bq4`JWXm-hmFQT&`Ft?|k<<dCJ16J2bMb*+eHwt;*~3a$gw4J(Y3o
z-8ozK+!NQ)kJUMULP6w1>7Ex~)t#<B+Us}Y>(i2xS67`-c6I-~YLzxyTT;wMT`f1m
zWFgP+Ad~-pvUW+_@KC>)u<OX1$(gBJb8Z{WjAyvUmbly8-ku}bC#t=(FUM{D=9?d1
z77Mo?oswt~qjFovEt5s{)~j<4wU<uhwY}=P8shm@>zin7!>MOR&u*Rm`o(tF><w9G
z%<o9ATys}j^ydnet7Z3H{Li1&eYtM((!{Xxjm9~NCV3W%CAI$cZP~Hu-VEK>>Q8GM
zRNuSj&Q2%{Ur_Y_)^DcOn?l|Q#Y{cF!GD8R{Lys>llHBuZ4QlGRVH{ZK3RU@HT#UI
ztFvr;?n{2lxzaM@oy(ea5%;HabcS=D-&(F2HZ3subW-WnW47I)JFnk8xMGb~=0XGe
z?F>=j9|b0bK9ya>IYsv<OG3~&-(c1SJN|DvR(9#K`U^dk-e>h+^HvF{c6u(6QA*Wu
z>W|XVnWOKcayljQxbOB^CyI|5Prk;OoV17I__dX;8q6jN0lWIdru=bowC?K6*z>uy
zb*1H^t3LkmLDSSeFJNF?aKw9N(cGfMqO6)A#m%>ub1l7`#PuXZX+_OhzD-gdL79nM
zr(y$E{C~%~V+(WDiMdW!Kg?_@>a%io&P~ZT$jE*!cI@OwI|D6UF%zk2j>`WoA36QY
z>EV?}S0=ShFB9<nC!@i{-CfxFrcy^oC-zl9*}Sl{=U&TiPWbgDNK~WI!qsUuQ&bDn
zN7n<THuZe>VihdTADh0#dBd_a)Aj(5TMI8{C9T`w5qDmM;fdy?$q7?bwg$dt5>a+|
z79^SAu!3d1TP{OYS>8&f>P_Mo8H9RDpY$jSEb4B3{rqka(<QFw-EUU=oV~C)cN*8_
r>62Vv=lB}Od{@18C4AH5>l#(1m%I9MR(`T8u6l9v#D8}EeeWaz(CB&;

diff --git a/lib/font/acmefontregular.ttf b/lib/font/acmefontregular.ttf
deleted file mode 100644
index d547b3febc45a4a7ab321b83180b26b7fdb2f505..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 66536
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
bK|6Voi(Y8g3RHUA=!5n@LMl1%p=WjgFTU;>

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 44f69af926..705ce933cf 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -170,7 +170,6 @@ class PassportGeneratorTransaction extends AbstractController
 
         $pdf->SetTextColor(0, 0, 0);
         $pdf->AddFont('Ubuntu-L', '', $this->projectDir . '/lib/font/ubuntul.php', true);
-        $pdf->AddFont('AcmeFont Regular', '', $this->projectDir . '/lib/font/acmefont.php', true);
 
         $x = 0.0;
         $y = 0.0;
@@ -227,9 +226,6 @@ class PassportGeneratorTransaction extends AbstractController
                 $pdf->SetXY($margins['cellMarginX'] + $x, $margins['cellMarginY'] + $y);
                 $pdf->Cell($margins['idLabelMarginX'], $margins['idLabelMarginY'], 'ID ' . $userId, 0, 0, 'R');
 
-                $pdf->SetFont('AcmeFont Regular', '', 5.3);
-                $pdf->Text(12.8 + $x, 18.6 + $y, $this->translator->trans('pass.claim'));
-
                 $pdf->useTemplate($fs_logo, $margins['logoMarginX'] + $x, $margins['logoMarginY'] + $y, 29.8);
 
                 $style = [
-- 
GitLab


From 406a52f2e416fbe511a02d11061bd74e811e264d Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:26:57 +0200
Subject: [PATCH 012/121] simplify

---
 .../PassportGeneratorTransaction.php          | 20 ++++++++-----------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 705ce933cf..edd5717369 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -186,6 +186,12 @@ class PassportGeneratorTransaction extends AbstractController
                 $pdf->SetTextColor(0, 0, 0);
 
                 ++$card;
+                $cardOnPage = ($card - 1) % 8;
+                $column = $cardOnPage % 2;
+                $row = floor($cardOnPage / 2);
+
+                $x = $column * 95;
+                $y = $row * 65;
 
                 $backgroundFile = $this->projectDir . '/img/pass_bg' . ($cutMarkers ? '' : '_cut') . '.png';
 
@@ -237,25 +243,15 @@ class PassportGeneratorTransaction extends AbstractController
                     'module_height' => 1 // height of a single module in points
                 ];
 
-                $baseUrl = getenv('FS_ENV') === 'dev'? 'https://foodsharing.de' : BASE_URL;
+                $baseUrl = getenv('FS_ENV') === 'dev' ? 'https://foodsharing.de' : BASE_URL;
                 $profileUrl = $baseUrl . '/user/' . $userId . '/profile';
 
                 $pdf->write2DBarcode($profileUrl, 'QRCODE,H', $margins['qrCodeMarginX'] + $x, $margins['qrCodeMarginY'] + $y, 20, 20, $style, 'N', true);
 
                 $this->addUserPhotoToPdf($pdf, $userId, $x, $y, $margins);
 
-                if ($x == 0) {
-                    $x += 95;
-                } else {
-                    $y += 65;
-                    $x = 0;
-                }
-
-                if ($card == 8) {
-                    $card = 0;
+                if ($cardOnPage == 7) {
                     $pdf->AddPage();
-                    $x = 0;
-                    $y = 0;
                 }
 
                 $pdfGeneratedUser[] = $user['id'];
-- 
GitLab


From 8f9e56b92da21bdcca66cdf1e8b76d56f53563fc Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:38:04 +0200
Subject: [PATCH 013/121] simplify

---
 .../PassportGeneratorTransaction.php          | 55 +++++++++++--------
 1 file changed, 33 insertions(+), 22 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index edd5717369..00967873b5 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -160,6 +160,13 @@ class PassportGeneratorTransaction extends AbstractController
         $protectPDF = !$ambassadorGeneration;
         $cutMarkers = $ambassadorGeneration;
 
+        $fontFamily = 'Ubuntu-L';
+        $fontStyle = '';
+        $initialFontSize = 10;
+        $minimumFontSize = 8;
+        $maxWidth = 49;
+        $card = 0;
+
         $pdf = new Fpdi();
 
         if ($protectPDF) {
@@ -171,9 +178,6 @@ class PassportGeneratorTransaction extends AbstractController
         $pdf->SetTextColor(0, 0, 0);
         $pdf->AddFont('Ubuntu-L', '', $this->projectDir . '/lib/font/ubuntul.php', true);
 
-        $x = 0.0;
-        $y = 0.0;
-        $card = 0;
 
         end($userIds);
 
@@ -198,36 +202,43 @@ class PassportGeneratorTransaction extends AbstractController
                 $pdf->Image($backgroundFile, $margins['backgroundMarginX'] + $x, $margins['backgroundMarginY'] + $y, 83, 55);
 
                 $name = $user['name'] . ' ' . $user['nachname'];
-                $fontSize = 10;
-                $maxWidth = 49;
-                $pdf->SetFont('Ubuntu-L', '', $fontSize);
-                $maxFontSize = min($maxWidth / $pdf->GetStringWidth($name) * $fontSize, $fontSize);
-                if ($maxFontSize >= 8) {
-                    $pdf->SetFont('Ubuntu-L', '', $maxFontSize);
-                    $pdf->Text($margins['nameMarginX'] + $x, $margins['nameMarginY'] + $y - 0.2, $name);
+                $nameX = $margins['nameMarginX'] + $x;
+                $nameY = $margins['nameMarginY'] + $y - 0.2;
+
+                $fullNameWidth = $pdf->GetStringWidth($name);
+                $maxFontSize = min($maxWidth / $fullNameWidth * $initialFontSize, $initialFontSize);
+
+                if ($maxFontSize >= $minimumFontSize) {
+                    $pdf->SetFont($fontFamily, $fontStyle, $maxFontSize);
+                    $pdf->Text($nameX, $nameY, $name);
                 } else {
-                    // Require line break after first name
-                    $fontSize = min(
-                        $maxWidth / $pdf->GetStringWidth($user['name']) * $fontSize,
-                        $maxWidth / $pdf->GetStringWidth($user['nachname']) * $fontSize,
-                        8
-                    );
-                    $pdf->SetFont('Ubuntu-L', '', $fontSize);
+                    $firstNameWidth = $pdf->GetStringWidth($user['name']);
+                    $lastNameWidth = $pdf->GetStringWidth($user['nachname']);
+
+                    $firstNameFontSize = min($maxWidth / $firstNameWidth * $initialFontSize, $initialFontSize);
+                    $lastNameFontSize = min($maxWidth / $lastNameWidth * $initialFontSize, $initialFontSize);
+
+                    $fontSize = min($firstNameFontSize, $lastNameFontSize);
+                    $fontSize = max($fontSize, $minimumFontSize);
+
+                    $pdf->SetFont($fontFamily, $fontStyle, $fontSize);
+
                     $lineHeight = $pdf->getStringHeight(0, $user['name']) * 0.7;
-                    $pdf->Text($margins['nameMarginX'] + $x, $margins['nameMarginY'] + $y - 0.2, $user['name']);
-                    $pdf->Text($margins['nameMarginX'] + $x, $margins['nameMarginY'] + $y + $lineHeight - 0.2, $user['nachname']);
+
+                    $pdf->Text($nameX, $nameY, $user['name']);
+                    $pdf->Text($nameX, $nameY + $lineHeight, $user['nachname']);
                 }
 
-                $pdf->SetFont('Ubuntu-L', '', 10);
+                $pdf->SetFont($fontFamily, $fontStyle, 10);
                 $pdf->Text($margins['validDownMarginX'] + $x, $margins['validDownMarginY'] + $y, $validDates->untilFrom);
                 $pdf->Text($margins['validTillMarginX'] + $x, $margins['validTillMarginY'] + $y, $validDates->validUntil);
 
-                $pdf->SetFont('Ubuntu-L', '', 6);
+                $pdf->SetFont($fontFamily, $fontStyle, 6);
                 $pdf->Text($margins['nameLabelMarginX'] + $x, $margins['nameLabelMarginY'] + $y, 'Name');
                 $pdf->Text($margins['validDownLabelMarginX'] + $x, $margins['validDownLabelMarginY'] + $y, 'Gültig ab');
                 $pdf->Text($margins['validTillLabelMarginX'] + $x, $margins['validTillLabelMarginY'] + $y, 'Gültig bis');
 
-                $pdf->SetFont('Ubuntu-L', '', 9);
+                $pdf->SetFont($fontFamily, $fontStyle, 9);
                 $pdf->SetTextColor(255, 255, 255);
                 $pdf->SetXY($margins['cellMarginX'] + $x, $margins['cellMarginY'] + $y);
                 $pdf->Cell($margins['idLabelMarginX'], $margins['idLabelMarginY'], 'ID ' . $userId, 0, 0, 'R');
-- 
GitLab


From 6f716f5cce3205e3f11f30f5042a3f8135837f95 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:41:16 +0200
Subject: [PATCH 014/121] todo

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 00967873b5..5951c51b72 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -234,6 +234,7 @@ class PassportGeneratorTransaction extends AbstractController
                 $pdf->Text($margins['validTillMarginX'] + $x, $margins['validTillMarginY'] + $y, $validDates->validUntil);
 
                 $pdf->SetFont($fontFamily, $fontStyle, 6);
+                // ToDo: Add translation keys
                 $pdf->Text($margins['nameLabelMarginX'] + $x, $margins['nameLabelMarginY'] + $y, 'Name');
                 $pdf->Text($margins['validDownLabelMarginX'] + $x, $margins['validDownLabelMarginY'] + $y, 'Gültig ab');
                 $pdf->Text($margins['validTillLabelMarginX'] + $x, $margins['validTillLabelMarginY'] + $y, 'Gültig bis');
-- 
GitLab


From 4754df4540e627109bf16940424d988f2da6d6b9 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:46:12 +0200
Subject: [PATCH 015/121] code style

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 5951c51b72..e183aed3fb 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -178,7 +178,6 @@ class PassportGeneratorTransaction extends AbstractController
         $pdf->SetTextColor(0, 0, 0);
         $pdf->AddFont('Ubuntu-L', '', $this->projectDir . '/lib/font/ubuntul.php', true);
 
-
         end($userIds);
 
         $pdf->setSourceFile($this->projectDir . '/img/foodsharing_logo.pdf');
-- 
GitLab


From 261973650c452b23e9c939f58db4ec7ad2ed84a3 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:54:48 +0200
Subject: [PATCH 016/121] simplify

---
 .../PassportGeneratorTransaction.php                  | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index e183aed3fb..8a75dc0318 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -276,9 +276,10 @@ class PassportGeneratorTransaction extends AbstractController
         return $result;
     }
 
-    private function calculateValidDates(?DateTime $passDate, bool $isAmbassador): stdClass
+    private function calculateValidDates(int $userId, bool $isAmbassador): stdClass
     {
         $generationUntilDate = '+3 years';
+        $passDate = $this->getPassDate($userId);
         $fromDate = $isAmbassador ? new DateTime() : clone $passDate;
 
         $untilFrom = $fromDate->format('d. m. Y');
@@ -294,8 +295,7 @@ class PassportGeneratorTransaction extends AbstractController
     public function generatePassportAsUser(int $userId): string
     {
         $isAmbassador = false;
-        $passDate = $this->getPassDate($userId);
-        $validDates = $this->calculateValidDates($passDate, $isAmbassador);
+        $validDates = $this->calculateValidDates($userId, $isAmbassador);
 
         $result = $this->generatePdf([$userId], $isAmbassador, $validDates);
 
@@ -306,14 +306,13 @@ class PassportGeneratorTransaction extends AbstractController
     {
         $isAmbassador = true;
         $result = new stdClass();
-        $validDates = $this->calculateValidDates(null, $isAmbassador);
+        $generatedUserId = $this->session->id();
+        $validDates = $this->calculateValidDates($generatedUserId, $isAmbassador);
 
         if ($regionPassportModel->createPdf) {
             $result = $this->generatePdf($regionPassportModel->userIds, $isAmbassador, $validDates);
         }
 
-        $generatedUserId = $this->session->id();
-
         $userIds = $result->pdfGeneratedUserIds ?? $regionPassportModel->userIds;
         if ($regionPassportModel->renew) {
             $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
-- 
GitLab


From 0c0f1c2dbae5d44919c96449e4e24e2b2417b5a7 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 22 Sep 2024 19:59:11 +0200
Subject: [PATCH 017/121] condition check for calculateValidDates

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 8a75dc0318..c1fba40eed 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -279,7 +279,7 @@ class PassportGeneratorTransaction extends AbstractController
     private function calculateValidDates(int $userId, bool $isAmbassador): stdClass
     {
         $generationUntilDate = '+3 years';
-        $passDate = $this->getPassDate($userId);
+        $passDate = !$isAmbassador ? $this->getPassDate($userId) : null;
         $fromDate = $isAmbassador ? new DateTime() : clone $passDate;
 
         $untilFrom = $fromDate->format('d. m. Y');
-- 
GitLab


From 5b9cfbcd35ea05a55386866a0f7f0adbde891077 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 23 Sep 2024 11:23:29 +0000
Subject: [PATCH 018/121] Apply 1 suggestion(s) to 1 file(s)

---
 client/src/api/stores.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/client/src/api/stores.js b/client/src/api/stores.js
index e705ba2828..a2fbf868c9 100644
--- a/client/src/api/stores.js
+++ b/client/src/api/stores.js
@@ -16,7 +16,6 @@ export async function getStoreInformation (storeId) {
 
 export async function updateStore (store) {
   const result = await patch(`/stores/${store.id}/information`, store)
-  console.log('store', store)
   return result
 }
 
-- 
GitLab


From aaae80bca60a938c73453fcb471d152f224e1f05 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 23 Sep 2024 16:01:05 +0200
Subject: [PATCH 019/121] added UrlGeneratorInterface

---
 .../PassportGeneratorTransaction.php                 | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index c1fba40eed..e8a5962955 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -15,6 +15,7 @@ use setasign\Fpdi\Tcpdf\Fpdi;
 use stdClass;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
 class PassportGeneratorTransaction extends AbstractController
@@ -29,6 +30,7 @@ class PassportGeneratorTransaction extends AbstractController
         protected FlashMessageHelper $flashMessageHelper,
         protected TranslationHelper $translationHelper,
         protected TranslatorInterface $translator,
+        private readonly UrlGeneratorInterface $router,
         #[Autowire(param: 'kernel.project_dir')]
         private readonly string $projectDir,
     ) {
@@ -111,6 +113,11 @@ class PassportGeneratorTransaction extends AbstractController
         ];
     }
 
+    private function getProfileUrl(int $userId): string
+    {
+        return $this->router->generate('user_profile', ['userId' => $userId], UrlGeneratorInterface::ABSOLUTE_URL);
+    }
+
     private function addUserPhotoToPdf(\TCPDF $pdf, int $userId, float $x, float $y, array $margins): void
     {
         $photo = $this->foodsaverGateway->getPhotoFileName($userId);
@@ -254,10 +261,7 @@ class PassportGeneratorTransaction extends AbstractController
                     'module_height' => 1 // height of a single module in points
                 ];
 
-                $baseUrl = getenv('FS_ENV') === 'dev' ? 'https://foodsharing.de' : BASE_URL;
-                $profileUrl = $baseUrl . '/user/' . $userId . '/profile';
-
-                $pdf->write2DBarcode($profileUrl, 'QRCODE,H', $margins['qrCodeMarginX'] + $x, $margins['qrCodeMarginY'] + $y, 20, 20, $style, 'N', true);
+                $pdf->write2DBarcode($this->getProfileUrl($userId), 'QRCODE,H', $margins['qrCodeMarginX'] + $x, $margins['qrCodeMarginY'] + $y, 20, 20, $style, 'N', true);
 
                 $this->addUserPhotoToPdf($pdf, $userId, $x, $y, $margins);
 
-- 
GitLab


From 8322ff0b67f74d75ebc35c42a8e2217b42996ff5 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 23 Sep 2024 17:13:24 +0200
Subject: [PATCH 020/121] added valid passport in Passport.vue

---
 src/Modules/Profile/ProfileGateway.php       |  5 ++-
 src/Modules/Settings/components/Passport.vue | 43 ++++++++++++++++----
 src/RestApi/UserRestController.php           |  1 +
 3 files changed, 40 insertions(+), 9 deletions(-)

diff --git a/src/Modules/Profile/ProfileGateway.php b/src/Modules/Profile/ProfileGateway.php
index 6eac411b44..d9c7ab4fce 100644
--- a/src/Modules/Profile/ProfileGateway.php
+++ b/src/Modules/Profile/ProfileGateway.php
@@ -75,7 +75,8 @@ final class ProfileGateway extends BaseGateway
 					UNIX_TIMESTAMP(fs.sleep_from) AS sleep_from_ts,
 					UNIX_TIMESTAMP(fs.sleep_until) AS sleep_until_ts,
 					fs.mailbox_id,
-					fs.deleted_at
+					fs.deleted_at,
+					fs.last_pass
 
 			FROM 	fs_foodsaver fs
 
@@ -415,7 +416,7 @@ final class ProfileGateway extends BaseGateway
 			LEFT JOIN fs_foodsaver bot ON pg.bot_id = bot.id
 			WHERE pg.foodsaver_id = :fs_id
 			ORDER BY pg.date
-			DESC 
+			DESC
 			LIMIT 15
 		';
 
diff --git a/src/Modules/Settings/components/Passport.vue b/src/Modules/Settings/components/Passport.vue
index 73932776b9..56417a5164 100644
--- a/src/Modules/Settings/components/Passport.vue
+++ b/src/Modules/Settings/components/Passport.vue
@@ -1,13 +1,22 @@
 <template>
   <div>
-    <div v-if="userDetails.isVerified">
-      {{ $i18n('settings.passport.verified_text') }}
-    </div>
-    <div v-else>
-      {{ $i18n('settings.passport.non_verified_text') }}
-    </div>
+    <b-alert variant="info" show>
+      <span v-if="userDetails.isVerified">
+        {{ $i18n('settings.passport.verified_text') }}
+      </span>
+      <span v-else>
+        {{ $i18n('settings.passport.non_verified_text') }}
+      </span>
+    </b-alert>
+
+    <b-alert :variant="validDays >= 0 ? 'info' : 'danger'" show>
+      <span v-if="validDays >= 0">Dein Ausweis ist noch {{ validDays }} Tage bzw. bis {{ dateMinusThreeYears }} gültig.</span>
+      <span v-else>Dein Ausweis ist nicht mehr gültig.</span>
+      <span>Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
+    </b-alert>
+
     <b-button
-      :disabled="!userDetails.isVerified"
+      :disabled="!userDetails.isVerified || validDays < 0"
       class="my-2"
       @click="tryCreateAsUser()"
     >
@@ -32,6 +41,26 @@ export default {
   },
   computed: {
     userDetails: () => userStore.getUserDetails,
+    validDays () {
+      const today = new Date()
+      const lastPassDate = new Date(this.userDetails.lastPassDate)
+
+      const dateMinusThreeYears = new Date(lastPassDate)
+      dateMinusThreeYears.setFullYear(dateMinusThreeYears.getFullYear() - 3)
+
+      const diffInMs = dateMinusThreeYears - today
+      return Math.floor(diffInMs / (1000 * 60 * 60 * 24))
+    },
+    dateMinusThreeYears () {
+      const lastPassDate = new Date(this.userDetails.lastPassDate)
+      const dateMinusThreeYears = new Date(lastPassDate)
+      dateMinusThreeYears.setFullYear(dateMinusThreeYears.getFullYear() - 3)
+      return dateMinusThreeYears.toLocaleDateString('de-DE', {
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+      })
+    },
   },
   async mounted () {
     await userStore.fetchDetails()
diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index 1b42e4c2e5..0d5978ced6 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -160,6 +160,7 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['gender'] = $data['geschlecht'];
             $response['photo'] = $data['photo'];
             $response['sleeping'] = boolval($data['sleep_status']);
+            $response['lastPassDate'] = $data['last_pass'];
 
             $response['stats']['weight'] = floatval($infos['stat_fetchweight']);
             $response['stats']['count'] = $infos['stat_fetchcount'];
-- 
GitLab


From fa154c46b59ae85d81df1ecc3ba5606d748817ef Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 23 Sep 2024 17:55:47 +0200
Subject: [PATCH 021/121] rework

---
 src/Modules/Settings/components/Passport.vue |  8 ++++++--
 src/RestApi/UserRestController.php           |  6 +++++-
 src/Utility/TimeHelper.php                   | 17 +++++++++++++++++
 3 files changed, 28 insertions(+), 3 deletions(-)

diff --git a/src/Modules/Settings/components/Passport.vue b/src/Modules/Settings/components/Passport.vue
index 56417a5164..fdd60e030c 100644
--- a/src/Modules/Settings/components/Passport.vue
+++ b/src/Modules/Settings/components/Passport.vue
@@ -9,8 +9,12 @@
       </span>
     </b-alert>
 
-    <b-alert :variant="validDays >= 0 ? 'info' : 'danger'" show>
-      <span v-if="validDays >= 0">Dein Ausweis ist noch {{ validDays }} Tage bzw. bis {{ dateMinusThreeYears }} gültig.</span>
+    <b-alert :variant="userDetails.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
+      <span v-if="userDetails.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userDetails.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userDetails.lastPassUntilValid, {
+        day: 'numeric',
+        month: 'numeric',
+        year: 'numeric',
+      }) }}</strong> gültig.</span>
       <span v-else>Dein Ausweis ist nicht mehr gültig.</span>
       <span>Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
     </b-alert>
diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index 0d5978ced6..7ccfed841d 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -51,6 +51,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
 use Symfony\Component\RateLimiter\RateLimiterFactory;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
+use Foodsharing\Utility\TimeHelper;
 
 class UserRestController extends AbstractFoodsharingRestController
 {
@@ -82,7 +83,8 @@ class UserRestController extends AbstractFoodsharingRestController
         private SearchPermissions $searchPermissions,
         private RegionTransactions $regionTransactions,
         private GroupTransactions $groupTransactions,
-        private readonly SettingsTransactions $settingsTransactions
+        private readonly SettingsTransactions $settingsTransactions,
+        private TimeHelper $timeHelper,
     ) {
     }
 
@@ -161,6 +163,8 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['photo'] = $data['photo'];
             $response['sleeping'] = boolval($data['sleep_status']);
             $response['lastPassDate'] = $data['last_pass'];
+            $response['lastPassUntilValid'] = $this->timeHelper->passportDatePlusThreeYears($data['last_pass']);
+            $response['lastPassUntilValidInDays'] = $this->timeHelper->passportValidDays($data['last_pass']);;
 
             $response['stats']['weight'] = floatval($infos['stat_fetchweight']);
             $response['stats']['count'] = $infos['stat_fetchcount'];
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index a3ef16a0eb..daf965855f 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -3,6 +3,7 @@
 namespace Foodsharing\Utility;
 
 use Carbon\Carbon;
+use DateTime;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
 final class TimeHelper
@@ -96,4 +97,20 @@ final class TimeHelper
 
         return $date;
     }
+
+    public function passportValidDays($lastPassDateString)
+    {
+        $lastPassDate = new DateTime($lastPassDateString);
+        $dateMinusThreeYears = new DateTime($lastPassDateString);
+        $dateMinusThreeYears->modify('+3 years');
+        $interval = $lastPassDate->diff($dateMinusThreeYears);
+        return $interval->days;
+    }
+
+    public function passportDatePlusThreeYears($lastPassDateString)
+    {
+        $dateMinusThreeYears = new DateTime($lastPassDateString);
+        $dateMinusThreeYears->modify('+3 years');
+        return $dateMinusThreeYears;
+    }
 }
-- 
GitLab


From 53e41c6095c1977120697f1ed45bd453cd2dd06a Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 23 Sep 2024 18:40:19 +0200
Subject: [PATCH 022/121] added error to dashboard

---
 .../Banners/Errors/ErrorContainer.vue         | 22 +++++++++++++++++++
 client/src/stores/user.js                     |  6 +++++
 src/Utility/TimeHelper.php                    | 15 +++++++++----
 translations/messages.de.yml                  |  8 +++++++
 4 files changed, 47 insertions(+), 4 deletions(-)

diff --git a/client/src/components/Banners/Errors/ErrorContainer.vue b/client/src/components/Banners/Errors/ErrorContainer.vue
index 28006f9f20..ee80e86134 100644
--- a/client/src/components/Banners/Errors/ErrorContainer.vue
+++ b/client/src/components/Banners/Errors/ErrorContainer.vue
@@ -119,6 +119,28 @@ export default {
         })
       }
 
+      if (this.userStore.isPassportInvalidRemaing) {
+        list.push({
+          field: 'passport_is_remain_invalid',
+          links: [{
+            text: 'error.passport_is_remain_invalid.link_1',
+            urlShortHand: 'settings',
+          },
+          ],
+        })
+      }
+
+      if (this.userStore.isPassportInvalid) {
+        list.push({
+          field: 'passport_invalid',
+          links: [{
+            text: 'error.passport_invalid.link_1',
+            urlShortHand: 'settings',
+          },
+          ],
+        })
+      }
+
       return list
     },
   },
diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index d7d8ccd751..300be92074 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -54,6 +54,12 @@ export const useUserStore = defineStore('user', {
     },
     hasBouncingEmail: () => false,
     hasActiveEmail: () => true,
+    isPassportInvalid: (state) => {
+      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 0) : true
+    },
+    isPassportInvalidRemaing: (state) => {
+      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 30) : true
+    },
   },
   actions: {
     async fetchDetails () {
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index daf965855f..88b4fb4a10 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -100,11 +100,18 @@ final class TimeHelper
 
     public function passportValidDays($lastPassDateString)
     {
+        $today = new DateTime();
         $lastPassDate = new DateTime($lastPassDateString);
-        $dateMinusThreeYears = new DateTime($lastPassDateString);
-        $dateMinusThreeYears->modify('+3 years');
-        $interval = $lastPassDate->diff($dateMinusThreeYears);
-        return $interval->days;
+        $validUntilDate = clone $lastPassDate;
+        $validUntilDate->modify('+3 years');
+
+        $interval = $today->diff($validUntilDate);
+
+        if ($interval->invert == 1) {
+            return 0;
+        } else {
+            return $interval->days;
+        }
     }
 
     public function passportDatePlusThreeYears($lastPassDateString)
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index ed626c7d0b..52ef1a7cce 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -3240,6 +3240,14 @@ error:
     title: "Wähle einen Stammbezirk aus."
     description: "Damit du weitermachen kannst, wird ein Stammbezirk benötigt."
     link: "Jetzt Stammbezirk auswählen"
+  passport_invalid:
+    title: "Dein Ausweis ist nicht mehr gültig!"
+    description: "Wende Dich an Deine Botschafter:innen, um ihn erneuern zu lassen."
+    link_1: "Profil-Einstellungen"
+  passport_is_remain_invalid:
+    title: "Dein Ausweis ist bald nicht mehr gültig!"
+    description: "Wende Dich an Deine Botschafter:innen, um ihn erneuern zu lassen."
+    link_1: "Profil-Einstellungen"
 
 information:
   push:
-- 
GitLab


From 91614ba320718c84f73870d50a4605190af75a04 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Wed, 25 Sep 2024 11:31:09 +0200
Subject: [PATCH 023/121] code style

---
 src/RestApi/UserRestController.php | 4 ++--
 src/Utility/TimeHelper.php         | 1 +
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index 7ccfed841d..70434c115c 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -36,6 +36,7 @@ use Foodsharing\Permissions\StorePermissions;
 use Foodsharing\RestApi\Models\Group\UserGroupModel;
 use Foodsharing\RestApi\Models\Region\UserRegionModel;
 use Foodsharing\Utility\EmailHelper;
+use Foodsharing\Utility\TimeHelper;
 use FOS\RestBundle\Controller\Annotations as Rest;
 use FOS\RestBundle\Request\ParamFetcher;
 use Nelmio\ApiDocBundle\Annotation\Model;
@@ -51,7 +52,6 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
 use Symfony\Component\RateLimiter\RateLimiterFactory;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
-use Foodsharing\Utility\TimeHelper;
 
 class UserRestController extends AbstractFoodsharingRestController
 {
@@ -164,7 +164,7 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['sleeping'] = boolval($data['sleep_status']);
             $response['lastPassDate'] = $data['last_pass'];
             $response['lastPassUntilValid'] = $this->timeHelper->passportDatePlusThreeYears($data['last_pass']);
-            $response['lastPassUntilValidInDays'] = $this->timeHelper->passportValidDays($data['last_pass']);;
+            $response['lastPassUntilValidInDays'] = $this->timeHelper->passportValidDays($data['last_pass']);
 
             $response['stats']['weight'] = floatval($infos['stat_fetchweight']);
             $response['stats']['count'] = $infos['stat_fetchcount'];
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index 88b4fb4a10..97fc511d10 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -118,6 +118,7 @@ final class TimeHelper
     {
         $dateMinusThreeYears = new DateTime($lastPassDateString);
         $dateMinusThreeYears->modify('+3 years');
+
         return $dateMinusThreeYears;
     }
 }
-- 
GitLab


From 3c145739aafc7ee8d7a0e390e300966ab48d66ac Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 28 Sep 2024 00:50:15 +0200
Subject: [PATCH 024/121] after merge

---
 client/src/components/Settings/Passport.vue | 26 ++++++++++++++++-----
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index f283c8ebe4..3573a77b97 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -1,11 +1,24 @@
 <template>
   <div>
-    <div v-if="userStore.isVerified">
-      {{ $i18n('settings.passport.verified_text') }}
-    </div>
-    <div v-else>
-      {{ $i18n('settings.passport.non_verified_text') }}
-    </div>
+    <b-alert variant="info" show>
+      <span v-if="userDetails.isVerified">
+        {{ $i18n('settings.passport.verified_text') }}
+      </span>
+      <span v-else>
+        {{ $i18n('settings.passport.non_verified_text') }}
+      </span>
+    </b-alert>
+
+    <b-alert :variant="userDetails.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
+      <span v-if="userDetails.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userDetails.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userDetails.lastPassUntilValid, {
+        day: 'numeric',
+        month: 'numeric',
+        year: 'numeric',
+      }) }}</strong> gültig.</span>
+      <span v-else>Dein Ausweis ist nicht mehr gültig.</span>
+      <span>Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
+    </b-alert>
+
     <div class="d-flex flex-wrap justify-content-center">
       <CreatePDFButton
         :disabled="!userStore.isVerified"
@@ -25,6 +38,7 @@
   </div>
 </template>
 
+
 <script setup>
 import GoogleWalletButton from './GoogleWalletButton.vue'
 import AppleWalletButton from './AppleWalletButton.vue'
-- 
GitLab


From c9d61da120773800f6fa423aab27b51d5133f15a Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 28 Sep 2024 01:03:09 +0200
Subject: [PATCH 025/121] removed role

---
 client/src/components/Settings/Passport.vue   |  1 -
 src/Lib/AppleWalletPass.php                   |  9 +-------
 src/Lib/GoogleWalletPass.php                  | 21 +++--------------
 .../PassportGeneratorTransaction.php          | 23 ++++++++++---------
 4 files changed, 16 insertions(+), 38 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index 3573a77b97..31d9e07d53 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -38,7 +38,6 @@
   </div>
 </template>
 
-
 <script setup>
 import GoogleWalletButton from './GoogleWalletButton.vue'
 import AppleWalletButton from './AppleWalletButton.vue'
diff --git a/src/Lib/AppleWalletPass.php b/src/Lib/AppleWalletPass.php
index 39dd94a9b0..5037cb6fff 100644
--- a/src/Lib/AppleWalletPass.php
+++ b/src/Lib/AppleWalletPass.php
@@ -41,7 +41,7 @@ class AppleWalletPass
         $this->certFilePass = APPLE_WALLET_CERTIFICATE_PASS;
     }
 
-    public function createNewPass(int $userId, string $name, string $profileURL, string $photoFileName, string $role, \DateTime $passDate): string
+    public function createNewPass(int $userId, string $name, string $profileURL, string $photoFileName, \DateTime $passDate): string
     {
         $pass = new PKPass($this->certFilePath, $this->certFilePass);
 
@@ -74,13 +74,6 @@ class AppleWalletPass
                         'value' => $name,
                     ],
                 ],
-                'secondaryFields' => [
-                    [
-                        'key' => 'role',
-                        'label' => $this->translator->trans('settings.passport.wallet.role'),
-                        'value' => $role,
-                    ],
-                ],
                 'auxiliaryFields' => [
                     [
                         'key' => 'valid_start',
diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index 70a888c5ee..0851523b52 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -242,7 +242,7 @@ class GoogleWalletPass
      *
      * @return string The pass object ID: "{$issuerId}.{$userId}"
      */
-    public function createObject(int $userId, string $name, string $profileURL, string $photo, string $role, ?DateTime $passDate)
+    public function createObject(int $userId, string $name, string $profileURL, string $photo, ?DateTime $passDate)
     {
         $issuerId = GOOGLE_WALLET_ISSUER_ID;
         $classSuffix = GOOGLE_WALLET_CLASS_ID;
@@ -290,11 +290,6 @@ class GoogleWalletPass
             ])
           ]),
           'textModulesData' => [
-            new TextModuleData([
-              'header' => $this->translator->trans('settings.passport.wallet.role'),
-              'body' => $role,
-              'id' => 'role'
-            ]),
             new TextModuleData([
               'header' => $this->translator->trans('settings.passport.wallet.valid_start'),
               'body' => $validStart,
@@ -447,7 +442,7 @@ class GoogleWalletPass
      * @param int $userId developer-defined unique ID for this pass object
      * @return string The pass object ID: "{$issuerId}.{$userId}"
      */
-    public function renewObject(int $userId, string $role, DateTime $passDate = new DateTime())
+    public function renewObject(int $userId, DateTime $passDate = new DateTime())
     {
         $issuerId = GOOGLE_WALLET_ISSUER_ID;
         $validStart = $passDate->format('d. m. Y');
@@ -479,11 +474,6 @@ class GoogleWalletPass
             ],
           ],
           'textModulesData' => [
-            new TextModuleData([
-              'header' => $this->translator->trans('settings.passport.wallet.role'),
-              'body' => $role,
-              'id' => 'role'
-            ]),
             new TextModuleData([
               'header' => $this->translator->trans('settings.passport.wallet.valid_start'),
               'body' => $validStart,
@@ -514,7 +504,7 @@ class GoogleWalletPass
      *
      * @return string an "Add to Google Wallet" link
      */
-    public function createNewPassJwt(int $userId, string $name, string $profileURL, string $photo, string $role, ?DateTime $passDate): string
+    public function createNewPassJwt(int $userId, string $name, string $profileURL, string $photo, ?DateTime $passDate): string
     {
         $issuerId = GOOGLE_WALLET_ISSUER_ID;
         $classSuffix = GOOGLE_WALLET_CLASS_ID;
@@ -548,11 +538,6 @@ class GoogleWalletPass
             ])
           ]),
           'textModulesData' => [
-            new TextModuleData([
-              'header' => $this->translator->trans('settings.passport.wallet.role'),
-              'body' => $role,
-              'id' => 'role'
-            ]),
             new TextModuleData([
               'header' => $this->translator->trans('settings.passport.wallet.valid_start'),
               'body' => $validStart,
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 4c438feda6..7beacbd94d 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -2,9 +2,9 @@
 
 namespace Foodsharing\Modules\PassportGenerator;
 
+use DateTime;
 use Foodsharing\Lib\AppleWalletPass;
 use Foodsharing\Lib\GoogleWalletPass;
-use DateTime;
 use Foodsharing\Lib\Session;
 use Foodsharing\Modules\Foodsaver\FoodsaverGateway;
 use Foodsharing\Modules\Profile\ProfileGateway;
@@ -298,13 +298,7 @@ class PassportGeneratorTransaction extends AbstractController
         $result->validUntil = $validUntil;
 
         return $result;
-// update the Google Wallet Pass if the user has one
-            foreach ($foodsavers as $fs_id) {
-                $foodsaver = $this->foodsaverGateway->getFoodsaverDetails($fs_id);
-                $role = $this->getRole($foodsaver['geschlecht'], $foodsaver['rolle']);
-                $this->googleWalletPass->renewObject($fs_id, $role);
-            }
-        }
+    }
 
     public function generatePassportAsUser(int $userId): string
     {
@@ -331,6 +325,7 @@ class PassportGeneratorTransaction extends AbstractController
         if ($regionPassportModel->renew) {
             $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
             $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
+            $this->updateGoogleWallet($userIds);
         }
 
         return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : json_encode(['userIds' => $regionPassportModel->userIds]);
@@ -351,6 +346,13 @@ class PassportGeneratorTransaction extends AbstractController
         return $date;
     }
 
+    private function updateGoogleWallet(array $userIds): void
+    {
+        foreach ($userIds as $userId) {
+            $this->googleWalletPass->renewObject($userId);
+        }
+    }
+
     public function areUsersInRegion(array $userIds, int $regionId): object
     {
         $result = true;
@@ -372,7 +374,6 @@ class PassportGeneratorTransaction extends AbstractController
     public function createWallet(int $userId, string $walletType): string
     {
         $name = $this->session->user('name') . ' ' . $this->session->user('nachname');
-        // $role = $this->getRole($this->session->user('gender'), $this->session->user('role'));
         $passDate = $this->getPassDate($userId);
         $profileURL = $this->router->generate('user_profile', ['userId' => $userId], UrlGeneratorInterface::ABSOLUTE_URL);
         $photo = $this->session->user('photo');
@@ -383,14 +384,14 @@ class PassportGeneratorTransaction extends AbstractController
             case 'apple':
                 $photo_uuid = substr($photo, strlen('/api/uploads/'));
                 $photoFileName = $this->uploadsTransactions->generateFilePath($photo_uuid);
-                $result = $this->appleWalletPass->createNewPass($userId, $name, $profileURL, $photoFileName, $role, $passDate);
+                $result = $this->appleWalletPass->createNewPass($userId, $name, $profileURL, $photoFileName, $passDate);
                 break;
             case 'google':
                 $userPhoto = BASE_URL . $photo;
                 if (getenv('FS_ENV') === 'dev') {
                     $userPhoto = 'https://foodsharing.de/img/50_q_avatar.png';
                 }
-                $result = $this->googleWalletPass->createNewPassJwt($userId, $name, $profileURL, $userPhoto, $role, $passDate);
+                $result = $this->googleWalletPass->createNewPassJwt($userId, $name, $profileURL, $userPhoto, $passDate);
                 break;
             default:
                 throw new \InvalidArgumentException("Ungültiger Wallet-Typ: $walletType");
-- 
GitLab


From 5ec798fdd4e58fb36b9b4ecbdb9580aa1dc48e4b Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 28 Sep 2024 01:09:21 +0200
Subject: [PATCH 026/121] fix

---
 client/src/components/Settings/Passport.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index 31d9e07d53..f7233cd17a 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -1,7 +1,7 @@
 <template>
   <div>
     <b-alert variant="info" show>
-      <span v-if="userDetails.isVerified">
+      <span v-if="userStore.isVerified">
         {{ $i18n('settings.passport.verified_text') }}
       </span>
       <span v-else>
@@ -9,8 +9,8 @@
       </span>
     </b-alert>
 
-    <b-alert :variant="userDetails.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
-      <span v-if="userDetails.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userDetails.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userDetails.lastPassUntilValid, {
+    <b-alert :variant="userStore.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
+      <span v-if="userStore.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userStore.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.lastPassUntilValid, {
         day: 'numeric',
         month: 'numeric',
         year: 'numeric',
@@ -19,7 +19,7 @@
       <span>Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
     </b-alert>
 
-    <div class="d-flex flex-wrap justify-content-center">
+    <div v-if="userStore.lastPassUntilValidInDays >= 0" class="d-flex flex-wrap justify-content-center">
       <CreatePDFButton
         :disabled="!userStore.isVerified"
         class="m-2"
-- 
GitLab


From f47072689bce23d1a6d183ccf4e8813824fcdf82 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Fri, 11 Oct 2024 20:04:29 +0200
Subject: [PATCH 027/121] passport can only be downloaded once it has been
 activated by the ambassador and is valid.

---
 client/src/components/Settings/Passport.vue   | 10 +++--
 .../PassportGeneratorTransaction.php          | 42 ++++++++++++-------
 src/RestApi/UserRestController.php            |  5 ++-
 src/Utility/TimeHelper.php                    | 33 ++++++++-------
 4 files changed, 54 insertions(+), 36 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index f7233cd17a..774376604d 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -10,13 +10,15 @@
     </b-alert>
 
     <b-alert :variant="userStore.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
-      <span v-if="userStore.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userStore.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.lastPassUntilValid, {
+      <span v-if="userStore.lastPassDate === null || userStore.lastPassDate === undefined">
+        Dein Ausweis wurde noch nicht aktiviert. Wende Dich dazu an Deine Botschafter:innen.
+      </span>
+      <span v-else-if="userStore.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userStore.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.lastPassUntilValid, {
         day: 'numeric',
         month: 'numeric',
         year: 'numeric',
-      }) }}</strong> gültig.</span>
-      <span v-else>Dein Ausweis ist nicht mehr gültig.</span>
-      <span>Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
+      }) }}</strong> gültig. Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
+      <span v-else>Dein Ausweis ist nicht mehr gültig. Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
     </b-alert>
 
     <div v-if="userStore.lastPassUntilValidInDays >= 0" class="d-flex flex-wrap justify-content-center">
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 7beacbd94d..5ed70a31ff 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -2,7 +2,9 @@
 
 namespace Foodsharing\Modules\PassportGenerator;
 
+use Carbon\Carbon;
 use DateTime;
+use Exception;
 use Foodsharing\Lib\AppleWalletPass;
 use Foodsharing\Lib\GoogleWalletPass;
 use Foodsharing\Lib\Session;
@@ -12,6 +14,7 @@ use Foodsharing\Modules\Region\RegionGateway;
 use Foodsharing\Modules\Uploads\UploadsTransactions;
 use Foodsharing\RestApi\Models\Passport\CreateRegionPassportModel;
 use Foodsharing\Utility\FlashMessageHelper;
+use Foodsharing\Utility\TimeHelper;
 use Foodsharing\Utility\TranslationHelper;
 use setasign\Fpdi\Tcpdf\Fpdi;
 use stdClass;
@@ -36,7 +39,8 @@ class PassportGeneratorTransaction extends AbstractController
         #[Autowire(param: 'kernel.project_dir')]
         private readonly string $projectDir,
         private GoogleWalletPass $googleWalletPass,
-        private AppleWalletPass $appleWalletPass
+        private AppleWalletPass $appleWalletPass,
+        private readonly TimeHelper $timeHelper
     ) {
     }
 
@@ -284,10 +288,10 @@ class PassportGeneratorTransaction extends AbstractController
         return $result;
     }
 
-    private function calculateValidDates(int $userId, bool $isAmbassador): stdClass
+    private function calculateValidDates(bool $isAmbassador, ?string $currentPassDate): stdClass
     {
         $generationUntilDate = '+3 years';
-        $passDate = !$isAmbassador ? $this->getPassDate($userId) : null;
+        $passDate = !$isAmbassador ? $currentPassDate : null;
         $fromDate = $isAmbassador ? new DateTime() : clone $passDate;
 
         $untilFrom = $fromDate->format('d. m. Y');
@@ -300,10 +304,18 @@ class PassportGeneratorTransaction extends AbstractController
         return $result;
     }
 
+    /**
+     * @throws Exception
+     */
     public function generatePassportAsUser(int $userId): string
     {
         $isAmbassador = false;
-        $validDates = $this->calculateValidDates($userId, $isAmbassador);
+        $date = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
+
+        if (!$this->isValidPassport($date)) {
+            throw new Exception('passport is not valid');
+        }
+        $validDates = $this->calculateValidDates($isAmbassador, $date);
 
         $result = $this->generatePdf([$userId], $isAmbassador, $validDates);
 
@@ -315,7 +327,8 @@ class PassportGeneratorTransaction extends AbstractController
         $isAmbassador = true;
         $result = new stdClass();
         $generatedUserId = $this->session->id();
-        $validDates = $this->calculateValidDates($generatedUserId, $isAmbassador);
+
+        $validDates = $this->calculateValidDates($isAmbassador);
 
         if ($regionPassportModel->createPdf) {
             $result = $this->generatePdf($regionPassportModel->userIds, $isAmbassador, $validDates);
@@ -331,19 +344,16 @@ class PassportGeneratorTransaction extends AbstractController
         return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : json_encode(['userIds' => $regionPassportModel->userIds]);
     }
 
-    public function getPassDate(int $userId): DateTime
+    /**
+     * @throws Exception
+     */
+    private function isValidPassport(string $date): bool
     {
-        $date = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
-
-        if (empty($date)) {
-            $verifyHistory = $this->profileGateway->getVerifyHistory($userId);
-            if (!empty($verifyHistory)) {
-                $latestEntry = end($verifyHistory);
-                $date = $latestEntry->date;
-            }
+        if (!$date) {
+            throw new Exception('Passport is not generated');
         }
-
-        return $date;
+        $carbonDate = new Carbon($date);
+        return $this->timeHelper->isPassportValid($carbonDate);
     }
 
     private function updateGoogleWallet(array $userIds): void
diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index 70434c115c..ada4470810 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -163,9 +163,10 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['photo'] = $data['photo'];
             $response['sleeping'] = boolval($data['sleep_status']);
             $response['lastPassDate'] = $data['last_pass'];
-            $response['lastPassUntilValid'] = $this->timeHelper->passportDatePlusThreeYears($data['last_pass']);
+            $response['lastPassUntilValid'] = isset($data['last_pass'])
+                ? $this->timeHelper->passportDatePlusThreeYears($data['last_pass'])
+                : null;
             $response['lastPassUntilValidInDays'] = $this->timeHelper->passportValidDays($data['last_pass']);
-
             $response['stats']['weight'] = floatval($infos['stat_fetchweight']);
             $response['stats']['count'] = $infos['stat_fetchcount'];
 
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index 13aca7e001..7373948895 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -92,27 +92,32 @@ final class TimeHelper
         }
     }
 
-    public function passportValidDays($lastPassDateString)
+    public function passportValidDays($lastPassDateString): int
     {
-        $today = new Carbon();
-        $lastPassDate = new Carbon($lastPassDateString);
-        $validUntilDate = clone $lastPassDate;
-        $validUntilDate->modify('+3 years');
-
-        $interval = $today->diff($validUntilDate);
+        if (isset($lastPassDateString)) {
+            throw new BadRequestHttpException('Invalid date format');
+        }
+        $today = Carbon::today();
+        $lastPassDate = Carbon::parse($lastPassDateString);
+        $validUntilDate = $lastPassDate->copy()->addYears(3);
 
-        if ($interval->invert == 1) {
+        if ($validUntilDate->isPast()) {
             return 0;
-        } else {
-            return $interval->days;
         }
+
+        return $today->diffInDays($validUntilDate);
     }
 
-    public function passportDatePlusThreeYears($lastPassDateString)
+    public function isPassportValid($lastPassDateString): bool
     {
-        $dateMinusThreeYears = new Carbon($lastPassDateString);
-        $dateMinusThreeYears->modify('+3 years');
+        return $this->passportValidDays($lastPassDateString) >= 1;
+    }
 
-        return $dateMinusThreeYears;
+    public function passportDatePlusThreeYears($lastPassDateString): Carbon
+    {
+        if (isset($lastPassDateString)) {
+            throw new BadRequestHttpException('Invalid date format');
+        }
+        return Carbon::parse($lastPassDateString->format('d.m.Y'))->addYears(3);
     }
 }
-- 
GitLab


From 575f2ff5b2ec3f2bbc2faf9e0978b11fbbca3f38 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Fri, 11 Oct 2024 20:09:33 +0200
Subject: [PATCH 028/121] code style

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 1 +
 src/Utility/TimeHelper.php                                     | 1 +
 2 files changed, 2 insertions(+)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 5ed70a31ff..e8c3273ac9 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -353,6 +353,7 @@ class PassportGeneratorTransaction extends AbstractController
             throw new Exception('Passport is not generated');
         }
         $carbonDate = new Carbon($date);
+
         return $this->timeHelper->isPassportValid($carbonDate);
     }
 
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index 7373948895..95c56de500 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -118,6 +118,7 @@ final class TimeHelper
         if (isset($lastPassDateString)) {
             throw new BadRequestHttpException('Invalid date format');
         }
+
         return Carbon::parse($lastPassDateString->format('d.m.Y'))->addYears(3);
     }
 }
-- 
GitLab


From b17a9552f9c5e0c03ea6fcf46e452e09cf5d3e21 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Fri, 11 Oct 2024 23:20:27 +0200
Subject: [PATCH 029/121] clean code

---
 .../PassportGeneratorTransaction.php          | 36 +++++--------------
 src/Utility/TimeHelper.php                    |  2 +-
 2 files changed, 10 insertions(+), 28 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index e8c3273ac9..e1b701b2a3 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -9,7 +9,6 @@ use Foodsharing\Lib\AppleWalletPass;
 use Foodsharing\Lib\GoogleWalletPass;
 use Foodsharing\Lib\Session;
 use Foodsharing\Modules\Foodsaver\FoodsaverGateway;
-use Foodsharing\Modules\Profile\ProfileGateway;
 use Foodsharing\Modules\Region\RegionGateway;
 use Foodsharing\Modules\Uploads\UploadsTransactions;
 use Foodsharing\RestApi\Models\Passport\CreateRegionPassportModel;
@@ -29,7 +28,6 @@ class PassportGeneratorTransaction extends AbstractController
         private readonly RegionGateway $regionGateway,
         private readonly FoodsaverGateway $foodsaverGateway,
         private readonly PassportGeneratorGateway $passportGeneratorGateway,
-        private readonly ProfileGateway $profileGateway,
         private readonly Session $session,
         private readonly UploadsTransactions $uploadsTransactions,
         protected FlashMessageHelper $flashMessageHelper,
@@ -288,14 +286,11 @@ class PassportGeneratorTransaction extends AbstractController
         return $result;
     }
 
-    private function calculateValidDates(bool $isAmbassador, ?string $currentPassDate): stdClass
+    private function calculateValidDates(?DateTime $currentPassDate = null): stdClass
     {
-        $generationUntilDate = '+3 years';
-        $passDate = !$isAmbassador ? $currentPassDate : null;
-        $fromDate = $isAmbassador ? new DateTime() : clone $passDate;
-
-        $untilFrom = $fromDate->format('d. m. Y');
-        $validUntil = (clone $fromDate)->modify($generationUntilDate)->format('d. m. Y');
+        $generationUntilDate = 3;
+        $untilFrom = $currentPassDate ? Carbon::parse($currentPassDate) : new Carbon();
+        $validUntil = $untilFrom->addYears($generationUntilDate);
 
         $result = new stdClass();
         $result->untilFrom = $untilFrom;
@@ -310,12 +305,12 @@ class PassportGeneratorTransaction extends AbstractController
     public function generatePassportAsUser(int $userId): string
     {
         $isAmbassador = false;
-        $date = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
+        $lastPassDate = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
 
-        if (!$this->isValidPassport($date)) {
+        if (!$this->timeHelper->isPassportValid($lastPassDate)) {
             throw new Exception('passport is not valid');
         }
-        $validDates = $this->calculateValidDates($isAmbassador, $date);
+        $validDates = $this->calculateValidDates($lastPassDate);
 
         $result = $this->generatePdf([$userId], $isAmbassador, $validDates);
 
@@ -328,7 +323,7 @@ class PassportGeneratorTransaction extends AbstractController
         $result = new stdClass();
         $generatedUserId = $this->session->id();
 
-        $validDates = $this->calculateValidDates($isAmbassador);
+        $validDates = $this->calculateValidDates();
 
         if ($regionPassportModel->createPdf) {
             $result = $this->generatePdf($regionPassportModel->userIds, $isAmbassador, $validDates);
@@ -344,19 +339,6 @@ class PassportGeneratorTransaction extends AbstractController
         return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : json_encode(['userIds' => $regionPassportModel->userIds]);
     }
 
-    /**
-     * @throws Exception
-     */
-    private function isValidPassport(string $date): bool
-    {
-        if (!$date) {
-            throw new Exception('Passport is not generated');
-        }
-        $carbonDate = new Carbon($date);
-
-        return $this->timeHelper->isPassportValid($carbonDate);
-    }
-
     private function updateGoogleWallet(array $userIds): void
     {
         foreach ($userIds as $userId) {
@@ -385,7 +367,7 @@ class PassportGeneratorTransaction extends AbstractController
     public function createWallet(int $userId, string $walletType): string
     {
         $name = $this->session->user('name') . ' ' . $this->session->user('nachname');
-        $passDate = $this->getPassDate($userId);
+        $passDate = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
         $profileURL = $this->router->generate('user_profile', ['userId' => $userId], UrlGeneratorInterface::ABSOLUTE_URL);
         $photo = $this->session->user('photo');
         if (!$photo) {
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index 95c56de500..67ae95510a 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -119,6 +119,6 @@ final class TimeHelper
             throw new BadRequestHttpException('Invalid date format');
         }
 
-        return Carbon::parse($lastPassDateString->format('d.m.Y'))->addYears(3);
+        return Carbon::parse($lastPassDateString)->addYears(3);
     }
 }
-- 
GitLab


From 4bc5222047a9f698b2bfa58d67dcfda6e2a88532 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 12:22:48 +0200
Subject: [PATCH 030/121] disable createPassports button if passportMember <= 0

---
 src/Modules/Region/components/MemberList.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 268054006a..f7fe685b18 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -74,7 +74,7 @@
             <b-button
               variant="outline-primary"
               size="sm"
-              :disabled="!passportMember"
+              :disabled="passportMember <= 0"
               @click="createPassports"
             >
               {{ $i18n('group.member_list.passports.generate_button') }} ({{ passportMember.length }})
-- 
GitLab


From 657ef8a2dde25a8e535e69f306e6f8f4020d1dd0 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 12:34:54 +0200
Subject: [PATCH 031/121] added translatedSelectedPassportGenerationOptions

---
 src/Modules/Region/components/MemberList.vue | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index f7fe685b18..916056fea3 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -77,7 +77,7 @@
               :disabled="passportMember <= 0"
               @click="createPassports"
             >
-              {{ $i18n('group.member_list.passports.generate_button') }} ({{ passportMember.length }})
+              {{ translatedSelectedPassportGenerationOptions }} ({{ passportMember.length }})
             </b-button>
           </div>
           <div class="col-md-4">
@@ -334,11 +334,20 @@ export default {
       passportGenerationOptions: [
         { text: 'PDF erstellen', value: 'createPdf' },
         { text: 'Ausweis erneuern', value: 'renew' },
+        { text: 'Verifizieren', value: 'verify' },
       ],
-      selectedPassportGenerationOption: ['createPdf', 'renew'],
+      selectedPassportGenerationOption: ['createPdf', 'renew', 'verify'],
     }
   },
   computed: {
+    translatedSelectedPassportGenerationOptions () {
+      return this.selectedPassportGenerationOption
+        .map(option => {
+          const foundOption = this.passportGenerationOptions.find(item => item.value === option)
+          return foundOption ? foundOption.text : option
+        })
+        .join(', ')
+    },
     createPdf () {
       return this.selectedPassportGenerationOption.includes('createPdf')
     },
-- 
GitLab


From e4dc9d472315588ff1a0426b102c6e3d4a48eeff Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 12:56:22 +0200
Subject: [PATCH 032/121] fix if only renew passport

---
 src/Modules/Region/components/MemberList.vue | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 916056fea3..4df89c2684 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -717,9 +717,11 @@ export default {
     async createPassports () {
       showLoader()
       try {
-        const blob = await createPassportAsAmbassador(this.regionId, this.passportMember, this.createPdf, this.renewPassport)
-        const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
-        this.downloadFile(blob, filename)
+        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.createPdf, this.renewPassport)
+        if (this.createPdf) {
+          const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
+          this.downloadFile(response, filename)
+        }
       } catch (e) {
         pulseError(i18n('error_unexpected'))
       }
-- 
GitLab


From 61d98886bf51bf3bdbdd2d3c13b781837e03aaac Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 13:59:34 +0200
Subject: [PATCH 033/121] added verification during passport generation

---
 src/Modules/Region/components/MemberList.vue | 37 +++++++++++++++-----
 1 file changed, 28 insertions(+), 9 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 4df89c2684..fe827a8ebe 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -333,10 +333,10 @@ export default {
       sortBy: '',
       passportGenerationOptions: [
         { text: 'PDF erstellen', value: 'createPdf' },
-        { text: 'Ausweis erneuern', value: 'renew' },
+        { text: 'Ausweis erstellen / erneuern', value: 'renew' },
         { text: 'Verifizieren', value: 'verify' },
       ],
-      selectedPassportGenerationOption: ['createPdf', 'renew', 'verify'],
+      selectedPassportGenerationOption: ['createPdf', 'renew'],
     }
   },
   computed: {
@@ -354,6 +354,9 @@ export default {
     renewPassport () {
       return this.selectedPassportGenerationOption.includes('renew')
     },
+    verifyDuringPassport () {
+      return this.selectedPassportGenerationOption.includes('verify')
+    },
     getAdminButton () {
       return (item) => {
         if (this.mayRemoveAdminOrAmbassador && this.rowItemIsAdminOrAmbassadorOfRegion(item)) {
@@ -716,14 +719,30 @@ export default {
     },
     async createPassports () {
       showLoader()
-      try {
-        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.createPdf, this.renewPassport)
-        if (this.createPdf) {
-          const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
-          this.downloadFile(response, filename)
+      if (this.renewPassport || this.createPdf) {
+        try {
+          const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.createPdf, this.renewPassport)
+          if (this.createPdf) {
+            const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
+            this.downloadFile(response, filename)
+          }
+        } catch (e) {
+          pulseError(i18n('error_unexpected'))
+        }
+      }
+
+      if (this.verifyDuringPassport) {
+        try {
+          for (const memberId of this.passportMember) {
+            const existingMember = regionStore.memberList.find(entry => entry.id === memberId)
+
+            if (!existingMember || !existingMember.isVerified) {
+              await this.verifyUser(memberId)
+            }
+          }
+        } catch (e) {
+          pulseError('Fehler bei der Verifizierung')
         }
-      } catch (e) {
-        pulseError(i18n('error_unexpected'))
       }
       hideLoader()
     },
-- 
GitLab


From a83493afacded9ac8665d1c300856c9ea1c300ac Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 16:57:35 +0200
Subject: [PATCH 034/121] added filterPassportUntilValid

---
 src/Modules/Region/components/MemberList.vue | 28 +++++++++++++++-----
 1 file changed, 22 insertions(+), 6 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index fe827a8ebe..05e8015509 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -79,6 +79,12 @@
             >
               {{ translatedSelectedPassportGenerationOptions }} ({{ passportMember.length }})
             </b-button>
+            <b-form-checkbox-group
+              v-model="selectedPassportGenerationOption"
+              :options="passportGenerationOptions"
+              switches
+              size="sm"
+            />
           </div>
           <div class="col-md-4">
             <b-form-checkbox
@@ -88,12 +94,8 @@
             >
               {{ $i18n('group.member_list.passports.filter_selection') }}
             </b-form-checkbox>
-            <b-form-checkbox-group
-              v-model="selectedPassportGenerationOption"
-              :options="passportGenerationOptions"
-              switches
-              size="sm"
-            />
+            <label>Filtere nach Mitgliedern mit Ausweis</label>
+            <b-form-select v-model="filterPassportUntilValid" :options="filterPassportUntilValidOptions" />
           </div>
         </div>
       </b-tab>
@@ -329,6 +331,7 @@ export default {
       selectMode: 'multi',
       passportMember: [],
       filterPassportMember: false,
+      filterPassportUntilValid: null,
       activeTab: null,
       sortBy: '',
       passportGenerationOptions: [
@@ -337,6 +340,11 @@ export default {
         { text: 'Verifizieren', value: 'verify' },
       ],
       selectedPassportGenerationOption: ['createPdf', 'renew'],
+      filterPassportUntilValidOptions: [
+        { text: 'keine Filterung', value: null },
+        { text: 'ohne Ausweis', value: 1 },
+        { text: 'mit Ausweis', value: 2 },
+      ],
     }
   },
   computed: {
@@ -421,6 +429,14 @@ export default {
           return false
         }
 
+        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === 2 && member.lastPassDate === null) {
+          return false
+        }
+
+        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === 1 && member.lastPassDate !== null) {
+          return false
+        }
+
         return true
       })
     },
-- 
GitLab


From 2a44c22de02e1bda912fd8eff198f9e086601c19 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 17:18:28 +0200
Subject: [PATCH 035/121] use else if in ErrorContainer.vue

---
 client/src/components/Banners/Errors/ErrorContainer.vue | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/client/src/components/Banners/Errors/ErrorContainer.vue b/client/src/components/Banners/Errors/ErrorContainer.vue
index ee80e86134..77c91045d9 100644
--- a/client/src/components/Banners/Errors/ErrorContainer.vue
+++ b/client/src/components/Banners/Errors/ErrorContainer.vue
@@ -128,9 +128,7 @@ export default {
           },
           ],
         })
-      }
-
-      if (this.userStore.isPassportInvalid) {
+      } else if (this.userStore.isPassportInvalid) {
         list.push({
           field: 'passport_invalid',
           links: [{
-- 
GitLab


From 153afa54bd02f0b6b1b8975cb680d7ae9f59efe0 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 17:26:48 +0200
Subject: [PATCH 036/121] fix ErrorContainer.vue if lastPassUntilValid is null

---
 client/src/components/Banners/Errors/ErrorContainer.vue | 1 -
 client/src/stores/user.js                               | 4 ++--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/client/src/components/Banners/Errors/ErrorContainer.vue b/client/src/components/Banners/Errors/ErrorContainer.vue
index 77c91045d9..2d28ebe2d2 100644
--- a/client/src/components/Banners/Errors/ErrorContainer.vue
+++ b/client/src/components/Banners/Errors/ErrorContainer.vue
@@ -118,7 +118,6 @@ export default {
           }],
         })
       }
-
       if (this.userStore.isPassportInvalidRemaing) {
         list.push({
           field: 'passport_is_remain_invalid',
diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index 300be92074..0310147160 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -55,10 +55,10 @@ export const useUserStore = defineStore('user', {
     hasBouncingEmail: () => false,
     hasActiveEmail: () => true,
     isPassportInvalid: (state) => {
-      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 0) : true
+      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 0) : false
     },
     isPassportInvalidRemaing: (state) => {
-      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 30) : true
+      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 30) : false
     },
   },
   actions: {
-- 
GitLab


From f410bc3bf0615adc27d7e9e4b608f2b44e4dc558 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 18:19:02 +0200
Subject: [PATCH 037/121] added format in calculateValidDates

---
 .../PassportGenerator/PassportGeneratorTransaction.php        | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index e1b701b2a3..8bc869f18c 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -293,8 +293,8 @@ class PassportGeneratorTransaction extends AbstractController
         $validUntil = $untilFrom->addYears($generationUntilDate);
 
         $result = new stdClass();
-        $result->untilFrom = $untilFrom;
-        $result->validUntil = $validUntil;
+        $result->untilFrom = $untilFrom->format('d.m.Y');
+        $result->validUntil = $validUntil->format('d.m.Y');
 
         return $result;
     }
-- 
GitLab


From a7fd352981350fda69cf09b785cc9c3212185a4d Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 18:19:25 +0200
Subject: [PATCH 038/121] revert from Carbon to modify in
 passportDatePlusThreeYears

---
 src/Utility/TimeHelper.php | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index 67ae95510a..0ec5a4734e 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -3,6 +3,7 @@
 namespace Foodsharing\Utility;
 
 use Carbon\Carbon;
+use DateTime;
 use Exception;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Contracts\Translation\TranslatorInterface;
@@ -94,9 +95,10 @@ final class TimeHelper
 
     public function passportValidDays($lastPassDateString): int
     {
-        if (isset($lastPassDateString)) {
+        if (!isset($lastPassDateString)) {
             throw new BadRequestHttpException('Invalid date format');
         }
+
         $today = Carbon::today();
         $lastPassDate = Carbon::parse($lastPassDateString);
         $validUntilDate = $lastPassDate->copy()->addYears(3);
@@ -113,12 +115,17 @@ final class TimeHelper
         return $this->passportValidDays($lastPassDateString) >= 1;
     }
 
-    public function passportDatePlusThreeYears($lastPassDateString): Carbon
+    public function passportDatePlusThreeYears($lastPassDateString): DateTime|false
     {
-        if (isset($lastPassDateString)) {
-            throw new BadRequestHttpException('Invalid date format');
+        if (empty($lastPassDateString)) {
+            throw new BadRequestHttpException('missing date');
+        }
+        $lastPassDate = DateTime::createFromFormat('Y-m-d H:i:s', $lastPassDateString);
+
+        if ($lastPassDate === false) {
+            throw new BadRequestHttpException('Invalid date format. Expected Y-m-d');
         }
 
-        return Carbon::parse($lastPassDateString)->addYears(3);
+        return $lastPassDate->modify('+3 years');
     }
 }
-- 
GitLab


From cc63cf67b271bd1a6e4f037193d3e7891cdef4fe Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 18:24:20 +0200
Subject: [PATCH 039/121] fix userStore in Passport.vue

---
 client/src/components/Settings/Passport.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index 774376604d..288ef6ea00 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -9,11 +9,11 @@
       </span>
     </b-alert>
 
-    <b-alert :variant="userStore.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
-      <span v-if="userStore.lastPassDate === null || userStore.lastPassDate === undefined">
+    <b-alert :variant="userStore.details.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
+      <span v-if="userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined">
         Dein Ausweis wurde noch nicht aktiviert. Wende Dich dazu an Deine Botschafter:innen.
       </span>
-      <span v-else-if="userStore.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userStore.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.lastPassUntilValid, {
+      <span v-else-if="userStore.details.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userStore.details.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.details.lastPassUntilValid, {
         day: 'numeric',
         month: 'numeric',
         year: 'numeric',
@@ -21,7 +21,7 @@
       <span v-else>Dein Ausweis ist nicht mehr gültig. Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
     </b-alert>
 
-    <div v-if="userStore.lastPassUntilValidInDays >= 0" class="d-flex flex-wrap justify-content-center">
+    <div v-if="userStore.details.lastPassUntilValidInDays >= 0" class="d-flex flex-wrap justify-content-center">
       <CreatePDFButton
         :disabled="!userStore.isVerified"
         class="m-2"
-- 
GitLab


From 21cd5b12e2d3b1eed5ada00219943554286a9737 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 18:28:21 +0200
Subject: [PATCH 040/121] translations

---
 src/Modules/Region/components/MemberList.vue | 4 ++--
 translations/messages.de.yml                 | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 05e8015509..a939cfc98e 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -94,7 +94,7 @@
             >
               {{ $i18n('group.member_list.passports.filter_selection') }}
             </b-form-checkbox>
-            <label>Filtere nach Mitgliedern mit Ausweis</label>
+            <label>Zeige nur Mitgliedern nach</label>
             <b-form-select v-model="filterPassportUntilValid" :options="filterPassportUntilValidOptions" />
           </div>
         </div>
@@ -336,7 +336,7 @@ export default {
       sortBy: '',
       passportGenerationOptions: [
         { text: 'PDF erstellen', value: 'createPdf' },
-        { text: 'Ausweis erstellen / erneuern', value: 'renew' },
+        { text: 'Ausweis erstellen / verlängern', value: 'renew' },
         { text: 'Verifizieren', value: 'verify' },
       ],
       selectedPassportGenerationOption: ['createPdf', 'renew'],
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index d541bbd9e0..f550a0ef2b 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -577,7 +577,7 @@ group:
       created_at: "Ausweis erstellt am"
       generate_button: "Ausweis/e für markierte erstellen"
       clear_selection: "Alle markierte zurücksetzen"
-      filter_selection: "Filter nach ausgewählten Mitgliedern"
+      filter_selection: "Zeige nur ausgewählte Mitglieder"
       never_before: "noch nie"
       button:
         verify: "Verifizieren"
-- 
GitLab


From 611200bb7749a0e566741c7e65c79408de26ecec Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 18:45:23 +0200
Subject: [PATCH 041/121] fix test

---
 src/Modules/Region/components/MemberList.vue | 10 +---------
 tests/Acceptance/IdCardsCest.php             |  2 +-
 2 files changed, 2 insertions(+), 10 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index a939cfc98e..1a783c7f9c 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -77,7 +77,7 @@
               :disabled="passportMember <= 0"
               @click="createPassports"
             >
-              {{ translatedSelectedPassportGenerationOptions }} ({{ passportMember.length }})
+              Ausführen ({{ passportMember.length }})
             </b-button>
             <b-form-checkbox-group
               v-model="selectedPassportGenerationOption"
@@ -348,14 +348,6 @@ export default {
     }
   },
   computed: {
-    translatedSelectedPassportGenerationOptions () {
-      return this.selectedPassportGenerationOption
-        .map(option => {
-          const foundOption = this.passportGenerationOptions.find(item => item.value === option)
-          return foundOption ? foundOption.text : option
-        })
-        .join(', ')
-    },
     createPdf () {
       return this.selectedPassportGenerationOption.includes('createPdf')
     },
diff --git a/tests/Acceptance/IdCardsCest.php b/tests/Acceptance/IdCardsCest.php
index c056527c19..08aa7fac9f 100644
--- a/tests/Acceptance/IdCardsCest.php
+++ b/tests/Acceptance/IdCardsCest.php
@@ -47,7 +47,7 @@ class IdCardsCest
             }
         ');
 
-        $I->click('Ausweis/e für markierte erstellen');
+        $I->click('Ausführen');
 
         // $I->waitForFileExists('/downloads/fs_passports_' . $region['id'] . '_' . convertRegionName($region['name']) . '.pdf', 10);
     }
-- 
GitLab


From eb10721e2a694496186672dc94b53bdfeb59c4e2 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 19:08:24 +0200
Subject: [PATCH 042/121] added a confirmationDialogue for 
 verifyDuringPassport

---
 src/Modules/Region/components/MemberList.vue | 8 +++++++-
 translations/messages.de.yml                 | 3 ++-
 2 files changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 1a783c7f9c..9166824b7d 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -740,12 +740,18 @@ export default {
       }
 
       if (this.verifyDuringPassport) {
+        const dialogueOptions = {
+          title: i18n('group.member_list.passports.button.verify'),
+          okTitle: i18n('button.yes_i_am_sure'),
+          okVariant: 'danger',
+        }
+        if (!await this.confirmationDialogue('group.member_list.passports.verify.do_selected', dialogueOptions)) return
         try {
           for (const memberId of this.passportMember) {
             const existingMember = regionStore.memberList.find(entry => entry.id === memberId)
 
             if (!existingMember || !existingMember.isVerified) {
-              await this.verifyUser(memberId)
+              await verifyUser(memberId)
             }
           }
         } catch (e) {
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index f550a0ef2b..da49980e10 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -583,7 +583,8 @@ group:
         verify: "Verifizieren"
         unverify: "Entverifizieren"
       verify:
-        do: "Account von {name} ({id}) verifizieren"
+        do: "Möchtest du {name} ({id}) wirklich verifizieren (Datenabgleich erfolgt / Ausweis kontrolliert etc.)?"
+        do_selected: "Möchtest du alle ausgewählten wirklich verifizieren (Datenabgleich erfolgt / Ausweis kontrolliert etc.)?"
         undo: "Verifizierung von {name} ({id}) aufheben"
     all_roles: "Alle Rollen"
 
-- 
GitLab


From 768539dd7896c66adbae0fe69f0ce0392d2ce58b Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 21:29:03 +0200
Subject: [PATCH 043/121] added checkbox in colum to select and unselect all
 rows

---
 src/Modules/Region/components/MemberList.vue | 25 +++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 9166824b7d..32d2bcb161 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -67,7 +67,7 @@
               size="sm"
               @click="clearSelected"
             >
-              {{ $i18n('group.member_list.passports.clear_selection') }}
+              Alle markieren verifizieren
             </b-button>
           </div>
           <div class="col-md-4 mb-2">
@@ -147,6 +147,19 @@
         class="foto-table"
         @sort-changed="sortBy = $event.sortBy ? $event.sortBy : ''"
       >
+        <template v-if="mayEditMembers" #thead-top>
+          <b-tr>
+            <b-th v-if="activeTab === ACTIVE_TAB_PASSPORT">
+              <b-form-checkbox
+                :checked="selectAllTable"
+                @change="toggleSelectAllTable"
+              />
+            </b-th>
+            <b-th v-for="field in filteredFields" :key="field.key">
+              {{ field.label }}
+            </b-th>
+          </b-tr>
+        </template>
         <template v-if="mayEditMembers" #cell(passportToggle)="row">
           <b-form-checkbox
             v-if="activeTab === ACTIVE_TAB_PASSPORT && !isNullOrEmptyOrWhitespace(row.item.avatar)"
@@ -345,6 +358,7 @@ export default {
         { text: 'ohne Ausweis', value: 1 },
         { text: 'mit Ausweis', value: 2 },
       ],
+      selectAllTable: false,
     }
   },
   computed: {
@@ -569,6 +583,15 @@ export default {
     }
   },
   methods: {
+    toggleSelectAllTable () {
+      this.selectAllTable = !this.selectAllTable
+
+      if (this.selectAllTable) {
+        this.passportMember = this.membersFiltered.map(member => member.id)
+      } else {
+        this.passportMember = []
+      }
+    },
     passUntilValid (creationDate) {
       const validUntil = new Date(creationDate)
       validUntil.setFullYear(validUntil.getFullYear() + 3)
-- 
GitLab


From 3f2077047dc25978c368c38f5389d982aa02afb8 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 21:55:07 +0200
Subject: [PATCH 044/121] moved filter options to dropdown in name search row

---
 src/Modules/Region/components/MemberList.vue | 65 ++++++++++++++------
 1 file changed, 45 insertions(+), 20 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 32d2bcb161..e665fee340 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -87,15 +87,7 @@
             />
           </div>
           <div class="col-md-4">
-            <b-form-checkbox
-              v-model="filterPassportMember"
-              switch
-              size="sm"
-            >
-              {{ $i18n('group.member_list.passports.filter_selection') }}
-            </b-form-checkbox>
-            <label>Zeige nur Mitgliedern nach</label>
-            <b-form-select v-model="filterPassportUntilValid" :options="filterPassportUntilValidOptions" />
+            Platzhalter
           </div>
         </div>
       </b-tab>
@@ -118,7 +110,44 @@
               :placeholder="$i18n('filterlist.filter_for_name_id')"
             >
           </div>
-          <div class="filter-for-delete">
+          <span class="filter-for-search">
+            <b-dropdown
+              id="dropdown-form"
+              ref="dropdown"
+              variant="link"
+              toggle-class="text-decoration-none"
+              no-caret
+            >
+              <template #button-content>
+                <button
+                  v-b-tooltip.hover
+                  :title="$i18n('button.clear_filter')"
+                  type="button"
+                  class="btn btn-sm"
+                >
+                  <i class="fas fa-filter" />
+                </button>
+              </template>
+
+              <b-dropdown-form>
+                <b-form-checkbox
+                  v-model="filterPassportMember"
+                  switch
+                  size="sm"
+                  class="mb-2"
+                >
+                  {{ $i18n('group.member_list.passports.filter_selection') }}
+                </b-form-checkbox>
+
+                <label class="mb-1">Zeige nur Mitgliedern nach</label>
+                <b-form-select
+                  v-model="filterPassportUntilValid"
+                  :options="filterPassportUntilValidOptions"
+                  size="sm"
+                  class="mb-2"
+                />
+              </b-dropdown-form>
+            </b-dropdown>
             <button
               v-b-tooltip.hover
               :title="$i18n('button.clear_filter')"
@@ -128,7 +157,7 @@
             >
               <i class="fas fa-times" />
             </button>
-          </div>
+          </span>
         </div>
       </div>
 
@@ -350,7 +379,6 @@ export default {
       passportGenerationOptions: [
         { text: 'PDF erstellen', value: 'createPdf' },
         { text: 'Ausweis erstellen / verlängern', value: 'renew' },
-        { text: 'Verifizieren', value: 'verify' },
       ],
       selectedPassportGenerationOption: ['createPdf', 'renew'],
       filterPassportUntilValidOptions: [
@@ -368,9 +396,6 @@ export default {
     renewPassport () {
       return this.selectedPassportGenerationOption.includes('renew')
     },
-    verifyDuringPassport () {
-      return this.selectedPassportGenerationOption.includes('verify')
-    },
     getAdminButton () {
       return (item) => {
         if (this.mayRemoveAdminOrAmbassador && this.rowItemIsAdminOrAmbassadorOfRegion(item)) {
@@ -831,17 +856,17 @@ export default {
   }
 }
 
-  .filter-for-delete {
+  .filter-for-search {
   @media (min-width: 375px) {
-    flex-basis: 2%;
+    flex-basis: 5%;
     order: 3;
-    margin:0.5rem;
+    margin: 0;
   }
 
   @media (min-width: 1200px) {
-    flex-basis: 5%;
+    flex-basis: 9%;
     order: 3;
-    margin:0.7rem;
+    margin: 0;
   }
 }
 
-- 
GitLab


From 9f94bd6371ad029fb72872292bd65d316f7081e4 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 21:57:39 +0200
Subject: [PATCH 045/121] moved selectedPassportGenerationOption

---
 src/Modules/Region/components/MemberList.vue | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index e665fee340..e188fc0d5f 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -67,7 +67,7 @@
               size="sm"
               @click="clearSelected"
             >
-              Alle markieren verifizieren
+              Alle markieren verifizieren ({{ passportMember.length }})
             </b-button>
           </div>
           <div class="col-md-4 mb-2">
@@ -77,8 +77,10 @@
               :disabled="passportMember <= 0"
               @click="createPassports"
             >
-              Ausführen ({{ passportMember.length }})
+              Erstellen ({{ passportMember.length }})
             </b-button>
+          </div>
+          <div class="col-md-4">
             <b-form-checkbox-group
               v-model="selectedPassportGenerationOption"
               :options="passportGenerationOptions"
@@ -86,9 +88,6 @@
               size="sm"
             />
           </div>
-          <div class="col-md-4">
-            Platzhalter
-          </div>
         </div>
       </b-tab>
     </b-tabs>
-- 
GitLab


From 257d497f519bd38707ac2868e215bbe091388dbb Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 22:11:01 +0200
Subject: [PATCH 046/121] fix toggleSelectAllTable checkbox

---
 src/Modules/Region/components/MemberList.vue | 18 ++++++------------
 1 file changed, 6 insertions(+), 12 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index e188fc0d5f..c34c59a097 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -175,18 +175,12 @@
         class="foto-table"
         @sort-changed="sortBy = $event.sortBy ? $event.sortBy : ''"
       >
-        <template v-if="mayEditMembers" #thead-top>
-          <b-tr>
-            <b-th v-if="activeTab === ACTIVE_TAB_PASSPORT">
-              <b-form-checkbox
-                :checked="selectAllTable"
-                @change="toggleSelectAllTable"
-              />
-            </b-th>
-            <b-th v-for="field in filteredFields" :key="field.key">
-              {{ field.label }}
-            </b-th>
-          </b-tr>
+        <template #head(passportToggle)>
+          <b-form-checkbox
+            v-if="mayEditMembers && activeTab === ACTIVE_TAB_PASSPORT"
+            :checked="selectAllTable"
+            @change="toggleSelectAllTable"
+          />
         </template>
         <template v-if="mayEditMembers" #cell(passportToggle)="row">
           <b-form-checkbox
-- 
GitLab


From 6e231357946bb1ca655b41344f88225cab5b8563 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 12 Oct 2024 22:14:47 +0200
Subject: [PATCH 047/121] added verifySelectedMember

---
 src/Modules/Region/components/MemberList.vue | 41 ++++++++++----------
 1 file changed, 20 insertions(+), 21 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index c34c59a097..3d16aa72dc 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -65,7 +65,7 @@
               :disabled="passportMember <= 0"
               variant="outline-primary"
               size="sm"
-              @click="clearSelected"
+              @click="verifySelectedMember"
             >
               Alle markieren verifizieren ({{ passportMember.length }})
             </b-button>
@@ -766,6 +766,25 @@ export default {
     clearSelected () {
       this.passportMember = []
     },
+    async verifySelectedMember () {
+      const dialogueOptions = {
+        title: i18n('group.member_list.passports.button.verify'),
+        okTitle: i18n('button.yes_i_am_sure'),
+        okVariant: 'danger',
+      }
+      if (!await this.confirmationDialogue('group.member_list.passports.verify.do_selected', dialogueOptions)) return
+      try {
+        for (const memberId of this.passportMember) {
+          const existingMember = regionStore.memberList.find(entry => entry.id === memberId)
+
+          if (!existingMember || !existingMember.isVerified) {
+            await verifyUser(memberId)
+          }
+        }
+      } catch (e) {
+        pulseError('Fehler bei der Verifizierung')
+      }
+    },
     async createPassports () {
       showLoader()
       if (this.renewPassport || this.createPdf) {
@@ -779,26 +798,6 @@ export default {
           pulseError(i18n('error_unexpected'))
         }
       }
-
-      if (this.verifyDuringPassport) {
-        const dialogueOptions = {
-          title: i18n('group.member_list.passports.button.verify'),
-          okTitle: i18n('button.yes_i_am_sure'),
-          okVariant: 'danger',
-        }
-        if (!await this.confirmationDialogue('group.member_list.passports.verify.do_selected', dialogueOptions)) return
-        try {
-          for (const memberId of this.passportMember) {
-            const existingMember = regionStore.memberList.find(entry => entry.id === memberId)
-
-            if (!existingMember || !existingMember.isVerified) {
-              await verifyUser(memberId)
-            }
-          }
-        } catch (e) {
-          pulseError('Fehler bei der Verifizierung')
-        }
-      }
       hideLoader()
     },
     downloadFile (blob, filename) {
-- 
GitLab


From e1af59216d3e63494c24e3b871f2450215226eb8 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 10:31:05 +0200
Subject: [PATCH 048/121] new layout for passport settings

---
 src/Modules/Region/components/MemberList.vue | 27 +++++++++++++-------
 1 file changed, 18 insertions(+), 9 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 3d16aa72dc..735189623d 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -72,21 +72,30 @@
           </div>
           <div class="col-md-4 mb-2">
             <b-button
+              :disabled="passportMember <= 0"
               variant="outline-primary"
               size="sm"
-              :disabled="passportMember <= 0"
               @click="createPassports"
             >
-              Erstellen ({{ passportMember.length }})
+              Ausweis erstellen ({{ passportMember.length }})
             </b-button>
-          </div>
-          <div class="col-md-4">
-            <b-form-checkbox-group
-              v-model="selectedPassportGenerationOption"
-              :options="passportGenerationOptions"
-              switches
+            <b-dropdown
+              id="dropdown-form"
+              ref="dropdown"
+              text="Einstellungen"
+              class="m-2"
               size="sm"
-            />
+              variant="outline-primary"
+            >
+              <b-dropdown-form>
+                <b-form-checkbox-group
+                  v-model="selectedPassportGenerationOption"
+                  :options="passportGenerationOptions"
+                  switches
+                  size="sm"
+                />
+              </b-dropdown-form>
+            </b-dropdown>
           </div>
         </div>
       </b-tab>
-- 
GitLab


From 92969449bb8dc0d5b48064b784fee4d80a4ea145 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 12:07:57 +0200
Subject: [PATCH 049/121] new layout for passport settings. without dropdown

---
 src/Modules/Region/components/MemberList.vue | 50 ++++++++------------
 1 file changed, 19 insertions(+), 31 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 735189623d..311ef1114d 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -59,43 +59,31 @@
         </div>
       </b-tab>
       <b-tab v-if="!isWorkGroup && mayEditMembers" :title="$i18n('group.member_list.passports.title')">
-        <div class="row">
-          <div class="col-md-4 mb-2">
-            <b-button
-              :disabled="passportMember <= 0"
-              variant="outline-primary"
+        <div class="d-flex justify-content-between">
+          <b-button
+            :disabled="passportMember <= 0"
+            variant="outline-primary"
+            size="sm"
+            @click="verifySelectedMember"
+          >
+            markierte verifizieren ({{ passportMember.length }})
+          </b-button>
+
+          <div class="d-flex align-items-sm-baseline align-items-stretch justify-content-end">
+            <b-form-checkbox-group
+              v-model="selectedPassportGenerationOption"
+              :options="passportGenerationOptions"
+              class="ml-2"
               size="sm"
-              @click="verifySelectedMember"
-            >
-              Alle markieren verifizieren ({{ passportMember.length }})
-            </b-button>
-          </div>
-          <div class="col-md-4 mb-2">
+            />
             <b-button
               :disabled="passportMember <= 0"
               variant="outline-primary"
               size="sm"
               @click="createPassports"
             >
-              Ausweis erstellen ({{ passportMember.length }})
+              Ausführen ({{ passportMember.length }})
             </b-button>
-            <b-dropdown
-              id="dropdown-form"
-              ref="dropdown"
-              text="Einstellungen"
-              class="m-2"
-              size="sm"
-              variant="outline-primary"
-            >
-              <b-dropdown-form>
-                <b-form-checkbox-group
-                  v-model="selectedPassportGenerationOption"
-                  :options="passportGenerationOptions"
-                  switches
-                  size="sm"
-                />
-              </b-dropdown-form>
-            </b-dropdown>
           </div>
         </div>
       </b-tab>
@@ -118,7 +106,7 @@
               :placeholder="$i18n('filterlist.filter_for_name_id')"
             >
           </div>
-          <span class="filter-for-search">
+          <b-button-group class="filter-for-search">
             <b-dropdown
               id="dropdown-form"
               ref="dropdown"
@@ -165,7 +153,7 @@
             >
               <i class="fas fa-times" />
             </button>
-          </span>
+          </b-button-group>
         </div>
       </div>
 
-- 
GitLab


From 81b7b88617d0f7dd1349002d8f1a0b343bea146e Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 12:20:47 +0200
Subject: [PATCH 050/121] disable button to create passport if createpdf or
 renewPassport is false

---
 src/Modules/Region/components/MemberList.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 311ef1114d..ad0bc8da85 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -77,7 +77,7 @@
               size="sm"
             />
             <b-button
-              :disabled="passportMember <= 0"
+              :disabled="passportMember <= 0 || !(createPdf || renewPassport)"
               variant="outline-primary"
               size="sm"
               @click="createPassports"
-- 
GitLab


From 9bfe1f624103048bf21f7acf4a1ad872e03bb592 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 13:05:07 +0200
Subject: [PATCH 051/121] added setPassportSettingsToLocalStorage

---
 src/Modules/Region/components/MemberList.vue | 53 +++++++++++---------
 1 file changed, 29 insertions(+), 24 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index ad0bc8da85..56da52996c 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -70,12 +70,22 @@
           </b-button>
 
           <div class="d-flex align-items-sm-baseline align-items-stretch justify-content-end">
-            <b-form-checkbox-group
-              v-model="selectedPassportGenerationOption"
-              :options="passportGenerationOptions"
+            <b-form-checkbox
+              v-model="isCreatePdf"
               class="ml-2"
               size="sm"
-            />
+              @change="setPassportSettingsToLocalStorage"
+            >
+              Erstelle PDF
+            </b-form-checkbox>
+            <b-form-checkbox
+              v-model="isRenewPassport"
+              class="ml-2 mr-2"
+              size="sm"
+              @change="setPassportSettingsToLocalStorage"
+            >
+              Ausweis erstellen / verlängern
+            </b-form-checkbox>
             <b-button
               :disabled="passportMember <= 0 || !(createPdf || renewPassport)"
               variant="outline-primary"
@@ -366,11 +376,8 @@ export default {
       filterPassportUntilValid: null,
       activeTab: null,
       sortBy: '',
-      passportGenerationOptions: [
-        { text: 'PDF erstellen', value: 'createPdf' },
-        { text: 'Ausweis erstellen / verlängern', value: 'renew' },
-      ],
-      selectedPassportGenerationOption: ['createPdf', 'renew'],
+      isCreatePdf: true,
+      isRenewPassport: true,
       filterPassportUntilValidOptions: [
         { text: 'keine Filterung', value: null },
         { text: 'ohne Ausweis', value: 1 },
@@ -380,12 +387,6 @@ export default {
     }
   },
   computed: {
-    createPdf () {
-      return this.selectedPassportGenerationOption.includes('createPdf')
-    },
-    renewPassport () {
-      return this.selectedPassportGenerationOption.includes('renew')
-    },
     getAdminButton () {
       return (item) => {
         if (this.mayRemoveAdminOrAmbassador && this.rowItemIsAdminOrAmbassadorOfRegion(item)) {
@@ -596,8 +597,14 @@ export default {
     if (!this.isDeactivatedRegion) {
       regionStore.fetchMemberList(this.groupId)
     }
+    this.isCreatePdf = localStorage.getItem('regionMemberList_createPdf')
+    this.isRenewPassport = localStorage.getItem('regionMemberList_renewPassport')
   },
   methods: {
+    setPassportSettingsToLocalStorage () {
+      localStorage.setItem('regionMemberList_createPdf', this.isCreatePdf)
+      localStorage.setItem('regionMemberList_renewPassport', this.isRenewPassport)
+    },
     toggleSelectAllTable () {
       this.selectAllTable = !this.selectAllTable
 
@@ -784,16 +791,14 @@ export default {
     },
     async createPassports () {
       showLoader()
-      if (this.renewPassport || this.createPdf) {
-        try {
-          const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.createPdf, this.renewPassport)
-          if (this.createPdf) {
-            const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
-            this.downloadFile(response, filename)
-          }
-        } catch (e) {
-          pulseError(i18n('error_unexpected'))
+      try {
+        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.isCreatePdf, this.isRenewPassport)
+        if (this.isCreatePdf) {
+          const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
+          this.downloadFile(response, filename)
         }
+      } catch (e) {
+        pulseError(i18n('error_unexpected'))
       }
       hideLoader()
     },
-- 
GitLab


From dcbfe6e59929c3e752888eaff21f80feb7bd1c9b Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 13:26:51 +0200
Subject: [PATCH 052/121] moved passport info from verification to own bell /
 mail in PassportGeneratorTransaction.php

---
 .../Core/DBConstants/Bell/BellType.php        |  2 +-
 .../PassportGeneratorTransaction.php          | 34 ++++++++++++++++++-
 src/RestApi/VerificationRestController.php    |  4 ---
 .../user/passport.de-de.body.html.twig        |  3 ++
 .../user/passport.de-de.subject.twig          |  1 +
 .../user/verification.de-de.body.html.twig    |  3 +-
 translations/messages.de.yml                  |  2 ++
 7 files changed, 41 insertions(+), 8 deletions(-)
 create mode 100644 templates/emailTemplates/user/passport.de-de.body.html.twig
 create mode 100644 templates/emailTemplates/user/passport.de-de.subject.twig

diff --git a/src/Modules/Core/DBConstants/Bell/BellType.php b/src/Modules/Core/DBConstants/Bell/BellType.php
index 1f871745ef..36c8dd6c11 100644
--- a/src/Modules/Core/DBConstants/Bell/BellType.php
+++ b/src/Modules/Core/DBConstants/Bell/BellType.php
@@ -42,7 +42,7 @@ class BellType
     /**
      * The creation of the foodsaver's pass has failed.
      */
-    final public const PASS_CREATION_FAILED = 'pass-fail-%d';
+    final public const PASS_CREATED_OR_RENEWED = 'pass-created-or-renewed-%d';
     /**
      * Notification for a store manager that someone wants to join a store.
      */
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 8bc869f18c..07a84e17dc 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -8,6 +8,8 @@ use Exception;
 use Foodsharing\Lib\AppleWalletPass;
 use Foodsharing\Lib\GoogleWalletPass;
 use Foodsharing\Lib\Session;
+use Foodsharing\Modules\Bell\DTO\Bell;
+use Foodsharing\Modules\Core\DBConstants\Bell\BellType;
 use Foodsharing\Modules\Foodsaver\FoodsaverGateway;
 use Foodsharing\Modules\Region\RegionGateway;
 use Foodsharing\Modules\Uploads\UploadsTransactions;
@@ -21,6 +23,8 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Contracts\Translation\TranslatorInterface;
+use Foodsharing\Modules\Bell\BellGateway;
+use Foodsharing\Utility\EmailHelper;
 
 class PassportGeneratorTransaction extends AbstractController
 {
@@ -38,7 +42,9 @@ class PassportGeneratorTransaction extends AbstractController
         private readonly string $projectDir,
         private GoogleWalletPass $googleWalletPass,
         private AppleWalletPass $appleWalletPass,
-        private readonly TimeHelper $timeHelper
+        private readonly TimeHelper $timeHelper,
+        private readonly BellGateway $bellGateway,
+        private readonly EmailHelper $emailHelper,
     ) {
     }
 
@@ -336,9 +342,35 @@ class PassportGeneratorTransaction extends AbstractController
             $this->updateGoogleWallet($userIds);
         }
 
+        $this->addBellAndSendPassportMail($userIds);
+
         return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : json_encode(['userIds' => $regionPassportModel->userIds]);
     }
 
+    private function addBellAndSendPassportMail(array $userIds): void
+    {
+        foreach ($userIds as $userId) {
+            $passportGenLink = '/user/current/settings?sub=passport';
+            $bellData = Bell::create(
+                'passport_created_or_renewed_title',
+                'passport_created_or_renewed',
+                'fas fa-camera',
+                ['href' => $passportGenLink],
+                ['user' => $this->session->user('name')],
+                BellType::createIdentifier(BellType::PASS_CREATED_OR_RENEWED, $userId)
+            );
+            $this->bellGateway->addBell($userId, $bellData);
+
+            $passportMailLink = 'https://foodsharing.de' . $passportGenLink;
+            $fs = $this->foodsaverGateway->getFoodsaver($userId);
+            $this->emailHelper->tplMail('user/passport', $fs['email'], [
+                'name' => $fs['name'],
+                'link' => $passportMailLink,
+                'anrede' => $this->translator->trans('salutation.' . $fs['geschlecht']),
+            ], false, true);
+        }
+    }
+
     private function updateGoogleWallet(array $userIds): void
     {
         foreach ($userIds as $userId) {
diff --git a/src/RestApi/VerificationRestController.php b/src/RestApi/VerificationRestController.php
index 7e6294bb62..21966a4c6f 100644
--- a/src/RestApi/VerificationRestController.php
+++ b/src/RestApi/VerificationRestController.php
@@ -98,22 +98,18 @@ class VerificationRestController extends AbstractFoodsharingRestController
         $this->foodsaverGateway->changeUserVerification($userId, $sessionId, true);
         $this->bellGateway->delBellsByIdentifier(BellType::createIdentifier(BellType::NEW_FOODSAVER_IN_REGION, $userId));
 
-        $passportGenLink = '/user/current/settings?sub=passport';
         $bellData = Bell::create(
             'foodsaver_verified_title',
             'foodsaver_verified',
             'fas fa-camera',
-            ['href' => $passportGenLink],
             ['user' => $this->session->user('name')],
             BellType::createIdentifier(BellType::FOODSAVER_VERIFIED, $userId)
         );
         $this->bellGateway->addBell($userId, $bellData);
 
-        $passportMailLink = 'https://foodsharing.de' . $passportGenLink;
         $fs = $this->foodsaverGateway->getFoodsaver($userId);
         $this->emailHelper->tplMail('user/verification', $fs['email'], [
             'name' => $fs['name'],
-            'link' => $passportMailLink,
             'anrede' => $this->translator->trans('salutation.' . $fs['geschlecht']),
         ], false, true);
 
diff --git a/templates/emailTemplates/user/passport.de-de.body.html.twig b/templates/emailTemplates/user/passport.de-de.body.html.twig
new file mode 100644
index 0000000000..2fb8835319
--- /dev/null
+++ b/templates/emailTemplates/user/passport.de-de.body.html.twig
@@ -0,0 +1,3 @@
+<p>{{ ANREDE }} {{ NAME }}, </p>
+<p>Dein Ausweis wurde aktiviert oder erneuert. Du kannst deinen Ausweis als PDF-Datei unter folgendem Link herunterladen:</p>
+<p><a href="{{ LINK }}" target="_blank" rel="noopener noreferrer">{{ LINK }}</a></p>
diff --git a/templates/emailTemplates/user/passport.de-de.subject.twig b/templates/emailTemplates/user/passport.de-de.subject.twig
new file mode 100644
index 0000000000..224429a67e
--- /dev/null
+++ b/templates/emailTemplates/user/passport.de-de.subject.twig
@@ -0,0 +1 @@
+Dein Ausweis ist aktiviert oder verlängert worden!
\ No newline at end of file
diff --git a/templates/emailTemplates/user/verification.de-de.body.html.twig b/templates/emailTemplates/user/verification.de-de.body.html.twig
index 30b355716c..aefc1de7fb 100644
--- a/templates/emailTemplates/user/verification.de-de.body.html.twig
+++ b/templates/emailTemplates/user/verification.de-de.body.html.twig
@@ -1,3 +1,2 @@
 <p>{{ ANREDE }} {{ NAME }}, </p>
-<p>Du wurdest verifiziert. Du kannst deinen Ausweis als PDF-Datei unter folgendem Link herunterladen:</p>
-<p><a href="{{ LINK }}" target="_blank" rel="noopener noreferrer">{{ LINK }}</a></p>
+<p>Du wurdest verifiziert. Du kannst dich jetzt in Abholungen eintragen.</p>
\ No newline at end of file
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index da49980e10..3619799fa9 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -1710,6 +1710,8 @@ bell:
   new_foodsaver_verified: "Schon verifiziert – kann loslegen"
   foodsaver_verified_title: "Du bist jetzt verifiziert!"
   foodsaver_verified: "Rufe deinen Ausweis unter …"
+  passport_created_or_renewed_title: "Dein Ausweis..."
+  passport_created_or_renewed: "ist aktiviert oder verlängert worden!"
   betrieb_fetch_title: "{betrieb}"
   betrieb_fetch: "{count} unbestätigte Abholzeiten"
 
-- 
GitLab


From 46e0af95bd8236154aabd086d69f91173126f83e Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 13:29:39 +0200
Subject: [PATCH 053/121] code style

---
 .../PassportGenerator/PassportGeneratorTransaction.php        | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 07a84e17dc..9063572c82 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -8,12 +8,14 @@ use Exception;
 use Foodsharing\Lib\AppleWalletPass;
 use Foodsharing\Lib\GoogleWalletPass;
 use Foodsharing\Lib\Session;
+use Foodsharing\Modules\Bell\BellGateway;
 use Foodsharing\Modules\Bell\DTO\Bell;
 use Foodsharing\Modules\Core\DBConstants\Bell\BellType;
 use Foodsharing\Modules\Foodsaver\FoodsaverGateway;
 use Foodsharing\Modules\Region\RegionGateway;
 use Foodsharing\Modules\Uploads\UploadsTransactions;
 use Foodsharing\RestApi\Models\Passport\CreateRegionPassportModel;
+use Foodsharing\Utility\EmailHelper;
 use Foodsharing\Utility\FlashMessageHelper;
 use Foodsharing\Utility\TimeHelper;
 use Foodsharing\Utility\TranslationHelper;
@@ -23,8 +25,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 use Symfony\Contracts\Translation\TranslatorInterface;
-use Foodsharing\Modules\Bell\BellGateway;
-use Foodsharing\Utility\EmailHelper;
 
 class PassportGeneratorTransaction extends AbstractController
 {
-- 
GitLab


From 6564094915929a50f2ee96426b99d88c4bc14618 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 11:32:46 +0000
Subject: [PATCH 054/121] Apply 1 suggestion(s) to 1 file(s)

---
 templates/emailTemplates/user/passport.de-de.body.html.twig | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/templates/emailTemplates/user/passport.de-de.body.html.twig b/templates/emailTemplates/user/passport.de-de.body.html.twig
index 2fb8835319..9191f74a21 100644
--- a/templates/emailTemplates/user/passport.de-de.body.html.twig
+++ b/templates/emailTemplates/user/passport.de-de.body.html.twig
@@ -1,3 +1,3 @@
 <p>{{ ANREDE }} {{ NAME }}, </p>
-<p>Dein Ausweis wurde aktiviert oder erneuert. Du kannst deinen Ausweis als PDF-Datei unter folgendem Link herunterladen:</p>
+<p>Dein Ausweis wurde aktiviert oder erneuert. Du kannst deinen Ausweis als PDF-Datei, Google- oder Apple-Wallet unter folgendem Link herunterladen:</p>
 <p><a href="{{ LINK }}" target="_blank" rel="noopener noreferrer">{{ LINK }}</a></p>
-- 
GitLab


From 3c0af3733872248a7600ea352e964a5185d3d0ac Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 13:41:20 +0200
Subject: [PATCH 055/121] show filter dropdown only if ACTIVE_TAB_PASSPORT

---
 src/Modules/Region/components/MemberList.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 56da52996c..6fa9eb57ba 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -118,6 +118,7 @@
           </div>
           <b-button-group class="filter-for-search">
             <b-dropdown
+              v-if="activeTab === ACTIVE_TAB_PASSPORT"
               id="dropdown-form"
               ref="dropdown"
               variant="link"
-- 
GitLab


From efff7fa19f04484cfd43cdbaf2ac1472977e6011 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 14:06:00 +0200
Subject: [PATCH 056/121] fix disabled in createPassports

---
 src/Modules/Region/components/MemberList.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 6fa9eb57ba..ee5a2a6fb1 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -87,7 +87,7 @@
               Ausweis erstellen / verlängern
             </b-form-checkbox>
             <b-button
-              :disabled="passportMember <= 0 || !(createPdf || renewPassport)"
+              :disabled="passportMember <= 0 || !(isCreatePdf || isRenewPassport)"
               variant="outline-primary"
               size="sm"
               @click="createPassports"
-- 
GitLab


From 3327392546c5f02be6d5dde5d6c1cc4b102f8cff Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 15:18:05 +0200
Subject: [PATCH 057/121] fixes

---
 client/src/api/verification.js               | 4 ++--
 client/src/components/Settings/Passport.vue  | 2 +-
 src/Lib/GoogleWalletPass.php                 | 2 +-
 src/Modules/Region/components/MemberList.vue | 4 +++-
 src/RestApi/UserRestController.php           | 4 +++-
 5 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/client/src/api/verification.js b/client/src/api/verification.js
index 8c2f09d6fd..09d1445a98 100644
--- a/client/src/api/verification.js
+++ b/client/src/api/verification.js
@@ -20,7 +20,7 @@ export async function createPassportAsUser () {
   return await post('/user/current/passport', {}, { responseType: 'blob' })
 }
 
-export async function createPassportAsAmbassador (regionId, userIds, createPdf = true, renew = true) {
+export async function createPassportAsAmbassador (regionId, userIds, createPdf, renew) {
   const options = createPdf ? { responseType: 'blob' } : {}
-  return await post(`/region/${regionId}/passport`, { userIds, createPdf, renew: renew }, options)
+  return await post(`/region/${regionId}/passport`, { userIds, createPdf, renew }, options)
 }
diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index 288ef6ea00..b528505b58 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -9,7 +9,7 @@
       </span>
     </b-alert>
 
-    <b-alert :variant="userStore.details.lastPassUntilValidInDays >= 0 ? 'info' : 'danger'" show>
+    <b-alert :variant="userStore.details.lastPassUntilValidInDays <= 30 ? 'danger' : 'info'" show>
       <span v-if="userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined">
         Dein Ausweis wurde noch nicht aktiviert. Wende Dich dazu an Deine Botschafter:innen.
       </span>
diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index 0851523b52..fbcdeb0c24 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -452,7 +452,7 @@ class GoogleWalletPass
             $this->service->genericobject->get("{$issuerId}.{$userId}");
         } catch (Exception $ex) {
             if (!empty($ex->getErrors()) && $ex->getErrors()[0]['reason'] == 'resourceNotFound') {
-                echo "Object {$issuerId}.{$userId} not found!";
+                // echo "Object {$issuerId}.{$userId} not found!";
 
                 return "{$issuerId}.{$userId}";
             } else {
diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index ee5a2a6fb1..dbd2eaf078 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -791,9 +791,11 @@ export default {
       }
     },
     async createPassports () {
+      const isCreatePdf = this.isCreatePdf === 'true'
+      const isRenewPassport = this.isRenewPassport === 'true'
       showLoader()
       try {
-        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.isCreatePdf, this.isRenewPassport)
+        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, isCreatePdf, isRenewPassport)
         if (this.isCreatePdf) {
           const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
           this.downloadFile(response, filename)
diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index ada4470810..7991442d4b 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -166,7 +166,9 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['lastPassUntilValid'] = isset($data['last_pass'])
                 ? $this->timeHelper->passportDatePlusThreeYears($data['last_pass'])
                 : null;
-            $response['lastPassUntilValidInDays'] = $this->timeHelper->passportValidDays($data['last_pass']);
+            $response['lastPassUntilValidInDays'] = isset($data['last_pass'])
+                ? $this->timeHelper->passportValidDays($data['last_pass'])
+                : null;
             $response['stats']['weight'] = floatval($infos['stat_fetchweight']);
             $response['stats']['count'] = $infos['stat_fetchcount'];
 
-- 
GitLab


From 062d25545296731614a66c90e8a6de71a190de17 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 16:24:50 +0200
Subject: [PATCH 058/121] fix localStorage

---
 src/Modules/Region/components/MemberList.vue | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index dbd2eaf078..2f624e5527 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -598,8 +598,8 @@ export default {
     if (!this.isDeactivatedRegion) {
       regionStore.fetchMemberList(this.groupId)
     }
-    this.isCreatePdf = localStorage.getItem('regionMemberList_createPdf')
-    this.isRenewPassport = localStorage.getItem('regionMemberList_renewPassport')
+    this.isCreatePdf = JSON.parse(localStorage.getItem('regionMemberList_createPdf'))
+    this.isRenewPassport = JSON.parse(localStorage.getItem('regionMemberList_renewPassport'))
   },
   methods: {
     setPassportSettingsToLocalStorage () {
@@ -791,11 +791,9 @@ export default {
       }
     },
     async createPassports () {
-      const isCreatePdf = this.isCreatePdf === 'true'
-      const isRenewPassport = this.isRenewPassport === 'true'
       showLoader()
       try {
-        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, isCreatePdf, isRenewPassport)
+        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.isCreatePdf, this.isRenewPassport)
         if (this.isCreatePdf) {
           const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
           this.downloadFile(response, filename)
-- 
GitLab


From 9a231e7fadd8683f756700c6aa2c15bbff049b23 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 17:02:12 +0200
Subject: [PATCH 059/121] fix condition check in Passport.vue

---
 client/src/components/Settings/Passport.vue | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index b528505b58..011130435d 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -13,7 +13,7 @@
       <span v-if="userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined">
         Dein Ausweis wurde noch nicht aktiviert. Wende Dich dazu an Deine Botschafter:innen.
       </span>
-      <span v-else-if="userStore.details.lastPassUntilValidInDays >= 0">Dein Ausweis ist noch <strong>{{ userStore.details.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.details.lastPassUntilValid, {
+      <span v-else-if="userStore.details.lastPassUntilValidInDays > 0">Dein Ausweis ist noch <strong>{{ userStore.details.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.details.lastPassUntilValid, {
         day: 'numeric',
         month: 'numeric',
         year: 'numeric',
@@ -21,18 +21,15 @@
       <span v-else>Dein Ausweis ist nicht mehr gültig. Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
     </b-alert>
 
-    <div v-if="userStore.details.lastPassUntilValidInDays >= 0" class="d-flex flex-wrap justify-content-center">
+    <div v-if="userStore.details.lastPassUntilValidInDays > 0 && userStore.isVerified" class="d-flex flex-wrap justify-content-center">
       <CreatePDFButton
-        :disabled="!userStore.isVerified"
         class="m-2"
       />
       <GoogleWalletButton
-        v-if="userStore.isVerified"
         class="m-2"
         href="/api/user/current/google/wallet"
       />
       <AppleWalletButton
-        v-if="userStore.isVerified"
         class="m-2"
         href="/api/user/current/apple/wallet"
       />
-- 
GitLab


From fe02a0f166cdf4437c6cee537aa9d3854d99da85 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 17:30:51 +0200
Subject: [PATCH 060/121] fix ErrorContainer.vue

---
 .../src/components/Banners/Errors/ErrorContainer.vue   | 10 +++++-----
 client/src/stores/user.js                              |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Banners/Errors/ErrorContainer.vue b/client/src/components/Banners/Errors/ErrorContainer.vue
index 2d28ebe2d2..6bda96370a 100644
--- a/client/src/components/Banners/Errors/ErrorContainer.vue
+++ b/client/src/components/Banners/Errors/ErrorContainer.vue
@@ -118,20 +118,20 @@ export default {
           }],
         })
       }
-      if (this.userStore.isPassportInvalidRemaing) {
+      if (this.userStore.isPassportInvalid) {
         list.push({
-          field: 'passport_is_remain_invalid',
+          field: 'passport_invalid',
           links: [{
             text: 'error.passport_is_remain_invalid.link_1',
             urlShortHand: 'settings',
           },
           ],
         })
-      } else if (this.userStore.isPassportInvalid) {
+      } else if (this.userStore.isPassportInvalidRemaining) {
         list.push({
-          field: 'passport_invalid',
+          field: 'passport_is_remain_invalid',
           links: [{
-            text: 'error.passport_invalid.link_1',
+            text: 'error.passport_is_remain_invalid.link_1',
             urlShortHand: 'settings',
           },
           ],
diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index 0310147160..8116c1c7c7 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -57,7 +57,7 @@ export const useUserStore = defineStore('user', {
     isPassportInvalid: (state) => {
       return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 0) : false
     },
-    isPassportInvalidRemaing: (state) => {
+    isPassportInvalidRemaining: (state) => {
       return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 30) : false
     },
   },
-- 
GitLab


From 47e190038df49550e99095fd8d7a6d1889761572 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 20:57:39 +0200
Subject: [PATCH 061/121] use calculateValidDates only in
 generatePassportAsAmbassador if needed

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 9063572c82..660bab7d00 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -329,9 +329,8 @@ class PassportGeneratorTransaction extends AbstractController
         $result = new stdClass();
         $generatedUserId = $this->session->id();
 
-        $validDates = $this->calculateValidDates();
-
         if ($regionPassportModel->createPdf) {
+            $validDates = $this->calculateValidDates();
             $result = $this->generatePdf($regionPassportModel->userIds, $isAmbassador, $validDates);
         }
 
-- 
GitLab


From 952ef60b329f70c90b055324665be9d6d758cbc4 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 13 Oct 2024 22:58:05 +0200
Subject: [PATCH 062/121] clean PassportGeneratorTransaction.php

---
 .../PassportGenerator/PassportGeneratorTransaction.php   | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 660bab7d00..e57eb1332f 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -310,7 +310,6 @@ class PassportGeneratorTransaction extends AbstractController
      */
     public function generatePassportAsUser(int $userId): string
     {
-        $isAmbassador = false;
         $lastPassDate = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
 
         if (!$this->timeHelper->isPassportValid($lastPassDate)) {
@@ -318,20 +317,19 @@ class PassportGeneratorTransaction extends AbstractController
         }
         $validDates = $this->calculateValidDates($lastPassDate);
 
-        $result = $this->generatePdf([$userId], $isAmbassador, $validDates);
+        $result = $this->generatePdf([$userId], false, $validDates);
 
         return $result->pdf->Output('', 'S');
     }
 
     public function generatePassportAsAmbassador(CreateRegionPassportModel $regionPassportModel): mixed
     {
-        $isAmbassador = true;
         $result = new stdClass();
         $generatedUserId = $this->session->id();
 
         if ($regionPassportModel->createPdf) {
             $validDates = $this->calculateValidDates();
-            $result = $this->generatePdf($regionPassportModel->userIds, $isAmbassador, $validDates);
+            $result = $this->generatePdf($regionPassportModel->userIds, true, $validDates);
         }
 
         $userIds = $result->pdfGeneratedUserIds ?? $regionPassportModel->userIds;
@@ -339,10 +337,9 @@ class PassportGeneratorTransaction extends AbstractController
             $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
             $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
             $this->updateGoogleWallet($userIds);
+            $this->addBellAndSendPassportMail($userIds);
         }
 
-        $this->addBellAndSendPassportMail($userIds);
-
         return $regionPassportModel->createPdf ? $result->pdf->Output('', 'S') : json_encode(['userIds' => $regionPassportModel->userIds]);
     }
 
-- 
GitLab


From 9dafdcd4768d5def0604988f63d236281a2cfdc4 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 20:26:47 +0200
Subject: [PATCH 063/121] added passportValid filter

---
 src/Modules/Region/components/MemberList.vue | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 2f624e5527..1dbe421d7f 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -383,6 +383,7 @@ export default {
         { text: 'keine Filterung', value: null },
         { text: 'ohne Ausweis', value: 1 },
         { text: 'mit Ausweis', value: 2 },
+        { text: 'Ausweis abgelaufen', value: 3 },
       ],
       selectAllTable: false,
     }
@@ -452,6 +453,10 @@ export default {
           return false
         }
 
+        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === 3 && this.passportValid(member.lastPassDate)) {
+          return false
+        }
+
         if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === 2 && member.lastPassDate === null) {
           return false
         }
@@ -620,6 +625,12 @@ export default {
       validUntil.setFullYear(validUntil.getFullYear() + 3)
       return validUntil
     },
+    passportValid (creationDate) {
+      if (creationDate === null) { return true }
+      const today = new Date()
+      const validUntil = this.passUntilValid(creationDate)
+      return today <= validUntil
+    },
     isNullOrEmptyOrWhitespace (str) {
       return (str ?? '').trim().length === 0
     },
-- 
GitLab


From 4fe3f8366958462ba5b5a22b42cd9fd1275b6999 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 20:51:13 +0200
Subject: [PATCH 064/121] translation and co in Passport.vue

---
 client/src/components/Settings/Passport.vue | 29 ++++++++++++++-------
 translations/messages.de.yml                |  8 ++++--
 2 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index 011130435d..289eba17d1 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -1,6 +1,6 @@
 <template>
   <div>
-    <b-alert variant="info" show>
+    <b-alert :variant="userStore.isVerified ? 'info' : 'danger'" show>
       <span v-if="userStore.isVerified">
         {{ $i18n('settings.passport.verified_text') }}
       </span>
@@ -9,19 +9,27 @@
       </span>
     </b-alert>
 
-    <b-alert :variant="userStore.details.lastPassUntilValidInDays <= 30 ? 'danger' : 'info'" show>
+    <b-alert :variant="userStore.isPassportInvalidRemaining ? 'danger' : 'info'" show>
       <span v-if="userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined">
-        Dein Ausweis wurde noch nicht aktiviert. Wende Dich dazu an Deine Botschafter:innen.
+        {{ $i18n('settings.passport.passport_not_activated') }} {{ $i18n('settings.passport.ask_your_ambassadors') }}
+      </span>
+      <Markdown
+        v-if="!userStore.isPassportInvalid"
+        :source="$i18n('settings.passport.passport_is_valid_until', {
+          days: userStore.details.lastPassUntilValidInDays,
+          date: $dateFormatter.format(userStore.details.lastPassUntilValid, {
+            day: 'numeric',
+            month: 'numeric',
+            year: 'numeric'
+          })
+        }) + ' ' + $i18n('settings.passport.ask_your_ambassadors')"
+      />
+      <span v-if="userStore.isPassportInvalid">
+        {{ $i18n('settings.passport.passport_is_invalid') }} {{ $i18n('settings.passport.ask_your_ambassadors') }}
       </span>
-      <span v-else-if="userStore.details.lastPassUntilValidInDays > 0">Dein Ausweis ist noch <strong>{{ userStore.details.lastPassUntilValidInDays }}</strong> Tage bzw. bis <strong>{{ $dateFormatter.format(userStore.details.lastPassUntilValid, {
-        day: 'numeric',
-        month: 'numeric',
-        year: 'numeric',
-      }) }}</strong> gültig. Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
-      <span v-else>Dein Ausweis ist nicht mehr gültig. Deine Botschafter:innen können Dir den Ausweis verlängern.</span>
     </b-alert>
 
-    <div v-if="userStore.details.lastPassUntilValidInDays > 0 && userStore.isVerified" class="d-flex flex-wrap justify-content-center">
+    <div v-if="!userStore.isPassportInvalid && userStore.isVerified" class="d-flex flex-wrap justify-content-center">
       <CreatePDFButton
         class="m-2"
       />
@@ -43,6 +51,7 @@ import AppleWalletButton from './AppleWalletButton.vue'
 import CreatePDFButton from './CreatePDFButton.vue'
 import { useUserStore } from '@/stores/user.js'
 import { onMounted } from 'vue'
+import Markdown from '@/components/Markdown/Markdown.vue'
 
 const userStore = useUserStore()
 
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index ebe607c0dd..730346c222 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -1412,7 +1412,7 @@ forum:
     title: "Wirklich E-Mail-Benachrichtigung versenden?"
   go: "Zum Forum"
   restore:
-    really: "Willst du folgenden Beitrag wirklich wiederherstellen?" 
+    really: "Willst du folgenden Beitrag wirklich wiederherstellen?"
     hidden_by: "Ausgeblendet von"
     reason: "Begründung: \"{reason}\""
     title: "Beitrag wiederherstellen"
@@ -1922,7 +1922,7 @@ map:
         member: "Nur meine Betriebe"
     users:
       role:
-        label: 'Rolle' 
+        label: 'Rolle'
         all: 'Alle'
         foodsaver: 'verifizierte Foodsaver:innen'
         store-manager: 'Betriebsverantwortliche'
@@ -2103,6 +2103,10 @@ settings:
     menu: "Ausweis"
     verified_text: "Dieser Ausweis ist für den Einsatz auf dem Smartphone gedacht. Dort kannst du ihn dir abspeichern und dann bei Verlangen vorzeigen. Solltest du kein Smartphone besitzen oder aus einem anderen Grund noch einen herkömmlichen Ausweis benötigen, wende dich an deine Botschafter:innen vor Ort."
     non_verified_text: "Du bist noch nicht verifiziert. Nur verifizierten Foodsaver:innen steht das Herunterladen des Ausweises zur Verfügung."
+    passport_not_activated: "Dein Ausweis wurde noch nicht aktiviert."
+    ask_your_ambassadors: "Wende Dich dazu an Deine Botschafter:innen."
+    passport_is_valid_until: "Dein Ausweis ist noch **{days}** Tage bzw. bis **{date}** gültig."
+    passport_is_invalid: "Dein Ausweis ist nicht mehr gültig."
     button: "Ausweis herunterladen"
     add_to_wallet:
       google: "Zu Google Wallet hinzufügen"
-- 
GitLab


From 568231a229baa68ccc4644a878b82a7a05724c4e Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 21:11:14 +0200
Subject: [PATCH 065/121] translation and co in MemberList.vue

---
 client/src/stores/user.js                    |  7 ++++++
 src/Modules/Region/components/MemberList.vue | 25 ++++++++++----------
 translations/messages.de.yml                 | 11 ++++++++-
 3 files changed, 30 insertions(+), 13 deletions(-)

diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index 8116c1c7c7..f5a098496b 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -99,3 +99,10 @@ export const SLEEP_STATUS = Object.freeze({
   TEMP: 1,
   FULL: 2,
 })
+
+export const PASSPORT_FILTER_OPTIONS = Object.freeze({
+  NO_FILTER: null,
+  NO_PASSPORT: 1,
+  WITH_PASSPORT: 2,
+  INVALID_PASSPORT: 3
+})
diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 1dbe421d7f..8b7c2d587e 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -66,7 +66,7 @@
             size="sm"
             @click="verifySelectedMember"
           >
-            markierte verifizieren ({{ passportMember.length }})
+            {{ $i18n('group.member_list.passports.verify_selected') }} ({{ passportMember.length }})
           </b-button>
 
           <div class="d-flex align-items-sm-baseline align-items-stretch justify-content-end">
@@ -76,7 +76,7 @@
               size="sm"
               @change="setPassportSettingsToLocalStorage"
             >
-              Erstelle PDF
+              {{ $i18n('group.member_list.passports.create_pdf') }}
             </b-form-checkbox>
             <b-form-checkbox
               v-model="isRenewPassport"
@@ -84,7 +84,7 @@
               size="sm"
               @change="setPassportSettingsToLocalStorage"
             >
-              Ausweis erstellen / verlängern
+              {{ $i18n('group.member_list.passports.active_or_renew_passport') }}
             </b-form-checkbox>
             <b-button
               :disabled="passportMember <= 0 || !(isCreatePdf || isRenewPassport)"
@@ -92,7 +92,7 @@
               size="sm"
               @click="createPassports"
             >
-              Ausführen ({{ passportMember.length }})
+              {{ $i18n('group.member_list.passports.execute') }} ({{ passportMember.length }})
             </b-button>
           </div>
         </div>
@@ -146,7 +146,7 @@
                   {{ $i18n('group.member_list.passports.filter_selection') }}
                 </b-form-checkbox>
 
-                <label class="mb-1">Zeige nur Mitgliedern nach</label>
+                <label class="mb-1">{{ $i18n('group.member_list.passports.show_only_member_after') }}</label>
                 <b-form-select
                   v-model="filterPassportUntilValid"
                   :options="filterPassportUntilValidOptions"
@@ -326,6 +326,7 @@ import ConfirmationDialogue from '@/mixins/ConfirmationDialogue'
 import Avatar from '@/components/Avatar/Avatar.vue'
 import MediaQueryMixin from '@/mixins/MediaQueryMixin'
 import { REGION_IDS } from '@/consts'
+import {PASSPORT_FILTER_OPTIONS} from "@/stores/user";
 
 const regionStore = useRegionStore()
 
@@ -380,10 +381,10 @@ export default {
       isCreatePdf: true,
       isRenewPassport: true,
       filterPassportUntilValidOptions: [
-        { text: 'keine Filterung', value: null },
-        { text: 'ohne Ausweis', value: 1 },
-        { text: 'mit Ausweis', value: 2 },
-        { text: 'Ausweis abgelaufen', value: 3 },
+        { text: i18n('group.member_list.passport.no_filter'), value: PASSPORT_FILTER_OPTIONS.NO_FILTER },
+        { text: i18n('group.member_list.passport.no_passport'), value: PASSPORT_FILTER_OPTIONS.NO_PASSPORT },
+        { text: i18n('group.member_list.passport.with_passport'), value: PASSPORT_FILTER_OPTIONS.WITH_PASSPORT },
+        { text: i18n('group.member_list.passport.invalid_passport'), value: PASSPORT_FILTER_OPTIONS.INVALID_PASSPORT },
       ],
       selectAllTable: false,
     }
@@ -453,15 +454,15 @@ export default {
           return false
         }
 
-        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === 3 && this.passportValid(member.lastPassDate)) {
+        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === PASSPORT_FILTER_OPTIONS.INVALID_PASSPORT && this.passportValid(member.lastPassDate)) {
           return false
         }
 
-        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === 2 && member.lastPassDate === null) {
+        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === PASSPORT_FILTER_OPTIONS.WITH_PASSPORT && member.lastPassDate === null) {
           return false
         }
 
-        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === 1 && member.lastPassDate !== null) {
+        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === PASSPORT_FILTER_OPTIONS.NO_PASSPORT && member.lastPassDate !== null) {
           return false
         }
 
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index 730346c222..a460440ed3 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -576,7 +576,11 @@ group:
       title: "Ausweise & Verifizierung"
       created_at: "Ausweis erstellt am"
       generate_button: "Ausweis/e für markierte erstellen"
-      clear_selection: "Alle markierte zurücksetzen"
+      verify_selected: "markierte verifizieren"
+      create_pdf: "Erstelle PDF"
+      active_or_renew_passport: "Ausweis aktivieren / verlängern"
+      execute: "Ausführen"
+      show_only_member_after: "Zeige nur Mitgliedern nach"
       filter_selection: "Zeige nur ausgewählte Mitglieder"
       never_before: "noch nie"
       button:
@@ -586,6 +590,11 @@ group:
         do: "Möchtest du {name} ({id}) wirklich verifizieren (Datenabgleich erfolgt / Ausweis kontrolliert etc.)?"
         do_selected: "Möchtest du alle ausgewählten wirklich verifizieren (Datenabgleich erfolgt / Ausweis kontrolliert etc.)?"
         undo: "Verifizierung von {name} ({id}) aufheben"
+      filter_options:
+        no_filter: "kein Filter"
+        no_passport: "kein Ausweis"
+        with_passport: "mit Ausweis"
+        invalid_passport: "ungültiger Ausweis"
     all_roles: "Alle Rollen"
 
 legal:
-- 
GitLab


From d82ee659f3ab7e2113fcc6f6c1cfcc6b89101a17 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 21:16:31 +0200
Subject: [PATCH 066/121] codestyle

---
 client/src/stores/user.js                    | 2 +-
 src/Modules/Region/components/MemberList.vue | 6 +++---
 translations/messages.de.yml                 | 1 +
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index f5a098496b..29d5dd991a 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -104,5 +104,5 @@ export const PASSPORT_FILTER_OPTIONS = Object.freeze({
   NO_FILTER: null,
   NO_PASSPORT: 1,
   WITH_PASSPORT: 2,
-  INVALID_PASSPORT: 3
+  INVALID_PASSPORT: 3,
 })
diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 8b7c2d587e..6540e17f3f 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -326,7 +326,7 @@ import ConfirmationDialogue from '@/mixins/ConfirmationDialogue'
 import Avatar from '@/components/Avatar/Avatar.vue'
 import MediaQueryMixin from '@/mixins/MediaQueryMixin'
 import { REGION_IDS } from '@/consts'
-import {PASSPORT_FILTER_OPTIONS} from "@/stores/user";
+import { PASSPORT_FILTER_OPTIONS } from '@/stores/user'
 
 const regionStore = useRegionStore()
 
@@ -521,7 +521,7 @@ export default {
         },
         {
           key: 'passUntilValid',
-          label: 'Gültig bis',
+          label: this.$i18n('group.valid_until'),
           sortable: true,
           class: 'align-middle',
         })
@@ -799,7 +799,7 @@ export default {
           }
         }
       } catch (e) {
-        pulseError('Fehler bei der Verifizierung')
+        pulseError(i18n('error_unexpected'))
       }
     },
     async createPassports () {
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index a460440ed3..2b84ad8a3d 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -404,6 +404,7 @@ group:
   description: "Beschreibung"
   photo: "Foto"
   last_activity: "Letzte Aktivität"
+  valid_until: "Gültis bis"
   filter_by_last_activity: "Nicht aktiv in Monaten"
   role_name: "Rolle"
   applications: "Bewerbungen"
-- 
GitLab


From e8aa6544017643326806465135fb6458d88e7fa4 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 21:46:26 +0200
Subject: [PATCH 067/121] added automaticPaperSize

---
 client/src/api/verification.js                        |  4 ++--
 .../PassportGeneratorTransaction.php                  | 10 +++++-----
 src/Modules/Region/components/MemberList.vue          | 11 ++++++++++-
 .../Models/Passport/CreateRegionPassportModel.php     | 11 +++++++++++
 translations/messages.de.yml                          |  1 +
 5 files changed, 29 insertions(+), 8 deletions(-)

diff --git a/client/src/api/verification.js b/client/src/api/verification.js
index 09d1445a98..853273ce1d 100644
--- a/client/src/api/verification.js
+++ b/client/src/api/verification.js
@@ -20,7 +20,7 @@ export async function createPassportAsUser () {
   return await post('/user/current/passport', {}, { responseType: 'blob' })
 }
 
-export async function createPassportAsAmbassador (regionId, userIds, createPdf, renew) {
+export async function createPassportAsAmbassador (regionId, userIds, createPdf, renew, automaticPaperSize) {
   const options = createPdf ? { responseType: 'blob' } : {}
-  return await post(`/region/${regionId}/passport`, { userIds, createPdf, renew }, options)
+  return await post(`/region/${regionId}/passport`, { userIds, createPdf, renew, automaticPaperSize }, options)
 }
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index e57eb1332f..e9ac6f082a 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -48,9 +48,9 @@ class PassportGeneratorTransaction extends AbstractController
     ) {
     }
 
-    private function setupPdfMargins(\TCPDF $pdf, array $userIds): array
+    private function setupPdfMargins(\TCPDF $pdf, array $userIds, bool $automatic_paper_size = true): array
     {
-        $singleUser = (count($userIds) === 1);
+        $singleUser = $automatic_paper_size && count($userIds) === 1;
 
         $pdf->AddPage(
             $singleUser ? 'L' : 'P',
@@ -174,7 +174,7 @@ class PassportGeneratorTransaction extends AbstractController
         }
     }
 
-    private function generatePdf(array $userIds, bool $ambassadorGeneration, $validDates): stdClass
+    private function generatePdf(array $userIds, bool $ambassadorGeneration, bool $automatic_paper_size, $validDates): stdClass
     {
         $protectPDF = !$ambassadorGeneration;
         $cutMarkers = $ambassadorGeneration;
@@ -192,7 +192,7 @@ class PassportGeneratorTransaction extends AbstractController
             $pdf->SetProtection(['print', 'copy', 'modify', 'assemble'], '', null, 0, null);
         }
 
-        $margins = $this->setupPdfMargins($pdf, $userIds);
+        $margins = $this->setupPdfMargins($pdf, $userIds, $automatic_paper_size);
 
         $pdf->SetTextColor(0, 0, 0);
         $pdf->AddFont('Ubuntu-L', '', $this->projectDir . '/lib/font/ubuntul.php', true);
@@ -329,7 +329,7 @@ class PassportGeneratorTransaction extends AbstractController
 
         if ($regionPassportModel->createPdf) {
             $validDates = $this->calculateValidDates();
-            $result = $this->generatePdf($regionPassportModel->userIds, true, $validDates);
+            $result = $this->generatePdf($regionPassportModel->userIds, true, $regionPassportModel->automatic_paper_selection, $validDates);
         }
 
         $userIds = $result->pdfGeneratedUserIds ?? $regionPassportModel->userIds;
diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 6540e17f3f..9267272a6b 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -70,6 +70,14 @@
           </b-button>
 
           <div class="d-flex align-items-sm-baseline align-items-stretch justify-content-end">
+            <b-form-checkbox
+              v-model="automaticPaperSize"
+              class="ml-2"
+              size="sm"
+              :disabled="passportMember.length !== 1"
+            >
+              {{ $i18n('group.member_list.passports.automatic_paper_size') }}
+            </b-form-checkbox>
             <b-form-checkbox
               v-model="isCreatePdf"
               class="ml-2"
@@ -376,6 +384,7 @@ export default {
       passportMember: [],
       filterPassportMember: false,
       filterPassportUntilValid: null,
+      automaticPaperSize: true,
       activeTab: null,
       sortBy: '',
       isCreatePdf: true,
@@ -805,7 +814,7 @@ export default {
     async createPassports () {
       showLoader()
       try {
-        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.isCreatePdf, this.isRenewPassport)
+        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.isCreatePdf, this.isRenewPassport, this.automaticPaperSize)
         if (this.isCreatePdf) {
           const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
           this.downloadFile(response, filename)
diff --git a/src/RestApi/Models/Passport/CreateRegionPassportModel.php b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
index 61eae01236..993c7ce508 100644
--- a/src/RestApi/Models/Passport/CreateRegionPassportModel.php
+++ b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
@@ -42,4 +42,15 @@ class CreateRegionPassportModel
      * @Assert\Type("boolean")
      */
     public bool $renew;
+
+    /**
+     * @OA\Property(
+     *     type="boolean",
+     *     description="Flag for automatic paper selection. If true, passport size is used for a single passport,
+     *     DIN A4 for multiple. If false, DIN A4 is always used."
+     * )
+     * @Assert\NotNull()
+     * @Assert\Type("boolean")
+     */
+    public bool $automatic_paper_size;
 }
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index 2b84ad8a3d..0235c5b3d6 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -578,6 +578,7 @@ group:
       created_at: "Ausweis erstellt am"
       generate_button: "Ausweis/e für markierte erstellen"
       verify_selected: "markierte verifizieren"
+      automatic_paper_size: "Papiergröße automatisch"
       create_pdf: "Erstelle PDF"
       active_or_renew_passport: "Ausweis aktivieren / verlängern"
       execute: "Ausführen"
-- 
GitLab


From c41df714e9bf3b60656211c0afa29dff38604e91 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 21:58:32 +0200
Subject: [PATCH 068/121] CHANGELOG.md and release notes

---
 CHANGELOG.md             |  1 +
 release-notes/2024-12.md | 30 ++++++++++++++++++++++++++++--
 2 files changed, 29 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11c39efc64..a7c5c0b7ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,7 @@
 - Some visual improvements related to store aplications #2157 !3702 @AntonBallmaier
 - "Thumb voting" polls can be created with only one option #975 !3684 @alex.simm
 - Given EDITORIAL_GROUP the same rights as ORGA-User to edit or add content pages !3717 @chriswalg
+- Rework passport and verification for ambassadors and foodsaver !3627 @chriswalg
 
 ## Bugfixes
 - Resolve "Spelling mistake in dates: "Verantstaltung" @Nika-Mel @McGoldi
diff --git a/release-notes/2024-12.md b/release-notes/2024-12.md
index c683a0eefc..4b012dcccb 100644
--- a/release-notes/2024-12.md
+++ b/release-notes/2024-12.md
@@ -19,8 +19,34 @@ Die Enwicklung dieses Systems ist noch nicht abgeschlossen und daher unvollstän
 Es gibt jetzt einen Dark Mode, der die Webseite in dunklen Farben darstellt. Dieser kann in den Einstellungen aktiviert werden. Darüber hinaus gibt in der Zukunft die Möglichkeit weitere Themes zu erstellen.
 Die Themes könnt ihr im Profilmenü neben der Sprachauswahl finden.
 
-### Digitale Foodsharing Ausweise
-Es ist nun möglich, sich den Foodsharing-Ausweis in Google Pay oder Apple Wallet zu speichern. ( !3591 ) Diese können als digitale Version des Ausweises genutzt werden.
+### Ausweise & Verifizierung
+
+#### Digitale Ausweise
+- Es ist nun möglich, sich den Foodsharing-Ausweis in Google Pay oder Apple Wallet zu speichern. ( !3591 ) Diese können als digitale Version des Ausweises genutzt werden.
+
+#### Mitglieder-Seite
+- Tab "Ausweise" wird zu "Ausweise & Verifizierung"
+- Ein berechnetes "Gültig bis"-Datum (Erstelldatum + 3 Jahre) wird angezeigt.
+- Es wird möglich sein, die PDF-Erstellung oder die Ausweis-Aktivierung bzw. Ausweis-Verlängerung über einen Schalter zu deaktivieren.(gespeichert im localStorage des Browsers).
+- Ein Dropdown-Filter in der Namen-/ID-Suche ermöglicht es, Benutzer mit oder ohne Ausweis zu filtern.
+- Es wurde die Möglichkeit eingebaut, die markierten Benutzer gleichzeitig zu verifizieren.
+- Der Popup-Text für die Verifizierung wurde angepasst und ein Popup für die Mehrfachbenutzer-Verifizierung hinzugefügt.
+- Der Button, um die Markierungen der Tabelle alle zu entfernen wurde, in die 1. Spalte eingefügt.
+- der Eintrag zur Ausweishistorie wird nur durch den BOT hinzugefügt. Nicht mehr durch den Foodsaver selbst.
+
+#### Für Foodsaver
+##### Ausweis-Seite
+- Die verbleibende Gültigkeitsdauer des Ausweises wird in Tagen und als Datum angezeigt. 
+- Hinweis-Text: "Deine Botschafter:innen können Dir den Ausweis verlängern."
+- Der Benutzer kann den Ausweis nur herunterladen, wenn er aktiviert oder gültig ist.
+
+##### Dashboard
+- Ab 30 Tagen vor Ablauf wird ein Fehler-Container mit dem Hinweis "Dein Ausweis ist bald nicht mehr gültig" angezeigt. 
+- Nach Ablauf wird "Dein Ausweis ist nicht mehr gültig" angezeigt.
+
+##### Weitere Änderungen
+- Die Rolle wurde auf dem Ausweis entfernt, da sie keine aktive Rolle darstellt, sondern nur anzeigt, welches Quiz absolviert wurde.
+- Der Hinweis zum Ausweis wurde aus der Verifizierungs-E-Mail entfernt und in eine neue Ausweis-Glocke und -E-Mail eingefügt.
 
 ### Bezirke
 - Verlassen des Bezirks ist mit einem Timer verbunden, um aus Versehen verlassen zu verhindern. ( !3562 )
-- 
GitLab


From 18d8df07513c37ddc694f39bc543b438bbb1a4d0 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 22:02:37 +0200
Subject: [PATCH 069/121] fix

---
 .../PassportGenerator/PassportGeneratorTransaction.php        | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index e9ac6f082a..d3afc4766a 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -48,7 +48,7 @@ class PassportGeneratorTransaction extends AbstractController
     ) {
     }
 
-    private function setupPdfMargins(\TCPDF $pdf, array $userIds, bool $automatic_paper_size = true): array
+    private function setupPdfMargins(\TCPDF $pdf, array $userIds, bool $automatic_paper_size): array
     {
         $singleUser = $automatic_paper_size && count($userIds) === 1;
 
@@ -317,7 +317,7 @@ class PassportGeneratorTransaction extends AbstractController
         }
         $validDates = $this->calculateValidDates($lastPassDate);
 
-        $result = $this->generatePdf([$userId], false, $validDates);
+        $result = $this->generatePdf([$userId], false, true, $validDates);
 
         return $result->pdf->Output('', 'S');
     }
-- 
GitLab


From f8490d7857bcbc5e9c61731d72bc27f85da6f75c Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 20 Oct 2024 22:04:52 +0200
Subject: [PATCH 070/121] fix

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 2 +-
 src/RestApi/Models/Passport/CreateRegionPassportModel.php      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index d3afc4766a..7822361d7a 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -329,7 +329,7 @@ class PassportGeneratorTransaction extends AbstractController
 
         if ($regionPassportModel->createPdf) {
             $validDates = $this->calculateValidDates();
-            $result = $this->generatePdf($regionPassportModel->userIds, true, $regionPassportModel->automatic_paper_selection, $validDates);
+            $result = $this->generatePdf($regionPassportModel->userIds, true, $regionPassportModel->automaticPaperSize, $validDates);
         }
 
         $userIds = $result->pdfGeneratedUserIds ?? $regionPassportModel->userIds;
diff --git a/src/RestApi/Models/Passport/CreateRegionPassportModel.php b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
index 993c7ce508..8b2f484e03 100644
--- a/src/RestApi/Models/Passport/CreateRegionPassportModel.php
+++ b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
@@ -52,5 +52,5 @@ class CreateRegionPassportModel
      * @Assert\NotNull()
      * @Assert\Type("boolean")
      */
-    public bool $automatic_paper_size;
+    public bool $automaticPaperSize;
 }
-- 
GitLab


From 271ebc88edd0090f4dd30c8e6efb3ba8a79fe3d8 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 21 Oct 2024 06:21:40 +0200
Subject: [PATCH 071/121] fix translation

---
 src/Modules/Region/components/MemberList.vue | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 9267272a6b..24c19fb9b5 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -390,10 +390,10 @@ export default {
       isCreatePdf: true,
       isRenewPassport: true,
       filterPassportUntilValidOptions: [
-        { text: i18n('group.member_list.passport.no_filter'), value: PASSPORT_FILTER_OPTIONS.NO_FILTER },
-        { text: i18n('group.member_list.passport.no_passport'), value: PASSPORT_FILTER_OPTIONS.NO_PASSPORT },
-        { text: i18n('group.member_list.passport.with_passport'), value: PASSPORT_FILTER_OPTIONS.WITH_PASSPORT },
-        { text: i18n('group.member_list.passport.invalid_passport'), value: PASSPORT_FILTER_OPTIONS.INVALID_PASSPORT },
+        { text: i18n('group.member_list.passports.filter_options.no_filter'), value: PASSPORT_FILTER_OPTIONS.NO_FILTER },
+        { text: i18n('group.member_list.passports.filter_options.no_passport'), value: PASSPORT_FILTER_OPTIONS.NO_PASSPORT },
+        { text: i18n('group.member_list.passports.filter_options.with_passport'), value: PASSPORT_FILTER_OPTIONS.WITH_PASSPORT },
+        { text: i18n('group.member_list.passports.filter_options.invalid_passport'), value: PASSPORT_FILTER_OPTIONS.INVALID_PASSPORT },
       ],
       selectAllTable: false,
     }
-- 
GitLab


From 5fcf335cafe245295b137bc66ca247c800c3808b Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 21 Oct 2024 06:25:57 +0200
Subject: [PATCH 072/121] removed role in GoogleWalletPass.php

---
 src/Lib/GoogleWalletPass.php | 22 ++--------------------
 1 file changed, 2 insertions(+), 20 deletions(-)

diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index fbcdeb0c24..3d262b8839 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -119,7 +119,7 @@ class GoogleWalletPass
             'cardTemplateOverride' => [
               'cardRowTemplateInfos' => [
                 [
-                  'threeItems' => [
+                  'twoItems' => [
                     'startItem' => [
                       'firstValue' => [
                         'fields' => [
@@ -129,15 +129,6 @@ class GoogleWalletPass
                         ]
                       ]
                     ],
-                    'middleItem' => [
-                      'firstValue' => [
-                        'fields' => [
-                          [
-                            'fieldPath' => "object.textModulesData['role']"
-                          ]
-                        ]
-                      ]
-                    ],
                     'endItem' => [
                       'firstValue' => [
                         'fields' => [
@@ -193,7 +184,7 @@ class GoogleWalletPass
             'cardTemplateOverride' => [
               'cardRowTemplateInfos' => [
                 [
-                  'threeItems' => [
+                  'twoItems' => [
                     'startItem' => [
                       'firstValue' => [
                         'fields' => [
@@ -203,15 +194,6 @@ class GoogleWalletPass
                         ]
                       ]
                     ],
-                    'middleItem' => [
-                      'firstValue' => [
-                        'fields' => [
-                          [
-                            'fieldPath' => "object.textModulesData['role']"
-                          ]
-                        ]
-                      ]
-                    ],
                     'endItem' => [
                       'firstValue' => [
                         'fields' => [
-- 
GitLab


From fb2fac84738f68dc5f2e6bb61c285288f5bd6228 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 09:10:08 +0100
Subject: [PATCH 073/121] fix import

---
 src/Modules/Region/components/MemberList.vue | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 692607c555..4d4f259eb6 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -334,8 +334,7 @@ import ConfirmationDialogue from '@/mixins/ConfirmationDialogue'
 import Avatar from '@/components/Avatar/Avatar.vue'
 import MediaQueryMixin from '@/mixins/MediaQueryMixin'
 import { REGION_IDS } from '@/consts'
-import { useUserStore } from '@/stores/user'
-import { PASSPORT_FILTER_OPTIONS } from '@/stores/user'
+import { PASSPORT_FILTER_OPTIONS, useUserStore } from '@/stores/user'
 
 const regionStore = useRegionStore()
 const userStore = useUserStore()
-- 
GitLab


From 55accff431778627b4fb4d0d8c1ffb3466dfe771 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 14:54:01 +0100
Subject: [PATCH 074/121] better naming und simplify

---
 .../Banners/Errors/ErrorContainer.vue         | 32 ++++++-------
 .../components/Banners/Errors/ErrorField.vue  |  2 +-
 client/src/components/Settings/Passport.vue   | 48 ++++++++++++-------
 client/src/stores/user.js                     |  2 +-
 translations/messages.de.yml                  |  8 ++--
 5 files changed, 53 insertions(+), 39 deletions(-)

diff --git a/client/src/components/Banners/Errors/ErrorContainer.vue b/client/src/components/Banners/Errors/ErrorContainer.vue
index 6bda96370a..583c330c59 100644
--- a/client/src/components/Banners/Errors/ErrorContainer.vue
+++ b/client/src/components/Banners/Errors/ErrorContainer.vue
@@ -40,7 +40,7 @@ export default {
           field: 'invalid_mobile_phonenumber',
           links: [{
             text: 'error.invalid_mobile_phonenumber.link',
-            urlShortHand: 'settings',
+            urlShorthand: 'settings',
           }],
         })
       }
@@ -51,7 +51,7 @@ export default {
           field: 'invalid_landline_phonenumber',
           links: [{
             text: 'error.invalid_landline_phonenumber.link',
-            urlShortHand: 'settings',
+            urlShorthand: 'settings',
           }],
         })
       }
@@ -61,7 +61,7 @@ export default {
           field: 'missing_user_avatar',
           links: [{
             text: 'error.missing_user_avatar.link',
-            urlShortHand: 'settings',
+            urlShorthand: 'settings',
           }],
         })
       }
@@ -72,7 +72,7 @@ export default {
           link: 'images/' + this.userStore.getAvatar,
           links: [{
             text: 'error.old_user_avatar.link',
-            urlShortHand: 'settings',
+            urlShorthand: 'settings',
           }],
         })
       }
@@ -86,7 +86,7 @@ export default {
           field: 'missing_geolocation',
           links: [{
             text: 'error.missing_geolocation.link',
-            urlShortHand: 'settings',
+            urlShorthand: 'settings',
           }],
         })
       }
@@ -96,11 +96,11 @@ export default {
           field: 'mail_activation',
           links: [{
             text: 'error.mail_activation.link_1',
-            urlShortHand: 'resendActivationMail',
+            urlShorthand: 'resendActivationMail',
           },
           {
             text: 'error.mail_activation.link_2',
-            urlShortHand: 'settings',
+            urlShorthand: 'settings',
           }],
         })
       }
@@ -110,29 +110,29 @@ export default {
           field: 'mail_bounce',
           links: [{
             text: 'error.mail_bounce.link_1',
-            urlShortHand: 'settings',
+            urlShorthand: 'settings',
           },
           {
             text: 'error.mail_bounce.link_2',
-            urlShortHand: 'freshdesk_locked_email',
+            urlShorthand: 'freshdesk_locked_email',
           }],
         })
       }
       if (this.userStore.isPassportInvalid) {
         list.push({
-          field: 'passport_invalid',
+          field: 'passport_is_invalid',
           links: [{
-            text: 'error.passport_is_remain_invalid.link_1',
-            urlShortHand: 'settings',
+            text: 'error.passport_is_invalid.link_1',
+            urlShorthand: 'settings',
           },
           ],
         })
-      } else if (this.userStore.isPassportInvalidRemaining) {
+      } else if (this.userStore.isPassportInvalidSoon) {
         list.push({
-          field: 'passport_is_remain_invalid',
+          field: 'passport_is_invalid_soon',
           links: [{
-            text: 'error.passport_is_remain_invalid.link_1',
-            urlShortHand: 'settings',
+            text: 'error.passport_is_invalid_soon.link_1',
+            urlShorthand: 'settings',
           },
           ],
         })
diff --git a/client/src/components/Banners/Errors/ErrorField.vue b/client/src/components/Banners/Errors/ErrorField.vue
index 8c8e503231..f3d98a06d7 100644
--- a/client/src/components/Banners/Errors/ErrorField.vue
+++ b/client/src/components/Banners/Errors/ErrorField.vue
@@ -22,7 +22,7 @@
           v-for="(link, key) in entry.links"
           :key="key"
           class="errorfield__link"
-          :href="link.urlShortHand ? $url(link.urlShortHand) : link.href"
+          :href="link.urlShorthand ? $url(link.urlShorthand) : link.href"
           @click="link.modal ? $bvModal.show(link.modal) : null"
           v-text="$i18n(link.text)"
         />
diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index 289eba17d1..e0ec3bccb8 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -9,24 +9,27 @@
       </span>
     </b-alert>
 
-    <b-alert :variant="userStore.isPassportInvalidRemaining ? 'danger' : 'info'" show>
-      <span v-if="userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined">
-        {{ $i18n('settings.passport.passport_not_activated') }} {{ $i18n('settings.passport.ask_your_ambassadors') }}
-      </span>
+    <!-- Alert for never activated passport -->
+    <b-alert
+      v-if="userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined"
+      variant="info"
+      show
+    >
+      {{ $i18n('settings.passport.passport_not_activated') }}
+      {{ $i18n('settings.passport.ask_your_ambassadors') }}
+    </b-alert>
+
+    <!-- Alert for activated passport (either valid or invalid) -->
+    <b-alert
+      v-else-if="userStore.isPassportInvalid"
+      :variant="userStore.isPassportInvalidSoon ? 'danger' : 'info'"
+      show
+    >
       <Markdown
-        v-if="!userStore.isPassportInvalid"
-        :source="$i18n('settings.passport.passport_is_valid_until', {
-          days: userStore.details.lastPassUntilValidInDays,
-          date: $dateFormatter.format(userStore.details.lastPassUntilValid, {
-            day: 'numeric',
-            month: 'numeric',
-            year: 'numeric'
-          })
-        }) + ' ' + $i18n('settings.passport.ask_your_ambassadors')"
+        :source="passportValidMessage"
       />
-      <span v-if="userStore.isPassportInvalid">
-        {{ $i18n('settings.passport.passport_is_invalid') }} {{ $i18n('settings.passport.ask_your_ambassadors') }}
-      </span>
+      {{ $i18n('settings.passport.passport_is_invalid') }}
+      {{ $i18n('settings.passport.ask_your_ambassadors') }}
     </b-alert>
 
     <div v-if="!userStore.isPassportInvalid && userStore.isVerified" class="d-flex flex-wrap justify-content-center">
@@ -50,7 +53,7 @@ import GoogleWalletButton from './GoogleWalletButton.vue'
 import AppleWalletButton from './AppleWalletButton.vue'
 import CreatePDFButton from './CreatePDFButton.vue'
 import { useUserStore } from '@/stores/user.js'
-import { onMounted } from 'vue'
+import { onMounted, computed } from 'vue'
 import Markdown from '@/components/Markdown/Markdown.vue'
 
 const userStore = useUserStore()
@@ -58,4 +61,15 @@ const userStore = useUserStore()
 onMounted(async () => {
   await userStore.fetchDetails()
 })
+
+const passportValidMessage = computed(() => {
+  return this.$i18n('settings.passport.passport_is_valid_until', {
+    days: userStore.details.lastPassUntilValidInDays,
+    date: this.$dateFormatter.format(userStore.details.lastPassUntilValid, {
+      day: 'numeric',
+      month: 'numeric',
+      year: 'numeric',
+    }),
+  }) + ' ' + this.$i18n('settings.passport.ask_your_ambassadors')
+})
 </script>
diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index e9f565b1bc..1c47ecbc62 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -58,7 +58,7 @@ export const useUserStore = defineStore('user', {
     isPassportInvalid: (state) => {
       return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 0) : false
     },
-    isPassportInvalidRemaining: (state) => {
+    isPassportInvalidSoon: (state) => {
       return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 30) : false
     },
   },
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index 6c8e3bfd52..b5785534d7 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -3343,11 +3343,11 @@ error:
     title: "Wähle einen Stammbezirk aus."
     description: "Damit du weitermachen kannst, wird ein Stammbezirk benötigt."
     link: "Jetzt Stammbezirk auswählen"
-  passport_invalid:
+  passport_is_invalid:
     title: "Dein Ausweis ist nicht mehr gültig!"
     description: "Wende Dich an Deine Botschafter:innen, um ihn erneuern zu lassen."
     link_1: "Profil-Einstellungen"
-  passport_is_remain_invalid:
+  passport_is_invalid_soon:
     title: "Dein Ausweis ist bald nicht mehr gültig!"
     description: "Wende Dich an Deine Botschafter:innen, um ihn erneuern zu lassen."
     link_1: "Profil-Einstellungen"
@@ -3569,7 +3569,7 @@ notifications:
     cancel: "Nicht aktivieren"
     content: |
       Damit du schneller mitbekommst, wenn andere Foodsaver:innen dir Nachrichten schicken, kannst du auf diesem Gerät Push-Benachrichtigungen aktivieren!
-      
+
       In deinen [Benachichtigungseinstellungen](/?page=settings&sub=info) kannst du diese Benachrichtigungen jeder Zeit wieder abschalten.
     dontAskAgain: "Nicht wieder fragen"
 
@@ -3769,7 +3769,7 @@ help:
 
         ---
 
-        Wichtig zu beachten: Diese Funktion basiert nur auf der foodsharing-eigenen Hygieneschulung. Weder im Profil hochgeladene Hygienezertifikate externer Anbieter (wie bspw. Metro), noch anderweitig vermerkte Hygieneschulungen werden akzeptiert. 
+        Wichtig zu beachten: Diese Funktion basiert nur auf der foodsharing-eigenen Hygieneschulung. Weder im Profil hochgeladene Hygienezertifikate externer Anbieter (wie bspw. Metro), noch anderweitig vermerkte Hygieneschulungen werden akzeptiert.
 team_page:
   board_member:
     title: "Vorstand"
-- 
GitLab


From c34502c11f75f5b172d69dcde09c3ba9ef631514 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 15:00:36 +0100
Subject: [PATCH 075/121] createPdf can be null

---
 src/RestApi/Models/Passport/CreateRegionPassportModel.php | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/RestApi/Models/Passport/CreateRegionPassportModel.php b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
index 8b2f484e03..bf2c1b0550 100644
--- a/src/RestApi/Models/Passport/CreateRegionPassportModel.php
+++ b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
@@ -28,10 +28,9 @@ class CreateRegionPassportModel
      *     type="boolean",
      *     description="Flag to create PDF"
      * )
-     * @Assert\NotNull()
      * @Assert\Type("boolean")
      */
-    public bool $createPdf;
+    public ?bool $createPdf = null;
 
     /**
      * @OA\Property(
-- 
GitLab


From e2fd242fd1c650f10fe4489d6372750390fae7bd Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 16:05:31 +0100
Subject: [PATCH 076/121] simplify Passport.vue

---
 client/src/components/Settings/Passport.vue | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index e0ec3bccb8..9c50e09f51 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -21,15 +21,16 @@
 
     <!-- Alert for activated passport (either valid or invalid) -->
     <b-alert
-      v-else-if="userStore.isPassportInvalid"
+      v-else
       :variant="userStore.isPassportInvalidSoon ? 'danger' : 'info'"
       show
     >
       <Markdown
+        v-if="!userStore.isPassportInvalid"
         :source="passportValidMessage"
       />
-      {{ $i18n('settings.passport.passport_is_invalid') }}
-      {{ $i18n('settings.passport.ask_your_ambassadors') }}
+      <span v-if="userStore.isPassportInvalid">{{ $i18n('settings.passport.passport_is_invalid') }}</span>
+      <span v-if="userStore.isPassportInvalid || userStore.isPassportInvalidSoon">{{ $i18n('settings.passport.ask_your_ambassadors') }}</span>
     </b-alert>
 
     <div v-if="!userStore.isPassportInvalid && userStore.isVerified" class="d-flex flex-wrap justify-content-center">
@@ -55,6 +56,8 @@ import CreatePDFButton from './CreatePDFButton.vue'
 import { useUserStore } from '@/stores/user.js'
 import { onMounted, computed } from 'vue'
 import Markdown from '@/components/Markdown/Markdown.vue'
+import i18n from '@/helper/i18n'
+import dateFormatter from '@/helper/date-formatter'
 
 const userStore = useUserStore()
 
@@ -63,13 +66,13 @@ onMounted(async () => {
 })
 
 const passportValidMessage = computed(() => {
-  return this.$i18n('settings.passport.passport_is_valid_until', {
+  return i18n('settings.passport.passport_is_valid_until', {
     days: userStore.details.lastPassUntilValidInDays,
-    date: this.$dateFormatter.format(userStore.details.lastPassUntilValid, {
+    date: dateFormatter.format(userStore.details.lastPassUntilValid, {
       day: 'numeric',
       month: 'numeric',
       year: 'numeric',
     }),
-  }) + ' ' + this.$i18n('settings.passport.ask_your_ambassadors')
+  })
 })
 </script>
-- 
GitLab


From 64559ad3751cbf2cd043e507b4b4e21f3818fef9 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 16:11:39 +0100
Subject: [PATCH 077/121] release notes

---
 release-notes/2024-12.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/release-notes/2024-12.md b/release-notes/2024-12.md
index 2ecaeb2654..0ae91dd599 100644
--- a/release-notes/2024-12.md
+++ b/release-notes/2024-12.md
@@ -27,9 +27,9 @@ Die Themes könnt ihr im Profilmenü neben der Sprachauswahl finden.
 - Es ist nun möglich, sich den Foodsharing-Ausweis in Google Pay oder Apple Wallet zu speichern. ( !3591 ) Diese können als digitale Version des Ausweises genutzt werden.
 
 #### Mitglieder-Seite
-- Tab "Ausweise" wird zu "Ausweise & Verifizierung"
+- Tab "Ausweise" wurde zu "Ausweise & Verifizierung"
 - Ein berechnetes "Gültig bis"-Datum (Erstelldatum + 3 Jahre) wird angezeigt.
-- Es wird möglich sein, die PDF-Erstellung oder die Ausweis-Aktivierung bzw. Ausweis-Verlängerung über einen Schalter zu deaktivieren.(gespeichert im localStorage des Browsers).
+- Es ist möglich sein, die PDF-Erstellung oder die Ausweis-Aktivierung bzw. Ausweis-Verlängerung über einen Schalter zu deaktivieren.(gespeichert im Browser).
 - Ein Dropdown-Filter in der Namen-/ID-Suche ermöglicht es, Benutzer mit oder ohne Ausweis zu filtern.
 - Es wurde die Möglichkeit eingebaut, die markierten Benutzer gleichzeitig zu verifizieren.
 - Der Popup-Text für die Verifizierung wurde angepasst und ein Popup für die Mehrfachbenutzer-Verifizierung hinzugefügt.
@@ -101,4 +101,4 @@ Vielen Dank an die überregionale AG Hygiene (allen voran [Jörg](/user/150506/p
 - Die Handynummer war ungewollt im Registrierungsformular ein Pflichtfeld. Dies wurde behoben. ( !3726)
 - Die Eingabe des Geburtsdatums bei der Registrierung und in den Profileinstellungen ist jetzt auf kleineren Displays besser möglich ( !3738)
 - Push-Benachrichtigungen für Chat-Nachrichten können jetzt einfach über das Chat-Menü aktiviert werden. ( !3375)
-- Beim Verfassen einer neuen Email können jetzt auch Empfängeradressen hinzugefügt werden, die den ersten Teil der Mailadresse in Anführungszeichen und mit Sonderzeichen haben. (!13757)
\ No newline at end of file
+- Beim Verfassen einer neuen Email können jetzt auch Empfängeradressen hinzugefügt werden, die den ersten Teil der Mailadresse in Anführungszeichen und mit Sonderzeichen haben. (!13757)
-- 
GitLab


From d5cd744fc0f0b1289044e628cabd22ac5501237e Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 15:12:17 +0000
Subject: [PATCH 078/121] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Anton Ballmaier <aballmaier@posteo.de>
---
 release-notes/2024-12.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/release-notes/2024-12.md b/release-notes/2024-12.md
index 0ae91dd599..44fd14226a 100644
--- a/release-notes/2024-12.md
+++ b/release-notes/2024-12.md
@@ -40,7 +40,7 @@ Die Themes könnt ihr im Profilmenü neben der Sprachauswahl finden.
 ##### Ausweis-Seite
 - Die verbleibende Gültigkeitsdauer des Ausweises wird in Tagen und als Datum angezeigt. 
 - Hinweis-Text: "Deine Botschafter:innen können Dir den Ausweis verlängern."
-- Der Benutzer kann den Ausweis nur herunterladen, wenn er aktiviert oder gültig ist.
+- Der Benutzer kann den Ausweis nur herunterladen, wenn er aktiviert und gültig ist.
 
 ##### Dashboard
 - Ab 30 Tagen vor Ablauf wird ein Fehler-Container mit dem Hinweis "Dein Ausweis ist bald nicht mehr gültig" angezeigt. 
-- 
GitLab


From 0a6556ec5b0ea5e01f8fefa14118595d7d5a84d6 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 16:19:54 +0100
Subject: [PATCH 079/121] use insertMultiple

---
 .../PassportGenerator/PassportGeneratorGateway.php   | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorGateway.php b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
index aae34f872c..97db081868 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorGateway.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
@@ -15,19 +15,17 @@ final class PassportGeneratorGateway extends BaseGateway
 
     public function logPassGeneration(int $generatedUserId, array $userIds): int
     {
-        $rowsInserted = 0;
         $now = $this->db->now();
 
-        foreach ($userIds as $userId) {
-            $this->db->insert('fs_pass_gen', [
+        $data = array_map(function($userId) use ($generatedUserId, $now) {
+            return [
                 'foodsaver_id' => $userId,
                 'date' => $now,
                 'bot_id' => $generatedUserId,
-            ]);
-            ++$rowsInserted;
-        }
+            ];
+        }, $userIds);
 
-        return $rowsInserted;
+        return $this->db->insertMultiple('fs_pass_gen', $data);
     }
 
     public function updateFoodsaverLastPassDate(array $foodsaver): int
-- 
GitLab


From e308134ab445e2b0ac50de971f84a61c21b88e9f Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 16:23:51 +0100
Subject: [PATCH 080/121] removed comment line

---
 src/Lib/GoogleWalletPass.php | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index 3d262b8839..261ca77e32 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -434,8 +434,6 @@ class GoogleWalletPass
             $this->service->genericobject->get("{$issuerId}.{$userId}");
         } catch (Exception $ex) {
             if (!empty($ex->getErrors()) && $ex->getErrors()[0]['reason'] == 'resourceNotFound') {
-                // echo "Object {$issuerId}.{$userId} not found!";
-
                 return "{$issuerId}.{$userId}";
             } else {
                 // Something else went wrong...
-- 
GitLab


From 6d9fe08034c1912b21d399e38cde4c3f1cb6be74 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 16:24:51 +0100
Subject: [PATCH 081/121] code style

---
 src/Modules/PassportGenerator/PassportGeneratorGateway.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorGateway.php b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
index 97db081868..a7ac10d9af 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorGateway.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
@@ -17,7 +17,7 @@ final class PassportGeneratorGateway extends BaseGateway
     {
         $now = $this->db->now();
 
-        $data = array_map(function($userId) use ($generatedUserId, $now) {
+        $data = array_map(function ($userId) use ($generatedUserId, $now) {
             return [
                 'foodsaver_id' => $userId,
                 'date' => $now,
-- 
GitLab


From 689cfc465ae62956841e2261cb0e0a6fec8429b9 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 10 Nov 2024 16:28:26 +0100
Subject: [PATCH 082/121] use const in user.js

---
 client/src/stores/user.js | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index 1c47ecbc62..ad13fad534 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -56,10 +56,10 @@ export const useUserStore = defineStore('user', {
     hasBouncingEmail: () => false,
     hasActiveEmail: () => true,
     isPassportInvalid: (state) => {
-      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 0) : false
+      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= PASSPORT_STATUS.INVALID) : false
     },
     isPassportInvalidSoon: (state) => {
-      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= 30) : false
+      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= PASSPORT_STATUS.INVALID_SOON) : false
     },
   },
   actions: {
@@ -107,3 +107,8 @@ export const PASSPORT_FILTER_OPTIONS = Object.freeze({
   WITH_PASSPORT: 2,
   INVALID_PASSPORT: 3,
 })
+
+export const PASSPORT_STATUS = Object.freeze({
+  INVALID: 0,
+  INVALID_SOON: 30,
+})
-- 
GitLab


From b9fbd27bf57551968e0ad197d47cd48724017f9e Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 18:52:47 +0100
Subject: [PATCH 083/121] use userIds

---
 src/Modules/PassportGenerator/PassportGeneratorGateway.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorGateway.php b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
index a7ac10d9af..802d72e4a2 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorGateway.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorGateway.php
@@ -28,9 +28,9 @@ final class PassportGeneratorGateway extends BaseGateway
         return $this->db->insertMultiple('fs_pass_gen', $data);
     }
 
-    public function updateFoodsaverLastPassDate(array $foodsaver): int
+    public function updateFoodsaverLastPassDate(array $userIds): int
     {
-        return $this->db->update('fs_foodsaver', ['last_pass' => $this->db->now()], ['id' => $foodsaver]);
+        return $this->db->update('fs_foodsaver', ['last_pass' => $this->db->now()], ['id' => $userIds]);
     }
 
     public function getFoodsaverLastPassDate(int $fsId): ?\DateTime
-- 
GitLab


From 46929685d82716fc627b6fa32606d5c2b3afb80c Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 18:53:54 +0100
Subject: [PATCH 084/121] change to $automaticPaperSize

---
 .../PassportGenerator/PassportGeneratorTransaction.php        | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 7822361d7a..575dea8cb0 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -48,9 +48,9 @@ class PassportGeneratorTransaction extends AbstractController
     ) {
     }
 
-    private function setupPdfMargins(\TCPDF $pdf, array $userIds, bool $automatic_paper_size): array
+    private function setupPdfMargins(\TCPDF $pdf, array $userIds, bool $automaticPaperSize): array
     {
-        $singleUser = $automatic_paper_size && count($userIds) === 1;
+        $singleUser = $automaticPaperSize && count($userIds) === 1;
 
         $pdf->AddPage(
             $singleUser ? 'L' : 'P',
-- 
GitLab


From 295bd43719a61185094fbe36fd52c9db338ec6bf Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 18:57:25 +0100
Subject: [PATCH 085/121] merge code in setupPdfMargins

---
 .../PassportGeneratorTransaction.php          | 91 ++++++-------------
 1 file changed, 30 insertions(+), 61 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 575dea8cb0..b62a3cd242 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -60,68 +60,37 @@ class PassportGeneratorTransaction extends AbstractController
         $pdf->SetAutoPageBreak(false, 0);
         $pdf->SetMargins(0, 0, 0, true);
 
-        $backgroundMarginX = $singleUser ? 0 : 10;
-        $backgroundMarginY = $singleUser ? 0 : 10;
-        $cellMarginX = 40;
-        $cellMarginY = $singleUser ? 3.2 : 13.2;
-        $idLabelMarginX = $singleUser ? 40 : 50;
-        $idLabelMarginY = 5;
-        $logoMarginX = $singleUser ? 3.5 : 13.5;
-        $logoMarginY = $singleUser ? 3.6 : 13.6;
-        $photoMarginX = $singleUser ? 4 : 14;
-        $photoMarginY = $singleUser ? 19.7 : 31;
-        $nameMaxWidthMarginX = $singleUser ? 31 : 41;
-        $nameMaxWidthMarginY = $singleUser ? 20 : 30;
-        $nameLabelMarginX = $singleUser ? 31 : 41;
-        $nameLabelMarginY = $singleUser ? 20 : 28;
-        $nameMarginX = $singleUser ? 31 : 41;
-        $nameMarginY = $singleUser ? 22 : 30.2;
-        $roleLabelMarginX = $singleUser ? 31 : 41;
-        $roleLabelMarginY = $singleUser ? 27 : 37;
-        $roleMarginX = $singleUser ? 31 : 41;
-        $roleMarginY = $singleUser ? 29 : 39;
-        $validTillLabelMarginX = $singleUser ? 31 : 41;
-        $validTillLabelMarginY = $singleUser ? 45 : 55;
-        $validTillMarginX = $singleUser ? 31 : 41;
-        $validTillMarginY = $singleUser ? 47 : 57;
-        $validDownLabelMarginX = $singleUser ? 31 : 41;
-        $validDownLabelMarginY = $singleUser ? 36 : 46;
-        $validDownMarginX = $singleUser ? 31 : 41;
-        $validDownMarginY = $singleUser ? 38 : 48;
-        $qrCodeMarginX = $singleUser ? 60 : 70.5;
-        $qrCodeMarginY = $singleUser ? 33 : 43;
-
         return [
-            'backgroundMarginX' => $backgroundMarginX,
-            'backgroundMarginY' => $backgroundMarginY,
-            'cellMarginX' => $cellMarginX,
-            'cellMarginY' => $cellMarginY,
-            'idLabelMarginX' => $idLabelMarginX,
-            'idLabelMarginY' => $idLabelMarginY,
-            'logoMarginX' => $logoMarginX,
-            'logoMarginY' => $logoMarginY,
-            'photoMarginX' => $photoMarginX,
-            'photoMarginY' => $photoMarginY,
-            'nameMaxWidthMarginX' => $nameMaxWidthMarginX,
-            'nameMaxWidthMarginY' => $nameMaxWidthMarginY,
-            'nameLabelMarginX' => $nameLabelMarginX,
-            'nameLabelMarginY' => $nameLabelMarginY,
-            'nameMarginX' => $nameMarginX,
-            'nameMarginY' => $nameMarginY,
-            'roleLabelMarginX' => $roleLabelMarginX,
-            'roleLabelMarginY' => $roleLabelMarginY,
-            'roleMarginX' => $roleMarginX,
-            'roleMarginY' => $roleMarginY,
-            'validTillLabelMarginX' => $validTillLabelMarginX,
-            'validTillLabelMarginY' => $validTillLabelMarginY,
-            'validTillMarginX' => $validTillMarginX,
-            'validTillMarginY' => $validTillMarginY,
-            'validDownLabelMarginX' => $validDownLabelMarginX,
-            'validDownLabelMarginY' => $validDownLabelMarginY,
-            'validDownMarginX' => $validDownMarginX,
-            'validDownMarginY' => $validDownMarginY,
-            'qrCodeMarginX' => $qrCodeMarginX,
-            'qrCodeMarginY' => $qrCodeMarginY,
+            'backgroundMarginX' => $singleUser ? 0 : 10,
+            'backgroundMarginY' => $singleUser ? 0 : 10,
+            'cellMarginX' => 40,
+            'cellMarginY' => $singleUser ? 3.2 : 13.2,
+            'idLabelMarginX' => $singleUser ? 40 : 50,
+            'idLabelMarginY' => 5,
+            'logoMarginX' => $singleUser ? 3.5 : 13.5,
+            'logoMarginY' => $singleUser ? 3.6 : 13.6,
+            'photoMarginX' => $singleUser ? 4 : 14,
+            'photoMarginY' => $singleUser ? 19.7 : 31,
+            'nameMaxWidthMarginX' => $singleUser ? 31 : 41,
+            'nameMaxWidthMarginY' => $singleUser ? 20 : 30,
+            'nameLabelMarginX' => $singleUser ? 31 : 41,
+            'nameLabelMarginY' => $singleUser ? 20 : 28,
+            'nameMarginX' => $singleUser ? 31 : 41,
+            'nameMarginY' => $singleUser ? 22 : 30.2,
+            'roleLabelMarginX' => $singleUser ? 31 : 41,
+            'roleLabelMarginY' => $singleUser ? 27 : 37,
+            'roleMarginX' => $singleUser ? 31 : 41,
+            'roleMarginY' => $singleUser ? 29 : 39,
+            'validTillLabelMarginX' => $singleUser ? 31 : 41,
+            'validTillLabelMarginY' => $singleUser ? 45 : 55,
+            'validTillMarginX' => $singleUser ? 31 : 41,
+            'validTillMarginY' => $singleUser ? 47 : 57,
+            'validDownLabelMarginX' => $singleUser ? 31 : 41,
+            'validDownLabelMarginY' => $singleUser ? 36 : 46,
+            'validDownMarginX' => $singleUser ? 31 : 41,
+            'validDownMarginY' => $singleUser ? 38 : 48,
+            'qrCodeMarginX' => $singleUser ? 60 : 70.5,
+            'qrCodeMarginY' => $singleUser ? 33 : 43,
         ];
     }
 
-- 
GitLab


From 2d24952cedb159fd421a1a51f742ba73f7aa17da Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 19:27:15 +0100
Subject: [PATCH 086/121] set passportMember.length

---
 src/Modules/Region/components/MemberList.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 4d4f259eb6..216c76c3ac 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -61,7 +61,7 @@
       <b-tab v-if="!isWorkGroup && mayEditMembers" :title="$i18n('group.member_list.passports.title')">
         <div class="d-flex justify-content-between">
           <b-button
-            :disabled="passportMember <= 0"
+            :disabled="passportMember.length <= 0"
             variant="outline-primary"
             size="sm"
             @click="verifySelectedMember"
@@ -95,7 +95,7 @@
               {{ $i18n('group.member_list.passports.active_or_renew_passport') }}
             </b-form-checkbox>
             <b-button
-              :disabled="passportMember <= 0 || !(isCreatePdf || isRenewPassport)"
+              :disabled="passportMember.length <= 0 || !(isCreatePdf || isRenewPassport)"
               variant="outline-primary"
               size="sm"
               @click="createPassports"
-- 
GitLab


From 3160eca3d90e4d04031ee10cf437b1cc219b61b8 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 19:53:51 +0100
Subject: [PATCH 087/121] INVALID_SOON_WARNING_TIME

---
 client/src/stores/user.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/client/src/stores/user.js b/client/src/stores/user.js
index ad13fad534..85c0a93a86 100644
--- a/client/src/stores/user.js
+++ b/client/src/stores/user.js
@@ -59,7 +59,7 @@ export const useUserStore = defineStore('user', {
       return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= PASSPORT_STATUS.INVALID) : false
     },
     isPassportInvalidSoon: (state) => {
-      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= PASSPORT_STATUS.INVALID_SOON) : false
+      return state.details.lastPassUntilValid ? (state.details.lastPassUntilValidInDays <= PASSPORT_STATUS.INVALID_SOON_WARNING_TIME) : false
     },
   },
   actions: {
@@ -110,5 +110,5 @@ export const PASSPORT_FILTER_OPTIONS = Object.freeze({
 
 export const PASSPORT_STATUS = Object.freeze({
   INVALID: 0,
-  INVALID_SOON: 30,
+  INVALID_SOON_WARNING_TIME: 30,
 })
-- 
GitLab


From 7a1f470caf1c7066756e65146393373e71aea74d Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 19:58:41 +0100
Subject: [PATCH 088/121] passportFilterOptions

---
 src/Modules/Region/components/MemberList.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 216c76c3ac..f74346978e 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -157,7 +157,7 @@
                 <label class="mb-1">{{ $i18n('group.member_list.passports.show_only_member_after') }}</label>
                 <b-form-select
                   v-model="filterPassportUntilValid"
-                  :options="filterPassportUntilValidOptions"
+                  :options="passportFilterOptions"
                   size="sm"
                   class="mb-2"
                 />
@@ -389,7 +389,7 @@ export default {
       mayRemoveAdminOrAmbassador: false,
       isCreatePdf: true,
       isRenewPassport: true,
-      filterPassportUntilValidOptions: [
+      passportFilterOptions: [
         { text: i18n('group.member_list.passports.filter_options.no_filter'), value: PASSPORT_FILTER_OPTIONS.NO_FILTER },
         { text: i18n('group.member_list.passports.filter_options.no_passport'), value: PASSPORT_FILTER_OPTIONS.NO_PASSPORT },
         { text: i18n('group.member_list.passports.filter_options.with_passport'), value: PASSPORT_FILTER_OPTIONS.WITH_PASSPORT },
-- 
GitLab


From 5f5f48f4b06ed01243e92602166e2415335ddefa Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 21:31:28 +0100
Subject: [PATCH 089/121] activate paper size checkbox if is isCreatePdf also
 true

---
 src/Modules/Region/components/MemberList.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index f74346978e..249b308da3 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -74,7 +74,7 @@
               v-model="automaticPaperSize"
               class="ml-2"
               size="sm"
-              :disabled="passportMember.length !== 1"
+              :disabled="!(isCreatePdf && passportMember.length === 1)"
             >
               {{ $i18n('group.member_list.passports.automatic_paper_size') }}
             </b-form-checkbox>
-- 
GitLab


From 8c8e43c987b19b4674c1095894169958d73df625 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 21:38:26 +0100
Subject: [PATCH 090/121] changed automaticPaperSize to usePaperSizeDinA4

---
 client/src/api/verification.js                              | 4 ++--
 .../PassportGenerator/PassportGeneratorTransaction.php      | 6 +++---
 src/Modules/Region/components/MemberList.vue                | 6 +++---
 src/RestApi/Models/Passport/CreateRegionPassportModel.php   | 2 +-
 translations/messages.de.yml                                | 2 +-
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/client/src/api/verification.js b/client/src/api/verification.js
index 853273ce1d..4025e22d83 100644
--- a/client/src/api/verification.js
+++ b/client/src/api/verification.js
@@ -20,7 +20,7 @@ export async function createPassportAsUser () {
   return await post('/user/current/passport', {}, { responseType: 'blob' })
 }
 
-export async function createPassportAsAmbassador (regionId, userIds, createPdf, renew, automaticPaperSize) {
+export async function createPassportAsAmbassador (regionId, userIds, createPdf, renew, usePaperSizeDinA4) {
   const options = createPdf ? { responseType: 'blob' } : {}
-  return await post(`/region/${regionId}/passport`, { userIds, createPdf, renew, automaticPaperSize }, options)
+  return await post(`/region/${regionId}/passport`, { userIds, createPdf, renew, usePaperSizeDinA4 }, options)
 }
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index b62a3cd242..15120d07fc 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -48,9 +48,9 @@ class PassportGeneratorTransaction extends AbstractController
     ) {
     }
 
-    private function setupPdfMargins(\TCPDF $pdf, array $userIds, bool $automaticPaperSize): array
+    private function setupPdfMargins(\TCPDF $pdf, array $userIds, bool $usePaperSizeDinA4): array
     {
-        $singleUser = $automaticPaperSize && count($userIds) === 1;
+        $singleUser = $usePaperSizeDinA4 && count($userIds) === 1;
 
         $pdf->AddPage(
             $singleUser ? 'L' : 'P',
@@ -298,7 +298,7 @@ class PassportGeneratorTransaction extends AbstractController
 
         if ($regionPassportModel->createPdf) {
             $validDates = $this->calculateValidDates();
-            $result = $this->generatePdf($regionPassportModel->userIds, true, $regionPassportModel->automaticPaperSize, $validDates);
+            $result = $this->generatePdf($regionPassportModel->userIds, true, $regionPassportModel->usePaperSizeDinA4, $validDates);
         }
 
         $userIds = $result->pdfGeneratedUserIds ?? $regionPassportModel->userIds;
diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 249b308da3..dc9c7cc589 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -71,7 +71,7 @@
 
           <div class="d-flex align-items-sm-baseline align-items-stretch justify-content-end">
             <b-form-checkbox
-              v-model="automaticPaperSize"
+              v-model="usePaperSizeDinA4"
               class="ml-2"
               size="sm"
               :disabled="!(isCreatePdf && passportMember.length === 1)"
@@ -381,7 +381,7 @@ export default {
       passportMember: [],
       filterPassportMember: false,
       filterPassportUntilValid: null,
-      automaticPaperSize: true,
+      usePaperSizeDinA4: true,
       activeTab: null,
       sortBy: '',
       mayEditMembers: false,
@@ -825,7 +825,7 @@ export default {
     async createPassports () {
       showLoader()
       try {
-        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.isCreatePdf, this.isRenewPassport, this.automaticPaperSize)
+        const response = await createPassportAsAmbassador(this.regionId, this.passportMember, this.isCreatePdf, this.isRenewPassport, this.usePaperSizeDinA4)
         if (this.isCreatePdf) {
           const filename = `fs_passports_${this.regionId}_${this.regionName}.pdf`
           this.downloadFile(response, filename)
diff --git a/src/RestApi/Models/Passport/CreateRegionPassportModel.php b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
index bf2c1b0550..e7ff020eea 100644
--- a/src/RestApi/Models/Passport/CreateRegionPassportModel.php
+++ b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
@@ -51,5 +51,5 @@ class CreateRegionPassportModel
      * @Assert\NotNull()
      * @Assert\Type("boolean")
      */
-    public bool $automaticPaperSize;
+    public bool $usePaperSizeDinA4;
 }
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index b5785534d7..06600e3ced 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -582,7 +582,7 @@ group:
       created_at: "Ausweis erstellt am"
       generate_button: "Ausweis/e für markierte erstellen"
       verify_selected: "markierte verifizieren"
-      automatic_paper_size: "Papiergröße automatisch"
+      automatic_paper_size: "A4-Papier nutzen"
       create_pdf: "Erstelle PDF"
       active_or_renew_passport: "Ausweis aktivieren / verlängern"
       execute: "Ausführen"
-- 
GitLab


From 5c6a950a31ebd9ef2599bb31f12f43393be6ea4f Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 21:41:37 +0100
Subject: [PATCH 091/121] changed passUntilValid to passportValidUntilDate

---
 src/Modules/Region/components/MemberList.vue | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index dc9c7cc589..bfb06eb4f6 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -243,11 +243,13 @@
           }) }}
         </template>
         <template #cell(passUntilValid)="row">
-          {{ row.item.lastPassDate === null ? '' : $dateFormatter.format(passUntilValid(row.item.lastPassDate), {
-            day: 'numeric',
-            month: 'numeric',
-            year: 'numeric',
-          }) }}
+          {{
+            row.item.lastPassDate === null ? '' : $dateFormatter.format(passportValidUntilDate(row.item.lastPassDate), {
+              day: 'numeric',
+              month: 'numeric',
+              year: 'numeric',
+            })
+          }}
         </template>
         <template #cell(lastActivity)="row">
           {{ $dateFormatter.format(row.item.lastActivity, {
@@ -641,7 +643,7 @@ export default {
         this.passportMember = []
       }
     },
-    passUntilValid (creationDate) {
+    passportValidUntilDate (creationDate) {
       const validUntil = new Date(creationDate)
       validUntil.setFullYear(validUntil.getFullYear() + 3)
       return validUntil
@@ -649,7 +651,7 @@ export default {
     passportValid (creationDate) {
       if (creationDate === null) { return true }
       const today = new Date()
-      const validUntil = this.passUntilValid(creationDate)
+      const validUntil = this.passportValidUntilDate(creationDate)
       return today <= validUntil
     },
     isNullOrEmptyOrWhitespace (str) {
-- 
GitLab


From 72425cc57dc77c7ec1e1aeb415a5bfbd6e213fb8 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 21:43:17 +0100
Subject: [PATCH 092/121] changed passportValid to isPassportValid

---
 src/Modules/Region/components/MemberList.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index bfb06eb4f6..c09a03e963 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -468,7 +468,7 @@ export default {
           return false
         }
 
-        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === PASSPORT_FILTER_OPTIONS.INVALID_PASSPORT && this.passportValid(member.lastPassDate)) {
+        if (this.activeTab === this.ACTIVE_TAB_PASSPORT && this.filterPassportUntilValid === PASSPORT_FILTER_OPTIONS.INVALID_PASSPORT && this.isPassportValid(member.lastPassDate)) {
           return false
         }
 
@@ -648,7 +648,7 @@ export default {
       validUntil.setFullYear(validUntil.getFullYear() + 3)
       return validUntil
     },
-    passportValid (creationDate) {
+    isPassportValid (creationDate) {
       if (creationDate === null) { return true }
       const today = new Date()
       const validUntil = this.passportValidUntilDate(creationDate)
-- 
GitLab


From 94f4b7c43414556da15af87c9fa4c9127be551bf Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 21:36:26 +0000
Subject: [PATCH 093/121] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Anton Ballmaier <aballmaier@posteo.de>
---
 src/Modules/Region/components/MemberList.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index c09a03e963..97b27135b5 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -816,7 +816,7 @@ export default {
         for (const memberId of this.passportMember) {
           const existingMember = regionStore.memberList.find(entry => entry.id === memberId)
 
-          if (!existingMember || !existingMember.isVerified) {
+          if (!existingMember?.isVerified) {
             await verifyUser(memberId)
           }
         }
-- 
GitLab


From 9f51c48e943168819c82d3c297aa6870b6f6c0d3 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Mon, 11 Nov 2024 22:40:09 +0100
Subject: [PATCH 094/121] moved to php attributes

---
 .../Passport/CreateRegionPassportModel.php    | 39 ++++++-------------
 1 file changed, 12 insertions(+), 27 deletions(-)

diff --git a/src/RestApi/Models/Passport/CreateRegionPassportModel.php b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
index e7ff020eea..364c857896 100644
--- a/src/RestApi/Models/Passport/CreateRegionPassportModel.php
+++ b/src/RestApi/Models/Passport/CreateRegionPassportModel.php
@@ -3,7 +3,7 @@
 namespace Foodsharing\RestApi\Models\Passport;
 
 use JMS\Serializer\Annotation\Type;
-use OpenApi\Annotations as OA;
+use OpenApi\Attributes as OA;
 use Symfony\Component\Validator\Constraints as Assert;
 
 /**
@@ -12,44 +12,29 @@ use Symfony\Component\Validator\Constraints as Assert;
  * This class contains the user IDs for which a region passport should be generated.
  * The data is provided in a format in which it is sent to the client.
  */
+#[OA\Schema(description: 'Data for creating a region passport')]
 class CreateRegionPassportModel
 {
     /**
      * Users for passport generation as array.
-     *
-     * @OA\Property(type="array", description="Users for passport generation",	items={"type"="integer"})
      */
+    #[OA\Property(description: 'Users for passport generation', type: 'array', items: new OA\Items(type: 'integer'))]
     #[Assert\All(new Assert\Positive())]
     #[Type('array<int>')]
     public array $userIds = [];
 
-    /**
-     * @OA\Property(
-     *     type="boolean",
-     *     description="Flag to create PDF"
-     * )
-     * @Assert\Type("boolean")
-     */
+    #[OA\Property(description: 'Flag to create PDF', type: 'boolean')]
+    #[Assert\Type('boolean')]
     public ?bool $createPdf = null;
 
-    /**
-     * @OA\Property(
-     *     type="boolean",
-     *     description="Flag to renew the passport"
-     * )
-     * @Assert\NotNull()
-     * @Assert\Type("boolean")
-     */
+    #[OA\Property(description: 'Flag to renew the passport', type: 'boolean')]
+    #[Assert\NotNull]
+    #[Assert\Type('boolean')]
     public bool $renew;
 
-    /**
-     * @OA\Property(
-     *     type="boolean",
-     *     description="Flag for automatic paper selection. If true, passport size is used for a single passport,
-     *     DIN A4 for multiple. If false, DIN A4 is always used."
-     * )
-     * @Assert\NotNull()
-     * @Assert\Type("boolean")
-     */
+    #[OA\Property(description: 'Flag for automatic paper selection. If true, passport size is used for a single passport,
+    DIN A4 for multiple. If false, DIN A4 is always used.', type: 'boolean')]
+    #[Assert\NotNull]
+    #[Assert\Type('boolean')]
     public bool $usePaperSizeDinA4;
 }
-- 
GitLab


From 49e314124e82b4e58deb895ca32ed71599de4fcb Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 17 Nov 2024 11:37:08 +0100
Subject: [PATCH 095/121] link_1 to link

---
 client/src/components/Banners/Errors/ErrorContainer.vue | 4 ++--
 translations/messages.de.yml                            | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/client/src/components/Banners/Errors/ErrorContainer.vue b/client/src/components/Banners/Errors/ErrorContainer.vue
index 583c330c59..96cba07a21 100644
--- a/client/src/components/Banners/Errors/ErrorContainer.vue
+++ b/client/src/components/Banners/Errors/ErrorContainer.vue
@@ -122,7 +122,7 @@ export default {
         list.push({
           field: 'passport_is_invalid',
           links: [{
-            text: 'error.passport_is_invalid.link_1',
+            text: 'error.passport_is_invalid.link',
             urlShorthand: 'settings',
           },
           ],
@@ -131,7 +131,7 @@ export default {
         list.push({
           field: 'passport_is_invalid_soon',
           links: [{
-            text: 'error.passport_is_invalid_soon.link_1',
+            text: 'error.passport_is_invalid_soon.link',
             urlShorthand: 'settings',
           },
           ],
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index 36008b824d..53661c7198 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -3361,11 +3361,11 @@ error:
   passport_is_invalid:
     title: "Dein Ausweis ist nicht mehr gültig!"
     description: "Wende Dich an Deine Botschafter:innen, um ihn erneuern zu lassen."
-    link_1: "Profil-Einstellungen"
+    link: "Profil-Einstellungen"
   passport_is_invalid_soon:
     title: "Dein Ausweis ist bald nicht mehr gültig!"
     description: "Wende Dich an Deine Botschafter:innen, um ihn erneuern zu lassen."
-    link_1: "Profil-Einstellungen"
+    link: "Profil-Einstellungen"
 
 information:
   push:
-- 
GitLab


From 6c798dadba0a7f0cce85163c3b0bf2704bd6a14c Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 17 Nov 2024 11:38:45 +0100
Subject: [PATCH 096/121] changed to passportValidityEndDate

---
 src/RestApi/UserRestController.php | 2 +-
 src/Utility/TimeHelper.php         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index 4fd9437614..f5793108cc 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -162,7 +162,7 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['sleeping'] = boolval($data['sleep_status']);
             $response['lastPassDate'] = $data['last_pass'];
             $response['lastPassUntilValid'] = isset($data['last_pass'])
-                ? $this->timeHelper->passportDatePlusThreeYears($data['last_pass'])
+                ? $this->timeHelper->passportValidityEndDate($data['last_pass'])
                 : null;
             $response['lastPassUntilValidInDays'] = isset($data['last_pass'])
                 ? $this->timeHelper->passportValidDays($data['last_pass'])
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index 0ec5a4734e..e8703577d9 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -115,7 +115,7 @@ final class TimeHelper
         return $this->passportValidDays($lastPassDateString) >= 1;
     }
 
-    public function passportDatePlusThreeYears($lastPassDateString): DateTime|false
+    public function passportValidityEndDate($lastPassDateString): DateTime|false
     {
         if (empty($lastPassDateString)) {
             throw new BadRequestHttpException('missing date');
-- 
GitLab


From 570abcc8bdfbaa2daef527b1b095103af9a0c39e Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 17 Nov 2024 11:43:03 +0100
Subject: [PATCH 097/121] release notes

---
 release-notes/2024-12.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/release-notes/2024-12.md b/release-notes/2024-12.md
index 4533988c06..07e0a73aca 100644
--- a/release-notes/2024-12.md
+++ b/release-notes/2024-12.md
@@ -29,7 +29,7 @@ Die Themes könnt ihr im Profilmenü neben der Sprachauswahl finden.
 #### Mitglieder-Seite
 - Tab "Ausweise" wurde zu "Ausweise & Verifizierung"
 - Ein berechnetes "Gültig bis"-Datum (Erstelldatum + 3 Jahre) wird angezeigt.
-- Es ist möglich sein, die PDF-Erstellung oder die Ausweis-Aktivierung bzw. Ausweis-Verlängerung über einen Schalter zu deaktivieren.(gespeichert im Browser).
+- Es ist jetzt möglich, einzustellen, ob man Ausweis-PDF-Erstellung und/oder Ausweis-Aktivierung bzw. -Verlängerung durchführen möchte.
 - Ein Dropdown-Filter in der Namen-/ID-Suche ermöglicht es, Benutzer mit oder ohne Ausweis zu filtern.
 - Es wurde die Möglichkeit eingebaut, die markierten Benutzer gleichzeitig zu verifizieren.
 - Der Popup-Text für die Verifizierung wurde angepasst und ein Popup für die Mehrfachbenutzer-Verifizierung hinzugefügt.
-- 
GitLab


From 8672be81ddb7e65f5e24b5128151706fe3e5de90 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 17 Nov 2024 11:44:54 +0100
Subject: [PATCH 098/121] separate alert for userStore.isVerified

---
 client/src/components/Settings/Passport.vue | 21 ++++++++++++++-------
 1 file changed, 14 insertions(+), 7 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index 9c50e09f51..a7c22f07d1 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -1,12 +1,19 @@
 <template>
   <div>
-    <b-alert :variant="userStore.isVerified ? 'info' : 'danger'" show>
-      <span v-if="userStore.isVerified">
-        {{ $i18n('settings.passport.verified_text') }}
-      </span>
-      <span v-else>
-        {{ $i18n('settings.passport.non_verified_text') }}
-      </span>
+    <b-alert
+      v-if="userStore.isVerified"
+      variant="info"
+      show
+    >
+      {{ $i18n('settings.passport.verified_text') }}
+    </b-alert>
+
+    <b-alert
+      v-if="!userStore.isVerified"
+      variant="danger"
+      show
+    >
+      {{ $i18n('settings.passport.non_verified_text') }}
     </b-alert>
 
     <!-- Alert for never activated passport -->
-- 
GitLab


From 4405d00baeedd6c58f9b1429198f40dc93df5325 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 17 Nov 2024 13:42:46 +0100
Subject: [PATCH 099/121] added isNotSetLastPassDate

---
 client/src/components/Settings/Passport.vue | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/client/src/components/Settings/Passport.vue b/client/src/components/Settings/Passport.vue
index a7c22f07d1..de4459fa8b 100644
--- a/client/src/components/Settings/Passport.vue
+++ b/client/src/components/Settings/Passport.vue
@@ -18,7 +18,7 @@
 
     <!-- Alert for never activated passport -->
     <b-alert
-      v-if="userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined"
+      v-if="isNotSetLastPassDate"
       variant="info"
       show
     >
@@ -40,7 +40,7 @@
       <span v-if="userStore.isPassportInvalid || userStore.isPassportInvalidSoon">{{ $i18n('settings.passport.ask_your_ambassadors') }}</span>
     </b-alert>
 
-    <div v-if="!userStore.isPassportInvalid && userStore.isVerified" class="d-flex flex-wrap justify-content-center">
+    <div v-if="!isNotSetLastPassDate && !userStore.isPassportInvalid && userStore.isVerified" class="d-flex flex-wrap justify-content-center">
       <CreatePDFButton
         class="m-2"
       />
@@ -72,6 +72,11 @@ onMounted(async () => {
   await userStore.fetchDetails()
 })
 
+const isNotSetLastPassDate = computed(() => {
+  return userStore.details.lastPassDate === null || userStore.details.lastPassDate === undefined
+},
+)
+
 const passportValidMessage = computed(() => {
   return i18n('settings.passport.passport_is_valid_until', {
     days: userStore.details.lastPassUntilValidInDays,
-- 
GitLab


From 8359ad1b8b17ea47ac68c07881710960e3a0bd51 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Tue, 19 Nov 2024 20:05:54 +0100
Subject: [PATCH 100/121] verification bell

---
 src/RestApi/VerificationRestController.php | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/RestApi/VerificationRestController.php b/src/RestApi/VerificationRestController.php
index 21966a4c6f..19879c278b 100644
--- a/src/RestApi/VerificationRestController.php
+++ b/src/RestApi/VerificationRestController.php
@@ -102,9 +102,11 @@ class VerificationRestController extends AbstractFoodsharingRestController
             'foodsaver_verified_title',
             'foodsaver_verified',
             'fas fa-camera',
+            [],
             ['user' => $this->session->user('name')],
             BellType::createIdentifier(BellType::FOODSAVER_VERIFIED, $userId)
         );
+
         $this->bellGateway->addBell($userId, $bellData);
 
         $fs = $this->foodsaverGateway->getFoodsaver($userId);
-- 
GitLab


From 6cda49cdd2b7adde049d60373711b9229ffbd953 Mon Sep 17 00:00:00 2001
From: Alex <alexander.simm@posteo.de>
Date: Wed, 20 Nov 2024 10:19:54 +0100
Subject: [PATCH 101/121] moved isPassportValid to
 PassportGeneratorTransaction; made passportValidDays more general

---
 .../PassportGeneratorTransaction.php          |  9 +++++-
 src/RestApi/UserRestController.php            |  3 +-
 src/Utility/TimeHelper.php                    | 29 ++++++++++++-------
 3 files changed, 29 insertions(+), 12 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 15120d07fc..ad4fcb86af 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -28,6 +28,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
 
 class PassportGeneratorTransaction extends AbstractController
 {
+    public const PASSPORT_VALIDITY_INTERVAL = '+3 years';
+
     public function __construct(
         private readonly RegionGateway $regionGateway,
         private readonly FoodsaverGateway $foodsaverGateway,
@@ -281,7 +283,7 @@ class PassportGeneratorTransaction extends AbstractController
     {
         $lastPassDate = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
 
-        if (!$this->timeHelper->isPassportValid($lastPassDate)) {
+        if (!$this->isPassportValid($lastPassDate)) {
             throw new Exception('passport is not valid');
         }
         $validDates = $this->calculateValidDates($lastPassDate);
@@ -291,6 +293,11 @@ class PassportGeneratorTransaction extends AbstractController
         return $result->pdf->Output('', 'S');
     }
 
+    private function isPassportValid(string $lastPassDateString): bool
+    {
+        return $this->timeHelper->passportValidDays($lastPassDateString) >= 1;
+    }
+
     public function generatePassportAsAmbassador(CreateRegionPassportModel $regionPassportModel): mixed
     {
         $result = new stdClass();
diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index f5793108cc..d100d79c82 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -13,6 +13,7 @@ use Foodsharing\Modules\Foodsaver\FoodsaverTransactions;
 use Foodsharing\Modules\Foodsaver\Profile;
 use Foodsharing\Modules\Group\GroupTransactions;
 use Foodsharing\Modules\Login\LoginGateway;
+use Foodsharing\Modules\PassportGenerator\PassportGeneratorTransaction;
 use Foodsharing\Modules\Profile\ProfileGateway;
 use Foodsharing\Modules\Profile\ProfileTransactions;
 use Foodsharing\Modules\Region\RegionGateway;
@@ -162,7 +163,7 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['sleeping'] = boolval($data['sleep_status']);
             $response['lastPassDate'] = $data['last_pass'];
             $response['lastPassUntilValid'] = isset($data['last_pass'])
-                ? $this->timeHelper->passportValidityEndDate($data['last_pass'])
+                ? $this->timeHelper->calculateEndDate($data['last_pass'], PassportGeneratorTransaction::PASSPORT_VALIDITY_INTERVAL)
                 : null;
             $response['lastPassUntilValidInDays'] = isset($data['last_pass'])
                 ? $this->timeHelper->passportValidDays($data['last_pass'])
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index e8703577d9..2f6801c7c8 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -4,7 +4,9 @@ namespace Foodsharing\Utility;
 
 use Carbon\Carbon;
 use DateTime;
+use DateMalformedStringException;
 use Exception;
+use Foodsharing\Modules\PassportGenerator\PassportGeneratorTransaction;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
@@ -110,22 +112,29 @@ final class TimeHelper
         return $today->diffInDays($validUntilDate);
     }
 
-    public function isPassportValid($lastPassDateString): bool
+    /**
+     * Parses a date time string and adds the specified interval. Returns the end date of the interval.
+     *
+     * @param string $dateString a date time string of the form 'Y-m-d H:i:s'
+     * @param string $addInterval any parseable interval string
+     * @return Carbon the end date
+     * @throws BadRequestHttpException if either the date string or the interval could not be parsed
+     */
+    public function calculateEndDate(string $dateString, string $addInterval): Carbon
     {
-        return $this->passportValidDays($lastPassDateString) >= 1;
-    }
-
-    public function passportValidityEndDate($lastPassDateString): DateTime|false
-    {
-        if (empty($lastPassDateString)) {
+        if (empty($dateString)) {
             throw new BadRequestHttpException('missing date');
         }
-        $lastPassDate = DateTime::createFromFormat('Y-m-d H:i:s', $lastPassDateString);
+        $parsedDate = Carbon::createFromFormat('Y-m-d H:i:s', $dateString);
 
-        if ($lastPassDate === false) {
+        if ($parsedDate === false) {
             throw new BadRequestHttpException('Invalid date format. Expected Y-m-d');
         }
 
-        return $lastPassDate->modify('+3 years');
+        $value = $parsedDate->modify($addInterval);
+        if (!$value) {
+            throw new BadRequestHttpException('Invalid interval string');
+        }
+        return $value;
     }
 }
-- 
GitLab


From b2470f8afb1e8fd6c91376f9a0dd289d8e0a2c1f Mon Sep 17 00:00:00 2001
From: Alex <alexander.simm@posteo.de>
Date: Wed, 20 Nov 2024 10:40:02 +0100
Subject: [PATCH 102/121] generalised passportValidDays

---
 .../PassportGeneratorTransaction.php            |  4 +++-
 src/RestApi/UserRestController.php              |  6 ++++--
 src/Utility/TimeHelper.php                      | 17 ++++++-----------
 3 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index ad4fcb86af..393f031968 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -295,7 +295,9 @@ class PassportGeneratorTransaction extends AbstractController
 
     private function isPassportValid(string $lastPassDateString): bool
     {
-        return $this->timeHelper->passportValidDays($lastPassDateString) >= 1;
+        $date = $this->timeHelper->calculateEndDate($lastPassDateString, self::PASSPORT_VALIDITY_INTERVAL);
+
+        return $this->timeHelper->daysInFuture($date) >= 1;
     }
 
     public function generatePassportAsAmbassador(CreateRegionPassportModel $regionPassportModel): mixed
diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index d100d79c82..07192ea056 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -154,6 +154,8 @@ class UserRestController extends AbstractFoodsharingRestController
         if ($loggedIn) {
             $infos = $this->foodsaverGateway->getFoodsaverBasics($data['id']);
 
+            $passValidityDate = $this->timeHelper->calculateEndDate($data['last_pass'], PassportGeneratorTransaction::PASSPORT_VALIDITY_INTERVAL);
+
             $response['mailboxId'] = $data['mailbox_id'];
             $response['hasCalendarToken'] = $this->settingsGateway->getApiToken($data['id']) !== null;
             $response['firstname'] = $data['name'];
@@ -163,10 +165,10 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['sleeping'] = boolval($data['sleep_status']);
             $response['lastPassDate'] = $data['last_pass'];
             $response['lastPassUntilValid'] = isset($data['last_pass'])
-                ? $this->timeHelper->calculateEndDate($data['last_pass'], PassportGeneratorTransaction::PASSPORT_VALIDITY_INTERVAL)
+                ? $passValidityDate
                 : null;
             $response['lastPassUntilValidInDays'] = isset($data['last_pass'])
-                ? $this->timeHelper->passportValidDays($data['last_pass'])
+                ? $this->timeHelper->daysInFuture($passValidityDate)
                 : null;
             $response['stats']['weight'] = floatval($infos['stat_fetchweight']);
             $response['stats']['count'] = $infos['stat_fetchcount'];
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index 2f6801c7c8..cb78ae5ae4 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -95,21 +95,16 @@ final class TimeHelper
         }
     }
 
-    public function passportValidDays($lastPassDateString): int
+    /**
+     * Returns the number of days by which the date is in the future, or 0 if the date is in the past.
+     */
+    public function daysInFuture(Carbon $date): int
     {
-        if (!isset($lastPassDateString)) {
-            throw new BadRequestHttpException('Invalid date format');
-        }
-
-        $today = Carbon::today();
-        $lastPassDate = Carbon::parse($lastPassDateString);
-        $validUntilDate = $lastPassDate->copy()->addYears(3);
-
-        if ($validUntilDate->isPast()) {
+        if ($date->isPast()) {
             return 0;
         }
 
-        return $today->diffInDays($validUntilDate);
+        return Carbon::today()->diffInDays($date);
     }
 
     /**
-- 
GitLab


From e1ee4cdf7e8645ae37f02f30bc3f89557cbebd8d Mon Sep 17 00:00:00 2001
From: Alex <alexander.simm@posteo.de>
Date: Wed, 20 Nov 2024 10:47:23 +0100
Subject: [PATCH 103/121] fix code style

---
 src/Utility/TimeHelper.php | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index cb78ae5ae4..a15adf7d60 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -3,10 +3,7 @@
 namespace Foodsharing\Utility;
 
 use Carbon\Carbon;
-use DateTime;
-use DateMalformedStringException;
 use Exception;
-use Foodsharing\Modules\PassportGenerator\PassportGeneratorTransaction;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
@@ -130,6 +127,7 @@ final class TimeHelper
         if (!$value) {
             throw new BadRequestHttpException('Invalid interval string');
         }
+
         return $value;
     }
 }
-- 
GitLab


From f5667af81aa7117ffb05e01c85a924309194b147 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Thu, 21 Nov 2024 19:06:08 +0100
Subject: [PATCH 104/121] moved wallet const from config.inc.dev.php to
 config.inc.php

---
 config.inc.dev.php | 11 -----------
 config.inc.php     | 11 +++++++++++
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/config.inc.dev.php b/config.inc.dev.php
index 8c3468b659..752337c6af 100644
--- a/config.inc.dev.php
+++ b/config.inc.dev.php
@@ -69,16 +69,5 @@ define('TWINGLE_URL', 'https://spenden.twingle.de/status/E4yxc5T7YJh7nZvL93Yu7Pl
 
 define('MAX_DELETE_OLD_ACCOUNTS_PER_DAY', 100);
 
-define('WALLET_LABEL', 'foodsharing');
-
-define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/keys/google.json');
-define('GOOGLE_WALLET_ISSUER_ID', 3388000000022365685);
-define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
-
-define('APPLE_WALLET_CERTIFICATE_PATH', __DIR__ . '/keys/apple.p12');
-define('APPLE_WALLET_CERTIFICATE_PASS', '8Kz9YxgAVFWRmqj9ZT');
-define('APPLE_WALLET_TEAM_ID', 'H97D45LYHL');
-define('APPLE_WALLET_PASS_TYPE_ID', 'pass.de.foodsharing.passport');
-
 define('ZAMMAD_URL', 'https://support.foodsharing.network/');
 define('ZAMMAD_TICKET_TOKEN', '');
diff --git a/config.inc.php b/config.inc.php
index 95c0cd4131..d3f994ce2c 100644
--- a/config.inc.php
+++ b/config.inc.php
@@ -92,3 +92,14 @@ define('DEADLOCK_QUERY_SLEEP_TIME_IN_MS', 200);
  * 2. Uncomment ll. 65-66 of this script and replace TO CHANGE AT DEPLOYMENT with the contents of public_key.txt and
  * 	private_key.txt
  */
+
+define('WALLET_LABEL', 'foodsharing');
+
+define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/keys/google.json');
+define('GOOGLE_WALLET_ISSUER_ID', 3388000000022365685);
+define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
+
+define('APPLE_WALLET_CERTIFICATE_PATH', __DIR__ . '/keys/apple.p12');
+define('APPLE_WALLET_CERTIFICATE_PASS', '8Kz9YxgAVFWRmqj9ZT');
+define('APPLE_WALLET_TEAM_ID', 'H97D45LYHL');
+define('APPLE_WALLET_PASS_TYPE_ID', 'pass.de.foodsharing.passport');
-- 
GitLab


From 8ca9deb3432c3ae6eb198242c0228d9bcf984766 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Thu, 21 Nov 2024 19:16:38 +0100
Subject: [PATCH 105/121] added a validation for google key file

---
 src/Lib/GoogleWalletPass.php | 57 +++++++++++++++++++++++++++++-------
 1 file changed, 46 insertions(+), 11 deletions(-)

diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index 261ca77e32..ddb09448b4 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -35,6 +35,7 @@ use Google\Service\Walletobjects\LocalizedString;
 use Google\Service\Walletobjects\TextModuleData;
 use Google\Service\Walletobjects\TranslatedString;
 use Google\Service\Walletobjects\Uri;
+use RuntimeException;
 use Symfony\Contracts\Translation\TranslatorInterface;
 
 /** Demo class for creating and managing passes in Google Wallet. */
@@ -72,20 +73,54 @@ class GoogleWalletPass
     /**
      * Create authenticated HTTP client using a service account file.
      */
-    public function auth()
+    public function auth(): void
     {
-        $this->credentials = new ServiceAccountCredentials(
-            Walletobjects::WALLET_OBJECT_ISSUER,
-            $this->keyFilePath
-        );
+        try {
+            if (!file_exists($this->keyFilePath)) {
+                throw new RuntimeException(
+                    "Key file not found: {$this->keyFilePath}"
+                );
+            }
+
+            if (!is_readable($this->keyFilePath)) {
+                throw new RuntimeException(
+                    "Key file is not readable. Please check file permissions: {$this->keyFilePath}"
+                );
+            }
+
+            $fileContent = file_get_contents($this->keyFilePath);
+            if (!$fileContent) {
+                throw new RuntimeException(
+                    "Unable to read key file or file is empty: {$this->keyFilePath}"
+                );
+            }
+
+            // Check if file contains valid JSON
+            json_decode($fileContent);
+            if (json_last_error() !== JSON_ERROR_NONE) {
+                throw new RuntimeException(
+                    "Key file does not contain valid JSON format: " . json_last_error_msg()
+                );
+            }
 
-        // Initialize Google Wallet API service
-        $this->client = new GoogleClient();
-        $this->client->setApplicationName(WALLET_LABEL);
-        $this->client->setScopes(Walletobjects::WALLET_OBJECT_ISSUER);
-        $this->client->setAuthConfig($this->keyFilePath);
+            $this->credentials = new ServiceAccountCredentials(
+                Walletobjects::WALLET_OBJECT_ISSUER,
+                $this->keyFilePath
+            );
 
-        $this->service = new Walletobjects($this->client);
+            // Initialize Google Wallet API service
+            $this->client = new GoogleClient();
+            $this->client->setApplicationName(WALLET_LABEL);
+            $this->client->setScopes(Walletobjects::WALLET_OBJECT_ISSUER);
+            $this->client->setAuthConfig($this->keyFilePath);
+
+            $this->service = new Walletobjects($this->client);
+
+        } catch (\Exception $e) {
+            throw new RuntimeException(
+                "Error during Google Wallet authentication: " . $e->getMessage()
+            );
+        }
     }
 
     /**
-- 
GitLab


From 4678201f76ca52a046020ace8b769d7d56714e0f Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Thu, 21 Nov 2024 19:24:11 +0100
Subject: [PATCH 106/121] fix code style

---
 src/Lib/GoogleWalletPass.php | 21 +++++----------------
 1 file changed, 5 insertions(+), 16 deletions(-)

diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index ddb09448b4..4b22472833 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -77,30 +77,22 @@ class GoogleWalletPass
     {
         try {
             if (!file_exists($this->keyFilePath)) {
-                throw new RuntimeException(
-                    "Key file not found: {$this->keyFilePath}"
-                );
+                throw new RuntimeException("Key file not found: {$this->keyFilePath}");
             }
 
             if (!is_readable($this->keyFilePath)) {
-                throw new RuntimeException(
-                    "Key file is not readable. Please check file permissions: {$this->keyFilePath}"
-                );
+                throw new RuntimeException("Key file is not readable. Please check file permissions: {$this->keyFilePath}");
             }
 
             $fileContent = file_get_contents($this->keyFilePath);
             if (!$fileContent) {
-                throw new RuntimeException(
-                    "Unable to read key file or file is empty: {$this->keyFilePath}"
-                );
+                throw new RuntimeException("Unable to read key file or file is empty: {$this->keyFilePath}");
             }
 
             // Check if file contains valid JSON
             json_decode($fileContent);
             if (json_last_error() !== JSON_ERROR_NONE) {
-                throw new RuntimeException(
-                    "Key file does not contain valid JSON format: " . json_last_error_msg()
-                );
+                throw new RuntimeException('Key file does not contain valid JSON format: ' . json_last_error_msg());
             }
 
             $this->credentials = new ServiceAccountCredentials(
@@ -115,11 +107,8 @@ class GoogleWalletPass
             $this->client->setAuthConfig($this->keyFilePath);
 
             $this->service = new Walletobjects($this->client);
-
         } catch (\Exception $e) {
-            throw new RuntimeException(
-                "Error during Google Wallet authentication: " . $e->getMessage()
-            );
+            throw new RuntimeException('Error during Google Wallet authentication: ' . $e->getMessage());
         }
     }
 
-- 
GitLab


From 7372bb590f41c9e48776223249b7917cdaf56b8d Mon Sep 17 00:00:00 2001
From: Alex <alexander.simm@posteo.de>
Date: Thu, 21 Nov 2024 20:17:51 +0100
Subject: [PATCH 107/121] fix error in PassportGeneratorTransaction

---
 .../PassportGeneratorTransaction.php             | 16 ++++++++++++----
 src/RestApi/UserRestController.php               | 11 ++++++-----
 src/Utility/TimeHelper.php                       |  5 +++--
 3 files changed, 21 insertions(+), 11 deletions(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 393f031968..340b92cda2 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -28,7 +28,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
 
 class PassportGeneratorTransaction extends AbstractController
 {
-    public const PASSPORT_VALIDITY_INTERVAL = '+3 years';
+    private const PASSPORT_VALIDITY_INTERVAL = '+3 years';
 
     public function __construct(
         private readonly RegionGateway $regionGateway,
@@ -283,7 +283,7 @@ class PassportGeneratorTransaction extends AbstractController
     {
         $lastPassDate = $this->passportGeneratorGateway->getFoodsaverLastPassDate($userId);
 
-        if (!$this->isPassportValid($lastPassDate)) {
+        if (empty($lastPassDate) || !$this->isPassportValid($lastPassDate)) {
             throw new Exception('passport is not valid');
         }
         $validDates = $this->calculateValidDates($lastPassDate);
@@ -293,13 +293,21 @@ class PassportGeneratorTransaction extends AbstractController
         return $result->pdf->Output('', 'S');
     }
 
-    private function isPassportValid(string $lastPassDateString): bool
+    private function isPassportValid(DateTime $lastPassDate): bool
     {
-        $date = $this->timeHelper->calculateEndDate($lastPassDateString, self::PASSPORT_VALIDITY_INTERVAL);
+        $date = $this->getPassportValidityEnd($lastPassDate);
 
         return $this->timeHelper->daysInFuture($date) >= 1;
     }
 
+    /**
+     * Returns the end of the validity of the passport which was created at the given date.
+     */
+    public function getPassportValidityEnd(DateTime $creationDate): DateTime
+    {
+        return Carbon::instance($creationDate)->modify(self::PASSPORT_VALIDITY_INTERVAL);
+    }
+
     public function generatePassportAsAmbassador(CreateRegionPassportModel $regionPassportModel): mixed
     {
         $result = new stdClass();
diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index 07192ea056..b9cb87525a 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -70,6 +70,7 @@ class UserRestController extends AbstractFoodsharingRestController
         private ProfileTransactions $profileTransactions,
         private FoodsaverTransactions $foodsaverTransactions,
         private SettingsGateway $settingsGateway,
+        private PassportGeneratorTransaction $passportGeneratorTransaction,
 
         private ProfilePermissions $profilePermissions,
         private QuizPermissions $quizPermissions,
@@ -154,7 +155,9 @@ class UserRestController extends AbstractFoodsharingRestController
         if ($loggedIn) {
             $infos = $this->foodsaverGateway->getFoodsaverBasics($data['id']);
 
-            $passValidityDate = $this->timeHelper->calculateEndDate($data['last_pass'], PassportGeneratorTransaction::PASSPORT_VALIDITY_INTERVAL);
+            $passValidityDate = isset($data['last_pass'])
+                ? $this->passportGeneratorTransaction->getPassportValidityEnd($data['last_pass'])
+                : null;
 
             $response['mailboxId'] = $data['mailbox_id'];
             $response['hasCalendarToken'] = $this->settingsGateway->getApiToken($data['id']) !== null;
@@ -164,10 +167,8 @@ class UserRestController extends AbstractFoodsharingRestController
             $response['photo'] = $data['photo'];
             $response['sleeping'] = boolval($data['sleep_status']);
             $response['lastPassDate'] = $data['last_pass'];
-            $response['lastPassUntilValid'] = isset($data['last_pass'])
-                ? $passValidityDate
-                : null;
-            $response['lastPassUntilValidInDays'] = isset($data['last_pass'])
+            $response['lastPassUntilValid'] = $passValidityDate;
+            $response['lastPassUntilValidInDays'] = !is_null($passValidityDate)
                 ? $this->timeHelper->daysInFuture($passValidityDate)
                 : null;
             $response['stats']['weight'] = floatval($infos['stat_fetchweight']);
diff --git a/src/Utility/TimeHelper.php b/src/Utility/TimeHelper.php
index a15adf7d60..cbb16ddca7 100644
--- a/src/Utility/TimeHelper.php
+++ b/src/Utility/TimeHelper.php
@@ -3,6 +3,7 @@
 namespace Foodsharing\Utility;
 
 use Carbon\Carbon;
+use DateTime;
 use Exception;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Contracts\Translation\TranslatorInterface;
@@ -95,9 +96,9 @@ final class TimeHelper
     /**
      * Returns the number of days by which the date is in the future, or 0 if the date is in the past.
      */
-    public function daysInFuture(Carbon $date): int
+    public function daysInFuture(DateTime $date): int
     {
-        if ($date->isPast()) {
+        if (Carbon::instance($date)->isPast()) {
             return 0;
         }
 
-- 
GitLab


From 8d713c6e92a7f9c6317696f6de37e683dd25dbc3 Mon Sep 17 00:00:00 2001
From: Alex <alexander.simm@posteo.de>
Date: Fri, 22 Nov 2024 09:26:44 +0100
Subject: [PATCH 108/121] bug fix

---
 src/RestApi/UserRestController.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index b9cb87525a..5d59904d6d 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -156,7 +156,7 @@ class UserRestController extends AbstractFoodsharingRestController
             $infos = $this->foodsaverGateway->getFoodsaverBasics($data['id']);
 
             $passValidityDate = isset($data['last_pass'])
-                ? $this->passportGeneratorTransaction->getPassportValidityEnd($data['last_pass'])
+                ? $this->passportGeneratorTransaction->getPassportValidityEnd(Carbon::parse($data['last_pass']))
                 : null;
 
             $response['mailboxId'] = $data['mailbox_id'];
-- 
GitLab


From cc5f8034a76149eb31ac22d661cb8f817ae8db8b Mon Sep 17 00:00:00 2001
From: Anton Ballmaier <aballmaier@posteo.de>
Date: Fri, 22 Nov 2024 12:38:05 +0100
Subject: [PATCH 109/121] paper size fix

---
 src/Modules/Region/components/MemberList.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index 97b27135b5..dd38283671 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -71,10 +71,10 @@
 
           <div class="d-flex align-items-sm-baseline align-items-stretch justify-content-end">
             <b-form-checkbox
+              v-if="isCreatePdf && passportMember.length === 1"
               v-model="usePaperSizeDinA4"
               class="ml-2"
               size="sm"
-              :disabled="!(isCreatePdf && passportMember.length === 1)"
             >
               {{ $i18n('group.member_list.passports.automatic_paper_size') }}
             </b-form-checkbox>
-- 
GitLab


From 9b891de5c23e7e6139e4f57f982e17ec6f6c711b Mon Sep 17 00:00:00 2001
From: Alex <alexander.simm@posteo.de>
Date: Fri, 22 Nov 2024 13:23:38 +0100
Subject: [PATCH 110/121] try to fix the serialization error

---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 340b92cda2..8c8e4853b9 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -305,7 +305,7 @@ class PassportGeneratorTransaction extends AbstractController
      */
     public function getPassportValidityEnd(DateTime $creationDate): DateTime
     {
-        return Carbon::instance($creationDate)->modify(self::PASSPORT_VALIDITY_INTERVAL);
+        return $creationDate->modify(self::PASSPORT_VALIDITY_INTERVAL);
     }
 
     public function generatePassportAsAmbassador(CreateRegionPassportModel $regionPassportModel): mixed
-- 
GitLab


From ae3f12aadcde066c6bea27991c841d3c06314c95 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 09:59:25 +0000
Subject: [PATCH 111/121] Apply 1 suggestion(s) to 1 file(s)

Co-authored-by: Anton Ballmaier <aballmaier@posteo.de>
---
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 8c8e4853b9..8c72c890f8 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -336,7 +336,7 @@ class PassportGeneratorTransaction extends AbstractController
             $bellData = Bell::create(
                 'passport_created_or_renewed_title',
                 'passport_created_or_renewed',
-                'fas fa-camera',
+                'fas fa-id-card',
                 ['href' => $passportGenLink],
                 ['user' => $this->session->user('name')],
                 BellType::createIdentifier(BellType::PASS_CREATED_OR_RENEWED, $userId)
-- 
GitLab


From 10f2e3736b8b4a01394d17d67544f6ba32eeeb16 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 11:52:15 +0100
Subject: [PATCH 112/121] added GOOGLE_WALLET_ENABLED

---
 config.inc.php                                           | 9 ++++++---
 src/Lib/GoogleWalletPass.php                             | 6 ++++--
 .../PassportGenerator/PassportGeneratorTransaction.php   | 4 +++-
 3 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/config.inc.php b/config.inc.php
index d3f994ce2c..79dca50f46 100644
--- a/config.inc.php
+++ b/config.inc.php
@@ -94,10 +94,13 @@ define('DEADLOCK_QUERY_SLEEP_TIME_IN_MS', 200);
  */
 
 define('WALLET_LABEL', 'foodsharing');
+define('GOOGLE_WALLET_ENABLED', false);
 
-define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/keys/google.json');
-define('GOOGLE_WALLET_ISSUER_ID', 3388000000022365685);
-define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
+if (GOOGLE_WALLET_ENABLED) {
+    define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/key/google.json');
+    define('GOOGLE_WALLET_ISSUER_ID', 3388000000022365685);
+    define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
+}
 
 define('APPLE_WALLET_CERTIFICATE_PATH', __DIR__ . '/keys/apple.p12');
 define('APPLE_WALLET_CERTIFICATE_PASS', '8Kz9YxgAVFWRmqj9ZT');
diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index 4b22472833..6dc8b028b5 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -65,9 +65,11 @@ class GoogleWalletPass
     public function __construct(
         private readonly TranslatorInterface $translator,
     ) {
-        $this->keyFilePath = GOOGLE_WALLET_KEY_PATH;
 
-        $this->auth();
+        if (GOOGLE_WALLET_ENABLED) {
+            $this->keyFilePath = GOOGLE_WALLET_KEY_PATH;
+            $this->auth();
+        }
     }
 
     /**
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index 8c8e4853b9..fecf94eefe 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -322,7 +322,9 @@ class PassportGeneratorTransaction extends AbstractController
         if ($regionPassportModel->renew) {
             $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
             $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
-            $this->updateGoogleWallet($userIds);
+            if (GOOGLE_WALLET_ENABLED) {
+                $this->updateGoogleWallet($userIds);
+            }
             $this->addBellAndSendPassportMail($userIds);
         }
 
-- 
GitLab


From c6e0b9aaebf147a9c05b3ca02327a90dd2be9470 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 12:10:53 +0100
Subject: [PATCH 113/121] code style

---
 src/Lib/GoogleWalletPass.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index 6dc8b028b5..17104381d8 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -65,7 +65,6 @@ class GoogleWalletPass
     public function __construct(
         private readonly TranslatorInterface $translator,
     ) {
-
         if (GOOGLE_WALLET_ENABLED) {
             $this->keyFilePath = GOOGLE_WALLET_KEY_PATH;
             $this->auth();
-- 
GitLab


From c4e0692cded215b2ba754604383cd8bef209b4e2 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 14:03:07 +0100
Subject: [PATCH 114/121] fix GOOGLE_WALLET_ENABLED

---
 config.inc.php | 9 +++------
 phpstan.neon   | 2 ++
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/config.inc.php b/config.inc.php
index 79dca50f46..d31f10189d 100644
--- a/config.inc.php
+++ b/config.inc.php
@@ -95,12 +95,9 @@ define('DEADLOCK_QUERY_SLEEP_TIME_IN_MS', 200);
 
 define('WALLET_LABEL', 'foodsharing');
 define('GOOGLE_WALLET_ENABLED', false);
-
-if (GOOGLE_WALLET_ENABLED) {
-    define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/key/google.json');
-    define('GOOGLE_WALLET_ISSUER_ID', 3388000000022365685);
-    define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
-}
+define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/key/google.json');
+define('GOOGLE_WALLET_ISSUER_ID', 3388000000022365685);
+define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
 
 define('APPLE_WALLET_CERTIFICATE_PATH', __DIR__ . '/keys/apple.p12');
 define('APPLE_WALLET_CERTIFICATE_PASS', '8Kz9YxgAVFWRmqj9ZT');
diff --git a/phpstan.neon b/phpstan.neon
index b1f888a9bc..d4d52ce147 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -8,6 +8,8 @@ parameters:
   bootstrapFiles:
     - config.inc.php
   treatPhpDocTypesAsCertain: false
+  dynamicConstantNames:
+      - GOOGLE_WALLET_ENABLED
   ignoreErrors:
     # Level 4+ see https://github.com/phpstan/phpstan/issues/3264
     # Level 4+ see https://github.com/phpstan/phpstan/issues/2889
-- 
GitLab


From 2f4555f780b0297b7337c9876e0b3bb7b13866e5 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 15:58:24 +0100
Subject: [PATCH 115/121] moved Carbon::parse to new DateTime

---
 src/RestApi/UserRestController.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index 5d59904d6d..b2f84d8450 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -4,6 +4,7 @@ namespace Foodsharing\RestApi;
 
 use Carbon\Carbon;
 use Exception;
+use DateTime;
 use Foodsharing\Lib\Session;
 use Foodsharing\Modules\Core\DBConstants\Foodsaver\Gender;
 use Foodsharing\Modules\Core\DBConstants\Foodsaver\Role;
@@ -156,7 +157,7 @@ class UserRestController extends AbstractFoodsharingRestController
             $infos = $this->foodsaverGateway->getFoodsaverBasics($data['id']);
 
             $passValidityDate = isset($data['last_pass'])
-                ? $this->passportGeneratorTransaction->getPassportValidityEnd(Carbon::parse($data['last_pass']))
+                ? $this->passportGeneratorTransaction->getPassportValidityEnd(new DateTime($data['last_pass']))
                 : null;
 
             $response['mailboxId'] = $data['mailbox_id'];
-- 
GitLab


From 1b21d5bd21bcf97dac57e44f2869d0a476d5bfc7 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 16:17:13 +0100
Subject: [PATCH 116/121] fix url for bell

---
 src/RestApi/VerificationRestController.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/RestApi/VerificationRestController.php b/src/RestApi/VerificationRestController.php
index 19879c278b..45eaf19935 100644
--- a/src/RestApi/VerificationRestController.php
+++ b/src/RestApi/VerificationRestController.php
@@ -102,7 +102,7 @@ class VerificationRestController extends AbstractFoodsharingRestController
             'foodsaver_verified_title',
             'foodsaver_verified',
             'fas fa-camera',
-            [],
+            ['href' => null],
             ['user' => $this->session->user('name')],
             BellType::createIdentifier(BellType::FOODSAVER_VERIFIED, $userId)
         );
-- 
GitLab


From e5c14661bf67da78c8ed7839c068d2eccc9ce0a4 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 16:21:50 +0100
Subject: [PATCH 117/121] added filter_options

---
 src/Modules/Region/components/MemberList.vue | 2 +-
 translations/messages.de.yml                 | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Modules/Region/components/MemberList.vue b/src/Modules/Region/components/MemberList.vue
index dd38283671..6d09c7075d 100644
--- a/src/Modules/Region/components/MemberList.vue
+++ b/src/Modules/Region/components/MemberList.vue
@@ -136,7 +136,7 @@
               <template #button-content>
                 <button
                   v-b-tooltip.hover
-                  :title="$i18n('button.clear_filter')"
+                  :title="$i18n('button.filter_options')"
                   type="button"
                   class="btn btn-sm"
                 >
diff --git a/translations/messages.de.yml b/translations/messages.de.yml
index e75b869b89..ab4ca461d4 100644
--- a/translations/messages.de.yml
+++ b/translations/messages.de.yml
@@ -376,6 +376,7 @@ button:
   answer: "Antworten"
   create: "Anlegen"
   clear_filter: "Filter leeren"
+  filter_options: "Filter-Optionen"
   reset_default: "Standard wiederherstellen"
   start: Los geht's!
   confirm: Bestätigen
-- 
GitLab


From 82d8c038ced5c32be3746d260c13e9043119f956 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sat, 23 Nov 2024 16:22:50 +0100
Subject: [PATCH 118/121] code style

---
 src/RestApi/UserRestController.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/RestApi/UserRestController.php b/src/RestApi/UserRestController.php
index b2f84d8450..dec250d7c5 100644
--- a/src/RestApi/UserRestController.php
+++ b/src/RestApi/UserRestController.php
@@ -3,8 +3,8 @@
 namespace Foodsharing\RestApi;
 
 use Carbon\Carbon;
-use Exception;
 use DateTime;
+use Exception;
 use Foodsharing\Lib\Session;
 use Foodsharing\Modules\Core\DBConstants\Foodsaver\Gender;
 use Foodsharing\Modules\Core\DBConstants\Foodsaver\Role;
-- 
GitLab


From 37e9db89b81a066dc2bcea3d0521bb35240ad5a5 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 24 Nov 2024 16:39:19 +0100
Subject: [PATCH 119/121] moved to is_numeric(GOOGLE_WALLET_ISSUER_ID)

---
 config.inc.php                                              | 6 +++---
 src/Lib/GoogleWalletPass.php                                | 2 +-
 .../PassportGenerator/PassportGeneratorTransaction.php      | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/config.inc.php b/config.inc.php
index d31f10189d..c4d2c0e189 100644
--- a/config.inc.php
+++ b/config.inc.php
@@ -94,9 +94,9 @@ define('DEADLOCK_QUERY_SLEEP_TIME_IN_MS', 200);
  */
 
 define('WALLET_LABEL', 'foodsharing');
-define('GOOGLE_WALLET_ENABLED', false);
-define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/key/google.json');
-define('GOOGLE_WALLET_ISSUER_ID', 3388000000022365685);
+define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/keys/google.json');
+// If GOOGLE_WALLET_ISSUER_ID is a string and not a numerical value, then the Google Wallet is deactivated in the backend.
+define('GOOGLE_WALLET_ISSUER_ID', 'ISSUER_ID');
 define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
 
 define('APPLE_WALLET_CERTIFICATE_PATH', __DIR__ . '/keys/apple.p12');
diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index 17104381d8..bcb5453c39 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -65,7 +65,7 @@ class GoogleWalletPass
     public function __construct(
         private readonly TranslatorInterface $translator,
     ) {
-        if (GOOGLE_WALLET_ENABLED) {
+        if (is_numeric(GOOGLE_WALLET_ISSUER_ID)) {
             $this->keyFilePath = GOOGLE_WALLET_KEY_PATH;
             $this->auth();
         }
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index b28e951a74..f62cd1b167 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -322,7 +322,7 @@ class PassportGeneratorTransaction extends AbstractController
         if ($regionPassportModel->renew) {
             $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
             $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
-            if (GOOGLE_WALLET_ENABLED) {
+            if (is_numeric(GOOGLE_WALLET_ISSUER_ID)) {
                 $this->updateGoogleWallet($userIds);
             }
             $this->addBellAndSendPassportMail($userIds);
-- 
GitLab


From 02b766bb5b53a131bf27f05a511066568fa73ceb Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 24 Nov 2024 16:44:08 +0100
Subject: [PATCH 120/121] switched to !empty(GOOGLE_WALLET_ISSUER_ID

---
 config.inc.php                                                 | 2 +-
 src/Lib/GoogleWalletPass.php                                   | 2 +-
 src/Modules/PassportGenerator/PassportGeneratorTransaction.php | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/config.inc.php b/config.inc.php
index c4d2c0e189..bfc28b9eed 100644
--- a/config.inc.php
+++ b/config.inc.php
@@ -96,7 +96,7 @@ define('DEADLOCK_QUERY_SLEEP_TIME_IN_MS', 200);
 define('WALLET_LABEL', 'foodsharing');
 define('GOOGLE_WALLET_KEY_PATH', __DIR__ . '/keys/google.json');
 // If GOOGLE_WALLET_ISSUER_ID is a string and not a numerical value, then the Google Wallet is deactivated in the backend.
-define('GOOGLE_WALLET_ISSUER_ID', 'ISSUER_ID');
+define('GOOGLE_WALLET_ISSUER_ID', '');
 define('GOOGLE_WALLET_CLASS_ID', 'foodsharing-passport');
 
 define('APPLE_WALLET_CERTIFICATE_PATH', __DIR__ . '/keys/apple.p12');
diff --git a/src/Lib/GoogleWalletPass.php b/src/Lib/GoogleWalletPass.php
index bcb5453c39..9d062c3242 100644
--- a/src/Lib/GoogleWalletPass.php
+++ b/src/Lib/GoogleWalletPass.php
@@ -65,7 +65,7 @@ class GoogleWalletPass
     public function __construct(
         private readonly TranslatorInterface $translator,
     ) {
-        if (is_numeric(GOOGLE_WALLET_ISSUER_ID)) {
+        if (!empty(GOOGLE_WALLET_ISSUER_ID)) {
             $this->keyFilePath = GOOGLE_WALLET_KEY_PATH;
             $this->auth();
         }
diff --git a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
index f62cd1b167..6ebcc61aa7 100644
--- a/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
+++ b/src/Modules/PassportGenerator/PassportGeneratorTransaction.php
@@ -322,7 +322,7 @@ class PassportGeneratorTransaction extends AbstractController
         if ($regionPassportModel->renew) {
             $this->passportGeneratorGateway->logPassGeneration($generatedUserId, $userIds);
             $this->passportGeneratorGateway->updateFoodsaverLastPassDate($userIds);
-            if (is_numeric(GOOGLE_WALLET_ISSUER_ID)) {
+            if (!empty(GOOGLE_WALLET_ISSUER_ID)) {
                 $this->updateGoogleWallet($userIds);
             }
             $this->addBellAndSendPassportMail($userIds);
-- 
GitLab


From 782699680f6d520acea05e9a8080702d477ed1d4 Mon Sep 17 00:00:00 2001
From: Christian Walgenbach <foodsharing@walgeo.de>
Date: Sun, 24 Nov 2024 16:48:05 +0100
Subject: [PATCH 121/121] fix code style

---
 phpstan.neon | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/phpstan.neon b/phpstan.neon
index d4d52ce147..33edba5bd8 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -9,7 +9,7 @@ parameters:
     - config.inc.php
   treatPhpDocTypesAsCertain: false
   dynamicConstantNames:
-      - GOOGLE_WALLET_ENABLED
+      - GOOGLE_WALLET_ISSUER_ID
   ignoreErrors:
     # Level 4+ see https://github.com/phpstan/phpstan/issues/3264
     # Level 4+ see https://github.com/phpstan/phpstan/issues/2889
-- 
GitLab