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 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 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