From ece573ed29ae37ea6d4fefafc61b41f6bd5249e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patrick=20M=C3=BCller?= <p.mueller@pcsg.de>
Date: Fri, 7 Jul 2017 12:47:09 +0200
Subject: [PATCH] refactor: temp commit (UserProfile)

---
 ajax/memberships/users/getList.php            |   2 +-
 ajax/memberships/users/getSessionUserData.php |  46 +++++
 ajax/memberships/users/update.php             |   1 +
 bin/classes/MembershipUsers.js                |  14 ++
 bin/controls/profile/UserProfile.css          |  11 ++
 bin/controls/profile/UserProfile.html         |  10 +
 bin/controls/profile/UserProfile.js           | 184 ++++++++++++++++++
 bin/membership.php                            |  71 -------
 composer.json                                 |  47 +++--
 database.xml                                  |   1 -
 intranet.xml                                  |   6 +
 locale.xml                                    | 126 ++++++++++--
 settings.xml                                  |  76 ++++++++
 .../Memberships/Users/CancelVerification.php  | 145 ++++++++++++++
 src/QUI/Memberships/Users/Handler.php         |  36 +++-
 src/QUI/Memberships/Users/MembershipUser.php  | 122 +++++++-----
 templates/mail_manualextend.html              |  11 ++
 17 files changed, 752 insertions(+), 157 deletions(-)
 create mode 100644 ajax/memberships/users/getSessionUserData.php
 create mode 100644 bin/controls/profile/UserProfile.css
 create mode 100644 bin/controls/profile/UserProfile.html
 create mode 100644 bin/controls/profile/UserProfile.js
 delete mode 100644 bin/membership.php
 create mode 100644 intranet.xml
 create mode 100644 src/QUI/Memberships/Users/CancelVerification.php
 create mode 100644 templates/mail_manualextend.html

diff --git a/ajax/memberships/users/getList.php b/ajax/memberships/users/getList.php
index c903e5f..3c917c2 100644
--- a/ajax/memberships/users/getList.php
+++ b/ajax/memberships/users/getList.php
@@ -43,7 +43,7 @@ function ($membershipId, $searchParams) {
         }
 
         /** @var \QUI\Memberships\Users\MembershipUser $TEST */
-//        $TEST = $MembershipUsers->getChild(22);
+//        $TEST = $MembershipUsers->getChild(26);
 //        $TEST->startManualCancel();
 
         $Grid = new Grid($searchParams);
diff --git a/ajax/memberships/users/getSessionUserData.php b/ajax/memberships/users/getSessionUserData.php
new file mode 100644
index 0000000..c84a472
--- /dev/null
+++ b/ajax/memberships/users/getSessionUserData.php
@@ -0,0 +1,46 @@
+<?php
+
+use QUI\Memberships\Users\Handler as MembershipUsersHandler;
+use QUI\Memberships\Users\MembershipUser;
+
+/**
+ * Get all MembershipUser Objects data for the current session user
+ *
+ * @return array - view data for all relevant MembershipUser objects
+ */
+QUI::$Ajax->registerFunction(
+    'package_quiqqer_memberships_ajax_memberships_users_getSessionUserData',
+    function () {
+        $SessionUser = QUI::getUserBySession();
+
+        try {
+            $membershipUsers = MembershipUsersHandler::getInstance()
+                ->getMembershipUsersByUserId($SessionUser->getId());
+
+            $data = array();
+
+            foreach ($membershipUsers as $MembershipUser) {
+                $data[] = $MembershipUser->getFrontendViewData();
+            }
+        } catch (\Exception $Exception) {
+            QUI\System\Log::addError('AJAX :: package_quiqqer_memberships_ajax_memberships_users_getHistory');
+            QUI\System\Log::writeException($Exception);
+
+            QUI::getMessagesHandler()->addError(
+                QUI::getLocale()->get(
+                    'quiqqer/memberships',
+                    'message.ajax.general.error',
+                    array(
+                        'error' => $Exception->getMessage()
+                    )
+                )
+            );
+
+            return array();
+        }
+
+        return $data;
+    },
+    array(),
+    'Permission::checkAdminUser'
+);
diff --git a/ajax/memberships/users/update.php b/ajax/memberships/users/update.php
index de3e2db..d9606e4 100644
--- a/ajax/memberships/users/update.php
+++ b/ajax/memberships/users/update.php
@@ -29,6 +29,7 @@ function ($membershipUserId, $attributes) {
 
                         if ($oldVal != $v) {
                             $updated[$k] = $oldVal . ' => ' . $v;
+                            $MembershipUser->sendManualExtendMail();
                         }
                         break;
 
diff --git a/bin/classes/MembershipUsers.js b/bin/classes/MembershipUsers.js
index d4cf6d6..8717452 100644
--- a/bin/classes/MembershipUsers.js
+++ b/bin/classes/MembershipUsers.js
@@ -105,6 +105,20 @@ define('package/quiqqer/memberships/bin/classes/MembershipUsers', [
             });
         },
 
+        /**
+         * Get all Membership data for the current session user
+         *
+         * @return {Promise}
+         */
+        getSessionUserData: function () {
+            return new Promise(function (resolve, reject) {
+                QUIAjax.get('package_quiqqer_memberships_ajax_memberships_users_getSessionUserData', resolve, {
+                    'package': pkg,
+                    onError  : reject
+                })
+            });
+        },
+
         /**
          * Get MembershipUser history
          *
diff --git a/bin/controls/profile/UserProfile.css b/bin/controls/profile/UserProfile.css
new file mode 100644
index 0000000..4674b95
--- /dev/null
+++ b/bin/controls/profile/UserProfile.css
@@ -0,0 +1,11 @@
+.quiqqer-memberships-profile-userprofile-status-cancelled {
+    color: red;
+}
+
+.quiqqer-memberships-profile-userprofile-status-cancelled_start {
+    color: orange;
+}
+
+.quiqqer-memberships-profile-userprofile-status-active {
+    color: green;
+}
\ No newline at end of file
diff --git a/bin/controls/profile/UserProfile.html b/bin/controls/profile/UserProfile.html
new file mode 100644
index 0000000..69ec9f6
--- /dev/null
+++ b/bin/controls/profile/UserProfile.html
@@ -0,0 +1,10 @@
+<h1>{{header}}</h1>
+<table class="quiqqer-memberships-profile-userprofile-table">
+    <thead>
+    <tr>
+        <th>{{headerMembership}}</th>
+        <th>{{headerMembershipData}}</th>
+    </tr>
+    </thead>
+    <tbody></tbody>
+</table>
\ No newline at end of file
diff --git a/bin/controls/profile/UserProfile.js b/bin/controls/profile/UserProfile.js
new file mode 100644
index 0000000..8ad2378
--- /dev/null
+++ b/bin/controls/profile/UserProfile.js
@@ -0,0 +1,184 @@
+/**
+ * UserProfile JavaScript Control
+ *
+ * View data from archived membership users
+ *
+ * @module package/quiqqer/memberships/bin/controls/profile/UserProfile
+ * @author www.pcsg.de (Patrick Müller)
+ *
+ * @require qui/controls/Control
+ * @require qui/controls/loader/Loader
+ * @require qui/controls/windows/Popup
+ * @require qui/controls/windows/Confirm
+ * @require qui/controls/buttons/Button
+ * @require utils/Controls
+ * @require controls/grid/Grid
+ * @require package/quiqqer/memberships/bin/Licenses
+ * @require package/quiqqer/memberships/bin/controls/LicenseBundles
+ * @require Locale
+ * @require Ajax
+ * @require Mustache
+ * @require text!package/quiqqer/memberships/bin/controls/profile/UserProfile.html
+ * @require css!package/quiqqer/memberships/bin/controls/profile/UserProfile.css
+ */
+define('package/quiqqer/memberships/bin/controls/profile/UserProfile', [
+
+    'qui/controls/Control',
+    'qui/controls/loader/Loader',
+    'qui/controls/windows/Popup',
+    'qui/controls/windows/Confirm',
+    'qui/controls/buttons/Button',
+
+    'utils/Controls',
+
+    'package/quiqqer/memberships/bin/Memberships',
+    'package/quiqqer/memberships/bin/MembershipUsers',
+
+    'Locale',
+    'Ajax',
+    'Mustache',
+
+    'text!package/quiqqer/memberships/bin/controls/profile/UserProfile.html',
+    'css!package/quiqqer/memberships/bin/controls/profile/UserProfile.css'
+
+], function (QUIControl, QUILoader, QUIPopup, QUIConfirm, QUIButton,
+             QUIControlUtils, Memberships, MembershipUsersHandler,
+             QUILocale, QUIAjax, Mustache, template) {
+    "use strict";
+
+    var lg = 'quiqqer/memberships';
+
+    return new Class({
+
+        Extends: QUIControl,
+        Type   : 'package/quiqqer/memberships/bin/controls/profile/UserProfile',
+
+        Binds: [
+            '$onInject',
+            '$onResize',
+            '$onCreate',
+            'refresh',
+            '$build'
+        ],
+
+        options: {
+            membershipId: false
+        },
+
+        initialize: function (options) {
+            this.parent(options);
+
+            this.Loader       = new QUILoader();
+            this.$memberships = [];
+
+            this.addEvents({
+                onCreate: this.$onCreate,
+                onInject: this.$onInject,
+                onResize: this.$onResize
+            });
+        },
+
+        /**
+         * Event: onCreate
+         */
+        $onCreate: function () {
+
+        },
+
+        /**
+         * Event: onImport
+         */
+        $onInject: function () {
+            this.$Elm.addClass('quiqqer-memberships-membershipusersarchive');
+
+            var lgPrefix = 'controls.profile.userprofile.template.';
+
+            this.$Elm.set('html', Mustache.render(template, {
+                header              : QUILocale.get(lg, lgPrefix + 'header'),
+                headerMembership    : QUILocale.get(lg, lgPrefix + 'headerMembership'),
+                headerMembershipData: QUILocale.get(lg, lgPrefix + 'headerMembershipData')
+            }));
+
+            this.Loader.inject(this.$Elm);
+
+            this.refresh();
+        },
+
+        /**
+         * event: onResize
+         */
+        $onResize: function () {
+            // @todo
+        },
+
+        /**
+         * Refresh control data
+         */
+        refresh: function () {
+            var self = this;
+
+            this.Loader.show();
+
+            MembershipUsersHandler.getSessionUserData().then(function (memberships) {
+                self.Loader.hide();
+                self.$memberships = memberships;
+                self.$build();
+            });
+        },
+
+        /**
+         * Fill table with membership data
+         */
+        $build: function () {
+            var self         = this;
+            var lgPrefix     = 'controls.profile.userprofile.datatable.';
+            var TableBodyElm = this.$Elm.getElement(
+                '.quiqqer-memberships-profile-userprofile-table tbody'
+            );
+
+            for (var i = 0, len = this.$memberships.length; i < len; i++) {
+                var Membership = this.$memberships[i];
+                var Row        = new Element('tr').inject(TableBodyElm);
+
+                // Membership title and description
+                new Element('td', {
+                    html: '<h2>' + Membership.membershipTitle + '</h2>' +
+                    '<p>' + Membership.membershipShort + '</p>'
+                }).inject(Row);
+
+                // Membership data (dates and status)
+                var status = 'active';
+
+                if (Membership.cancelled) {
+                    status = 'cancelled';
+                } else if (Membership.cancelDate) {
+                    status = 'cancelled_start';
+                }
+
+                new Element('td', {
+                    html: '<table>' +
+                    '<tr>' +
+                    '<td>' + QUILocale.get(lg, lgPrefix + 'labelAddedDate') + '</td>' +
+                    '<td>' + Membership.addedDate + '</td>' +
+                    '</tr>' +
+                    '<tr>' +
+                    '<td>' + QUILocale.get(lg, lgPrefix + 'labelStatus') + '</td>' +
+                    '<td><span class="quiqqer-memberships-profile-userprofile-status-' + status + '">'
+                    + QUILocale.get(lg, lgPrefix + 'status.' + status) + '</span></td>' +
+                    '</tr>' +
+                    '</table>'
+                }).inject(Row);
+            }
+        },
+
+        $getCancelBtn: function()
+        {
+
+        },
+
+        $getAbortCancelBtn: function()
+        {
+
+        }
+    });
+});
diff --git a/bin/membership.php b/bin/membership.php
deleted file mode 100644
index 29726c9..0000000
--- a/bin/membership.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-
-use QUI\Memberships\Users\Handler as MembershipUsersHandler;
-
-define('QUIQQER_SYSTEM', true);
-
-require dirname(__FILE__, 4) . '/header.php';
-
-/**
- * Send 401 status code if anything goes wrong
- */
-function send401()
-{
-    $Response = QUI::getGlobalResponse();
-    $Response->setStatusCode(401, 'Unauthorized or malformed request');
-    $Response->send();
-
-    exit;
-}
-
-/**
- * Cancel membership
- */
-function cancel()
-{
-    // @todo check if user is logged in?
-
-    $Engine = QUI::getTemplateManager()->getEngine();
-    $Engine->assign(array(
-        'error' => false
-    ));
-
-    try {
-        $MembershipUsers = MembershipUsersHandler::getInstance();
-        /** @var \QUI\Memberships\Users\MembershipUser $MembershipUser */
-        $MembershipUser  = $MembershipUsers->getChild((int)$_REQUEST['mid']);
-        $MembershipUser->confirmManualCancel($_REQUEST['hash']);
-    } catch (QUI\Memberships\Exception $Exception) {
-        $Engine->assign(array(
-            'error'        => true,
-            'errorMessage' => $Exception->getMessage()
-        ));
-    } catch (\Exception $Exception) {
-        send401();
-    }
-
-    $template = $Engine->fetch(dirname(__FILE__, 2) . '/templates/cancel_confirm.html');
-
-    \QUI\System\Log::writeRecursive($template);
-
-    echo $template;
-    exit;
-}
-
-if (empty($_REQUEST['mid'])
-    || empty($_REQUEST['hash'])
-    || empty($_REQUEST['action'])
-) {
-    send401();
-}
-
-switch ($_REQUEST['action']) {
-    case 'confirmManualCancel':
-        cancel();
-        break;
-
-    default:
-        send401();
-}
-
-exit;
diff --git a/composer.json b/composer.json
index 1e4c3cb..dc7ba8a 100644
--- a/composer.json
+++ b/composer.json
@@ -1,28 +1,27 @@
 {
-    "name" : "quiqqer/memberships",
-    "type" : "quiqqer-module",
-    "description" : "Mitgliedschafen für ERP",
-    "version" : "dev-dev",
-    "license" : "GPL-3.0+",
-    "authors" : [
-        {
-            "name": "Patrick Müller",
-            "email": "p.mueller@pcsg.de",
-            "homepage": "http://www.pcsg.de",
-            "role": "Developer"
-        }
-    ],
-    "support" : {
-        "email": "support@pcsg.de",
-        "url": "http://www.pcsg.de"
-    },
-    "require": {
-        
+  "name": "quiqqer/memberships",
+  "type": "quiqqer-module",
+  "description": "Mitgliedschafen für ERP",
+  "version": "dev-dev",
+  "license": "GPL-3.0+",
+  "authors": [
+    {
+      "name": "Patrick Müller",
+      "email": "p.mueller@pcsg.de",
+      "homepage": "http://www.pcsg.de",
+      "role": "Developer"
     }
-    ,"autoload": {
-  "psr-4": {
+  ],
+  "support": {
+    "email": "support@pcsg.de",
+    "url": "http://www.pcsg.de"
+  },
+  "require": {
+    "quiqqer/verification": "dev-dev"
+  },
+  "autoload": {
+    "psr-4": {
       "QUI\\Memberships\\": "src/QUI/Memberships"
-   }
-}
-
+    }
+  }
 }
diff --git a/database.xml b/database.xml
index 1c7a6f1..75cf91f 100644
--- a/database.xml
+++ b/database.xml
@@ -30,7 +30,6 @@
             <field type="TIMESTAMP NULL DEFAULT NULL">endDate</field>
             <field type="SMALLINT UNSIGNED NOT NULL DEFAULT 0">extendCounter</field>
             <field type="TIMESTAMP NULL DEFAULT NULL">cancelDate</field>
-            <field type="VARCHAR(255) NULL">cancelHash</field>
             <field type="TINYINT(1) NOT NULL DEFAULT 0">cancelled</field>
             <field type="TINYINT(1) NOT NULL DEFAULT 0">archived</field>
             <field type="TIMESTAMP NULL DEFAULT NULL">archiveDate</field>
diff --git a/intranet.xml b/intranet.xml
new file mode 100644
index 0000000..2e601af
--- /dev/null
+++ b/intranet.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<menu>
+    <item require="package/quiqqer/memberships/bin/controls/profile/UserProfile" icon="fa fa-id-card-o" name="memberships-profile">
+        <locale group="quiqqer/memberships" var="profile.button.text" />
+    </item>
+</menu>
diff --git a/locale.xml b/locale.xml
index 35e81b4..7360d07 100644
--- a/locale.xml
+++ b/locale.xml
@@ -66,12 +66,12 @@
             <en><![CDATA[Determines how the runtime of a membership is extended for a user when the user enters the membership even though he is already an active member.]]></en>
         </locale>
         <locale name="settings.extendMode.option.reset">
-            <de><![CDATA[Start- und End-Zeitpunkt werden neu gesetzt]]></de>
-            <en><![CDATA[Start and end times are reset]]></en>
+            <de><![CDATA[Start- und Endzeitpunkt werden neu gesetzt, als wäre der Benutzer neu in die Mitgliedschaft gekommen]]></de>
+            <en><![CDATA[The start and end times are reset as if the user had entered the membership]]></en>
         </locale>
         <locale name="settings.extendMode.option.prolong">
-            <de><![CDATA[Start-Zeitpunkt bleibt erhalten, nur End-Zeitpunkt wird neu gesetzt]]></de>
-            <en><![CDATA[Start time is retained, only end time is reset]]></en>
+            <de><![CDATA[Endzeitpunkt wird um die Laufzeit der Mitgliedschaft verlängert]]></de>
+            <en><![CDATA[End time is extended by the duration of the membership]]></en>
         </locale>
         <locale name="settings.durationMode.title">
             <de><![CDATA[Genauigkeit von Laufzeiten]]></de>
@@ -89,6 +89,50 @@
             <de><![CDATA[Exakt (Laufzeiten werden sekundengenau berechnet)]]></de>
             <en><![CDATA[Exact (duration is calculated by the second)]]></en>
         </locale>
+        <locale name="settings.cancelDuration.title">
+            <de><![CDATA[Gültigkeitsdauer f. Kündigungs-Bestätigungs-Link]]></de>
+            <en><![CDATA[Duration of cancellation confirmation links]]></en>
+        </locale>
+        <locale name="settings.cancelDuration.description" html="true">
+            <de><![CDATA[Bestimmt, wie lange der Link zur Bestätigung einer Mitgliedschafts-Kündigung gültig bleibt. Angabe in <b>Minuten</b>.]]></de>
+            <en><![CDATA[Determines how long the link to confirm a membership termination remains valid (<b>minutes</b>).]]></en>
+        </locale>
+        <locale name="settings.sendAutoExtendMail.title">
+            <de><![CDATA[E-Mail bei automatischer Verlängerung]]></de>
+            <en><![CDATA[Mail on auto extension]]></en>
+        </locale>
+        <locale name="settings.sendAutoExtendMail.description">
+            <de><![CDATA[Bei einer automatischen Verlängerung der Mitgliedschaft wird eine E-Mail an den Benutzer gesendet]]></de>
+            <en><![CDATA[An automatic extension of the membership will send an e-mail to the user]]></en>
+        </locale>
+        <locale name="settings.sendManualExtendMail.title">
+            <de><![CDATA[E-Mail bei manueller Verlängerung]]></de>
+            <en><![CDATA[Mail on manual extension]]></en>
+        </locale>
+        <locale name="settings.sendManualExtendMail.description">
+            <de><![CDATA[Bei einer manuellen Verlängerung der Mitgliedschaft wird eine E-Mail an den Benutzer gesendet. Manuelle Verlängerung = Bearbeitung des End-Datums einer Mitgliedschaft durch einen Administrator oder das erneute Hinzufügen eines Benutzers zu einer Mitgliedschaft, in der er sich bereits befindet.]]></de>
+            <en><![CDATA[A manual extension of the membership will send an e-mail to the user. Manual Extension = The end date of a membership is edited by an administrator or a user is re-added to a membership that he already is in.]]></en>
+        </locale>
+        <locale name="settings.dateformat.title">
+            <de><![CDATA[Datumsformatierung]]></de>
+            <en><![CDATA[Date format]]></en>
+        </locale>
+        <locale name="settings.dateFormatShort.title">
+            <de><![CDATA[Datumsformatierung (kurz)]]></de>
+            <en><![CDATA[Date format (short)]]></en>
+        </locale>
+        <locale name="settings.dateFormatShort.description" html="true">
+            <de><![CDATA[Datumsformatierung für Datumsangaben in kurzem Format. Das kurze Datumsformat wird in Nachrichten an den Benutzer verwendet, wenn die <b>Genauigkeit von Laufzeiten</b> auf <b>Tag-genau</b> gestellt ist.<br><br>Für verfügbare Platzhalter siehe <a href="http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters" target="_blank">http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters</a>]]></de>
+            <en><![CDATA[Date format for dates in short format. The short format is used in messages sent to the user if the <b>Accuracy of membership duration</b> setting is set to <b>Day-exact</b>.<br><br>For available placeholder see: <a href="http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters" target="_blank">http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters</a>]]></en>
+        </locale>
+        <locale name="settings.dateFormatLong.title">
+            <de><![CDATA[Datumsformatierung (lang)]]></de>
+            <en><![CDATA[Date format (long)]]></en>
+        </locale>
+        <locale name="settings.dateFormatLong.description" html="true">
+            <de><![CDATA[Datumsformatierung für Datumsangaben in langem Format. Das lange Datumsformat wird in Nachrichten an den Benutzer verwendet, wenn die <b>Genauigkeit von Laufzeiten</b> auf <b>Exakt</b> gestellt ist.<br><br>Für verfügbare Platzhalter siehe <a href="http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters" target="_blank">http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters</a>]]></de>
+            <en><![CDATA[Date format for dates in long format. The short format is used in messages sent to the user if the <b>Accuracy of membership duration</b> setting is set to <b>Exact</b>.<br><br>For available placeholder see: <a href="http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters" target="_blank">http://php.net/manual/de/function.strftime.php#refsect1-function.strftime-parameters</a>]]></en>
+        </locale>
 
         <!-- Cron -->
         <locale name="cron.checkMembershipUsers.title">
@@ -100,6 +144,12 @@
             <en><![CDATA[Checks for each membership whether it needs to be extended or terminated.]]></en>
         </locale>
 
+        <!-- Profile -->
+        <locale name="profile.button.text">
+            <de><![CDATA[Mitgliedschaften]]></de>
+            <en><![CDATA[Memberships]]></en>
+        </locale>
+
     </groups>
 
     <groups name="quiqqer/memberships" datatype="js">
@@ -600,6 +650,40 @@
             <en><![CDATA[Send confirmation]]></en>
         </locale>
 
+        <!-- Control: profile/UserProfile -->
+        <locale name="controls.profile.userprofile.template.header">
+            <de><![CDATA[Ihre Mitgliedschaften]]></de>
+            <en><![CDATA[Your memberships]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.template.headerMembership">
+            <de><![CDATA[Mitgliedschaft]]></de>
+            <en><![CDATA[Membership]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.template.headerMembershipData">
+            <de><![CDATA[Details]]></de>
+            <en><![CDATA[Details]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.labelAddedDate">
+            <de><![CDATA[In Mitgliedschaft seit]]></de>
+            <en><![CDATA[In membership since]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.labelStatus">
+            <de><![CDATA[Status]]></de>
+            <en><![CDATA[Status]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.status.cancelled">
+            <de><![CDATA[gekündigt]]></de>
+            <en><![CDATA[cancelled]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.status.cancelled_start">
+            <de><![CDATA[Kündigungsbestätigung ausstehend]]></de>
+            <en><![CDATA[Cancellation confirmation pending]]></en>
+        </locale>
+        <locale name="controls.profile.userprofile.datatable.status.active">
+            <de><![CDATA[aktiv]]></de>
+            <en><![CDATA[active]]></en>
+        </locale>
+
         <!-- Templates -->
         <locale name="templates.mail.greeting">
             <de><![CDATA[Hallo [name]!]]></de>
@@ -621,14 +705,6 @@
             <de><![CDATA[Hiermit bestätigen wir die Kündigung Ihrer Mitgliedschaft <b>[membershipTitle]</b> zum <b>[endDate]</b>. Bis zu diesem Zeitpunkt können Sie weiterhin alle Vorteile Ihrer Mitgliedschaft genießen.]]></de>
             <en><![CDATA[We hereby confirm the termination of your membership <b>[membershipTitle]</b> as of <b>[endDate]</b>. Until that time, you can continue to enjoy all the benefits of your membership.]]></en>
         </locale>
-        <locale name="templates.cancel.confirm.success">
-            <de><![CDATA[Die Kündigung Ihrer Mitgliedschaft war erfolgreich!]]></de>
-            <en><![CDATA[The termination of your membership has been successful!]]></en>
-        </locale>
-        <locale name="templates.cancel.confirm.error">
-            <de><![CDATA[Bei der Kündigung Ihrer Mitgliedschaft ist ein Fehler aufgetreten:<br><br>[errorMessage]]]></de>
-            <en><![CDATA[An error occurred during the termination of your membership:<br><br>[errorMessage]]]></en>
-        </locale>
         <locale name="templates.mail.expired.subject" html="true">
             <de><![CDATA[Ablauf Ihrer Mitgliedschaft]]></de>
             <en><![CDATA[Expiration of your membership]]></en>
@@ -645,6 +721,32 @@
             <de><![CDATA[Ihre Mitgliedschaft <b>[membershipTitle]</b> wurde automatisch bis zum <b>[endDate]</b> verlängert.]]></de>
             <en><![CDATA[Your membership <b>[membershipTitle]</b> has been extended automatically until <b>[endDate]</b>.]]></en>
         </locale>
+        <locale name="templates.mail.manualextend.subject" html="true">
+            <de><![CDATA[Verlängerung Ihrer Mitgliedschaft]]></de>
+            <en><![CDATA[Extension of your membership]]></en>
+        </locale>
+        <locale name="templates.mail.manualextend.body" html="true">
+            <de><![CDATA[Ihre Mitgliedschaft <b>[membershipTitle]</b> wurde bis zum <b>[endDate]</b> verlängert.]]></de>
+            <en><![CDATA[Your membership <b>[membershipTitle]</b> has been extended until <b>[endDate]</b>.]]></en>
+        </locale>
+
+        <!-- CancelVerification -->
+        <locale name="verification.cancel.success">
+            <de><![CDATA[Die Kündigung Ihrer Mitgliedschaft war erfolgreich! Sie erhalten noch eine separate E-Mail mit der Bestätigung Ihrer Kündigung.]]></de>
+            <en><![CDATA[The termination of your membership has been successful! You will receive a separate e-mail with confirmation of your termination.]]></en>
+        </locale>
+        <locale name="verification.cancel.error.general">
+            <de><![CDATA[Bei der Kündigung Ihrer Mitgliedschaft ist ein Fehler aufgetreten. Bitte starten Sie den Kündigungsvorgang erneut oder kontaktieren Sie einen Administrator.]]></de>
+            <en><![CDATA[There was an error while canceling your membership. Please restart the termination process or contact an administrator.]]></en>
+        </locale>
+        <locale name="verification.cancel.error.expired">
+            <de><![CDATA[Die Gültigkeit Ihres Kündigungs-Links ist leider abgelaufen. Bitte starten sie den Kündigungsvorgang erneut.]]></de>
+            <en><![CDATA[The validity of your termination link has unfortunately expired. Please restart the termination process.]]></en>
+        </locale>
+        <locale name="verification.cancel.error.already_cancelled">
+            <de><![CDATA[Die Mitgliedschaft wurde bereits gekündigt.]]></de>
+            <en><![CDATA[The membership has already been cancelled.]]></en>
+        </locale>
 
     </groups>
 </locales>
diff --git a/settings.xml b/settings.xml
index 8ddb590..a30b622 100644
--- a/settings.xml
+++ b/settings.xml
@@ -16,6 +16,30 @@
                     <type><![CDATA[string]]></type>
                     <defaultvalue>reset</defaultvalue>
                 </conf>
+                <conf name="cancelDuration">
+                    <type><![CDATA[integer]]></type>
+                    <defaultvalue>1440</defaultvalue>
+                </conf>
+                <conf name="sendAutoExtendMail">
+                    <type><![CDATA[bool]]></type>
+                    <defaultvalue>1</defaultvalue>
+                </conf>
+                <conf name="sendManualExtendMail">
+                    <type><![CDATA[bool]]></type>
+                    <defaultvalue>1</defaultvalue>
+                </conf>
+            </section>
+
+            <section name="date_formats_short">
+                <conf name="??">
+                    <type><![CDATA[string]]></type>
+                </conf>
+            </section>
+
+            <section name="date_formats_long">
+                <conf name="??">
+                    <type><![CDATA[string]]></type>
+                </conf>
             </section>
 
         </config>
@@ -68,6 +92,58 @@
                                 <locale group="quiqqer/memberships" var="settings.durationMode.option.exact"/>
                             </option>
                         </select>
+
+                        <input conf="membershipusers.cancelDuration" type="number">
+                            <text>
+                                <locale group="quiqqer/memberships" var="settings.cancelDuration.title"/>
+                            </text>
+                            <description>
+                                <locale group="quiqqer/memberships" var="settings.cancelDuration.description"/>
+                            </description>
+                        </input>
+
+                        <input conf="membershipusers.sendAutoExtendMail" type="checkbox">
+                            <text>
+                                <locale group="quiqqer/memberships" var="settings.sendAutoExtendMail.title"/>
+                            </text>
+                            <description>
+                                <locale group="quiqqer/memberships" var="settings.sendAutoExtendMail.description"/>
+                            </description>
+                        </input>
+
+                        <input conf="membershipusers.sendManualExtendMail" type="checkbox">
+                            <text>
+                                <locale group="quiqqer/memberships" var="settings.sendManualExtendMail.title"/>
+                            </text>
+                            <description>
+                                <locale group="quiqqer/memberships" var="settings.sendManualExtendMail.description"/>
+                            </description>
+                        </input>
+
+                    </settings>
+
+                    <settings title="dateformat" name="dateformat">
+                        <title>
+                            <locale group="quiqqer/memberships" var="settings.dateformat.title"/>
+                        </title>
+
+                        <input conf="date_formats_short" type="text" data-qui="controls/system/AvailableLanguages">
+                            <text>
+                                <locale group="quiqqer/memberships" var="settings.dateFormatShort.title"/>
+                            </text>
+                            <description>
+                                <locale group="quiqqer/memberships" var="settings.dateFormatShort.description"/>
+                            </description>
+                        </input>
+
+                        <input conf="date_formats_long" type="text" data-qui="controls/system/AvailableLanguages">
+                            <text>
+                                <locale group="quiqqer/memberships" var="settings.dateFormatLong.title"/>
+                            </text>
+                            <description>
+                                <locale group="quiqqer/memberships" var="settings.dateFormatLong.description"/>
+                            </description>
+                        </input>
                     </settings>
                 </category>
 
diff --git a/src/QUI/Memberships/Users/CancelVerification.php b/src/QUI/Memberships/Users/CancelVerification.php
new file mode 100644
index 0000000..9b4d8e1
--- /dev/null
+++ b/src/QUI/Memberships/Users/CancelVerification.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace QUI\Memberships\Users;
+
+use QUI\Verification\VerificationInterface;
+use QUI\Memberships\Users\Handler as MembershipUsersHandler;
+use QUI\Verification\Verifier;
+use QUI;
+
+class CancelVerification implements VerificationInterface
+{
+    /**
+     * Verification identifier
+     *
+     * @var string
+     */
+    protected $identifier = null;
+
+    /**
+     * CancelVerification constructor.
+     *
+     * @param int $membershipUserId
+     */
+    public function __construct($membershipUserId)
+    {
+        $this->identifier = $membershipUserId;
+    }
+
+    /**
+     * Get a unique identifier that identifies this Verification
+     *
+     * @return string
+     */
+    public function getIdentifier()
+    {
+        return $this->identifier;
+    }
+
+    /**
+     * Get the duration of a Verification (minutes)
+     *
+     * @return int|false - duration in minutes;
+     * if this method returns false use the module setting default value
+     */
+    public static function getValidDuration()
+    {
+        return (int)MembershipUsersHandler::getSetting('cancelDuration');
+    }
+
+    /**
+     * Execute this method on successful verification
+     *
+     * @param string $identifier - Unique Verification identifier
+     * @return void
+     */
+    public static function onSuccess($identifier)
+    {
+        /** @var MembershipUser $MembershipUser */
+        $MembershipUser = MembershipUsersHandler::getInstance()->getChild((int)$identifier);
+        $MembershipUser->confirmManualCancel();
+    }
+
+    /**
+     * Execute this method on unsuccessful verification
+     *
+     * @param string $identifier - Unique Verification identifier
+     * @return void
+     */
+    public static function onError($identifier)
+    {
+        // nothing
+    }
+
+    /**
+     * This message is displayed to the user on successful verification
+     *
+     * @param string $identifier - Unique Verification identifier
+     * @return string
+     */
+    public static function getSuccessMessage($identifier)
+    {
+        return QUI::getLocale()->get(
+            'quiqqer/memberships',
+            'verification.cancel.success'
+        );
+    }
+
+    /**
+     * This message is displayed to the user on unsuccessful verification
+     *
+     * @param string $identifier - Unique Verification identifier
+     * @param string $reason - The reason for the error (see \QUI\Verification\Verifier::REASON_)
+     * @return string
+     */
+    public static function getErrorMessage($identifier, $reason)
+    {
+        switch ($reason) {
+            case Verifier::ERROR_REASON_EXPIRED:
+                $msg = QUI::getLocale()->get(
+                    'quiqqer/memberships',
+                    'verification.cancel.error.expired'
+                );
+                break;
+
+            case Verifier::ERROR_REASON_ALREADY_VERIFIED:
+                $msg = QUI::getLocale()->get(
+                    'quiqqer/memberships',
+                    'verification.cancel.error.already_cancelled'
+                );
+                break;
+
+            default:
+                $msg = QUI::getLocale()->get(
+                    'quiqqer/memberships',
+                    'verification.cancel.error.general'
+                );
+        }
+
+        return $msg;
+    }
+
+    /**
+     * Automatically redirect the user to this URL on successful verification
+     *
+     * @param string $identifier - Unique Verification identifier
+     * @return string|false - If this method returns false, no redirection takes place
+     */
+    public static function getOnSuccessRedirectUrl($identifier)
+    {
+        return false;
+    }
+
+    /**
+     * Automatically redirect the user to this URL on unsuccessful verification
+     *
+     * Hint: This requires that an active Verification with the given identifier exists!
+     *
+     * @param string $identifier - Unique Verification identifier
+     * @return string|false - If this method returns false, no redirection takes place
+     */
+    public static function getOnErrorRedirectUrl($identifier)
+    {
+        return false;
+    }
+}
diff --git a/src/QUI/Memberships/Users/Handler.php b/src/QUI/Memberships/Users/Handler.php
index c985f5c..916dc75 100644
--- a/src/QUI/Memberships/Users/Handler.php
+++ b/src/QUI/Memberships/Users/Handler.php
@@ -134,6 +134,41 @@ public function getUserIdsByMembershipId($membershipId)
         return $membershipUserIds;
     }
 
+    /**
+     * Get all MembershipUser objects by userId
+     *
+     * @param int $userId - QUIQQER User ID
+     * @param bool $includeArchived (optional) - include archived MembershipUsers
+     * @return MembershipUser[]
+     */
+    public function getMembershipUsersByUserId($userId, $includeArchived = false)
+    {
+        $where = array(
+            'userId'   => $userId,
+            'archived' => 0
+        );
+
+        if ($includeArchived === true) {
+            unset($where['archived']);
+        }
+
+        $result = QUI::getDataBase()->fetch(array(
+            'select' => array(
+                'id'
+            ),
+            'from'   => self::getDataBaseTableName(),
+            'where'  => $where
+        ));
+
+        $membershipUsers = array();
+
+        foreach ($result as $row) {
+            $membershipUsers[] = self::getChild($row['id']);
+        }
+
+        return $membershipUsers;
+    }
+
 //    /**
 //     * Get membership
 //     *
@@ -204,7 +239,6 @@ public function getChildAttributes()
             'endDate',
             'archived',
             'history',
-            'cancelHash',
             'cancelDate',
             'cancelled',
             'archiveReason',
diff --git a/src/QUI/Memberships/Users/MembershipUser.php b/src/QUI/Memberships/Users/MembershipUser.php
index 57e2a56..12cbd7f 100644
--- a/src/QUI/Memberships/Users/MembershipUser.php
+++ b/src/QUI/Memberships/Users/MembershipUser.php
@@ -9,6 +9,7 @@
 use QUI\Memberships\Utils;
 use QUI\Mail\Mailer;
 use QUI\Permissions\Permission;
+use QUI\Verification\Verifier;
 
 /**
  * Class MembershipUser
@@ -61,6 +62,7 @@ public function extend($auto = true)
         $Membership = $this->getMembership();
         $extendMode = MembershipUsersHandler::getSetting('extendMode');
 
+        // Calculate new start and/or end time
         if ($auto || $extendMode === 'reset') {
             $start         = time();
             $extendCounter = $this->getAttribute('extendCounter');
@@ -87,11 +89,57 @@ public function extend($auto = true)
         $this->addHistoryEntry(MembershipUsersHandler::HISTORY_TYPE_EXTENDED, json_encode($historyData));
         $this->update();
 
-        // send autoextend mail
-        $subject = $this->getUser()->getLocale()->get('quiqqer/memberships', 'templates.mail.autoextend.subject');
+        // send mail
+        if ($auto) {
+            $this->sendAutoExtendMail();
+        } else {
+            $this->sendManualExtendMail();
+        }
+    }
+
+    /**
+     * Send mail to the user if the membership is extended automatically
+     *
+     * @return void
+     */
+    protected function sendAutoExtendMail()
+    {
+        $sendMail = MembershipUsersHandler::getSetting('sendAutoExtendMail');
+
+        if (!$sendMail) {
+            return;
+        }
+
+        $subject = $this->getUser()->getLocale()->get(
+            'quiqqer/memberships', 'templates.mail.autoextend.subject'
+        );
+
         $this->sendMail($subject, dirname(__FILE__, 5) . '/templates/mail_autoextend.html');
     }
 
+    /**
+     * Send mail to the user if the membership is extended manually
+     *
+     * Manually = Either by admin edit or if the user is re-added to the membership
+     * although he already is a member
+     *
+     * @return void
+     */
+    public function sendManualExtendMail()
+    {
+        $sendMail = MembershipUsersHandler::getSetting('sendManualExtendMail');
+
+        if (!$sendMail) {
+            return;
+        }
+
+        $subject = $this->getUser()->getLocale()->get(
+            'quiqqer/memberships', 'templates.mail.manualextend.subject'
+        );
+
+        $this->sendMail($subject, dirname(__FILE__, 5) . '/templates/mail_manualextend.html');
+    }
+
     /**
      * Expires this memberships user
      *
@@ -122,21 +170,8 @@ public function startManualCancel()
         }
 
         $cancelDate = Utils::getFormattedTimestamp();
-        $cancelHash = md5(openssl_random_pseudo_bytes(256));
-        $cancelUrl  = QUI::getRewrite()->getProject()->getVHost(true);
-        $cancelUrl  .= URL_OPT_DIR . 'quiqqer/memberships/bin/membership.php';
-
-        $params = array(
-            'mid'    => $this->id,
-            'hash'   => $cancelHash,
-            'action' => 'confirmManualCancel'
-        );
-
-        $cancelUrl .= '?' . http_build_query($params);
 
-        // generate random hash
         $this->setAttributes(array(
-            'cancelHash' => $cancelHash,
             'cancelDate' => $cancelDate
         ));
 
@@ -145,13 +180,15 @@ public function startManualCancel()
         // save cancel hash and date to database
         $this->update();
 
+        $CancelVerification = new CancelVerification($this->id);
+
         // send cancellation mail
         $this->sendMail(
             QUI::getLocale()->get('quiqqer/memberships', 'templates.mail.startcancel.subject'),
             dirname(__FILE__, 5) . '/templates/mail_startcancel.html',
             array(
                 'cancelDate' => $cancelDate,
-                'cancelUrl'  => $cancelUrl
+                'cancelUrl'  => Verifier::startVerification($CancelVerification)
             )
         );
     }
@@ -159,34 +196,14 @@ public function startManualCancel()
     /**
      * Confirm membership cancellation
      *
-     * @param string $confirmHash - cancel hash
      * @return void
      *
      * @throws QUI\Memberships\Exception
      */
-    public function confirmManualCancel($confirmHash)
+    public function confirmManualCancel()
     {
         if ($this->isCancelled()) {
-            throw new QUI\Memberships\Exception(array(
-                'quiqqer/memberships',
-                'exception.users.membershipuser.confirmManualCancel.already.cancelled'
-            ));
-        }
-
-        $cancelHash = $this->getAttribute('cancelHash');
-
-//        if (empty($cancelHash)) {
-//            throw new QUI\Memberships\Exception(array(
-//                'quiqqer/memberships',
-//                'exception.users.membershipuser.confirmManualCancel.no.hash'
-//            ));
-//        }
-
-        if ($confirmHash !== $cancelHash) {
-            throw new QUI\Memberships\Exception(array(
-                'quiqqer/memberships',
-                'exception.users.membershipuser.confirmManualCancel.hash.mismatch'
-            ));
+            return;
         }
 
         $this->setAttributes(array(
@@ -426,21 +443,30 @@ public function getHistory()
     protected function formatDate($date)
     {
         $Locale       = $this->getUser()->getLocale();
+        $lang         = $Locale->getCurrent();
         $durationMode = MembershipsHandler::getSetting('durationMode');
-        $timestamp    = strtotime($date);
+        $Conf         = QUI::getPackage('quiqqer/memberships')->getConfig();
 
         switch ($durationMode) {
             case 'day':
-                $dayDate       = date('Y-m-d', $timestamp);
-                $formattedDate = $Locale->formatDate(strtotime($dayDate));
+                $dateFormat = $Conf->get('date_formats_short', $lang);
+
+                // fallback to default value
+                if (empty($dateFormat)) {
+                    $dateFormat = '%D';
+                }
                 break;
 
             default:
-                $minuteDate    = date('Y-m-d H:i', $timestamp);
-                $formattedDate = $Locale->formatDate(strtotime($minuteDate));
+                $dateFormat = $Conf->get('date_formats_long', $lang);
+
+                // fallback to default value
+                if (empty($dateFormat)) {
+                    $dateFormat = '%D %H:%M';
+                }
         }
 
-        return $formattedDate;
+        return $Locale->formatDate(strtotime($date), $dateFormat);
     }
 
     /**
@@ -452,19 +478,21 @@ public function getFrontendViewData()
     {
         $QuiqqerUser = $this->getUser();
         $Membership  = $this->getMembership();
+        $Locale      = $QuiqqerUser->getLocale();
 
         return array(
             'id'              => $this->getId(),
             'userId'          => $QuiqqerUser->getId(),
             'membershipId'    => $Membership->getId(),
-            'membershipTitle' => $Membership->getTitle(),
+            'membershipTitle' => $Membership->getTitle($Locale),
+            'membershipShort' => $Membership->getDescription($Locale),
             'username'        => $QuiqqerUser->getUsername(),
             'fullName'        => $QuiqqerUser->getName(),
             'addedDate'       => $this->formatDate($this->getAttribute('addedDate')),
             'beginDate'       => $this->formatDate($this->getAttribute('beginDate')),
             'endDate'         => $this->formatDate($this->getAttribute('endDate')),
-            'archived'        => $this->isArchived(),
-            'archiveReason'   => $this->getAttribute('archiveReason'),
+//            'archived'        => $this->isArchived(),
+//            'archiveReason'   => $this->getAttribute('archiveReason'),
             'cancelled'       => $this->isCancelled()
         );
     }
diff --git a/templates/mail_manualextend.html b/templates/mail_manualextend.html
new file mode 100644
index 0000000..a9e3963
--- /dev/null
+++ b/templates/mail_manualextend.html
@@ -0,0 +1,11 @@
+<h1>
+    {locale group="quiqqer/memberships" value="templates.mail.greeting" Locale=$Locale
+    name=$MembershipUser->getUser()->getName()}
+</h1>
+
+<p>
+    {locale group="quiqqer/memberships" value="templates.mail.manualextend.body" Locale=$Locale
+    membershipTitle=$MembershipUser->getMembership()->getTitle()
+    endDate=$data['endDate']
+    }
+</p>
\ No newline at end of file
-- 
GitLab