diff --git a/application/ConstructionConsent/ConstructionConsent.php b/application/ConstructionConsent/ConstructionConsent.php
index 1a09bbea6..b20d3ebdf 100644
--- a/application/ConstructionConsent/ConstructionConsent.php
+++ b/application/ConstructionConsent/ConstructionConsent.php
@@ -679,4 +679,85 @@ FROM ConstructionConsent
return $where;
}
+ public static function searchConstructionConsentBasedOnName($name, $filter = []) {
+ $items = [];
+ $db = FronkDB::singleton();
+
+ $searchName = trim($name);
+ if (empty($searchName)) {
+// return $items;
+ }
+
+ $searchTerms = explode(' ', $searchName);
+ $searchConditions = [];
+
+ foreach ($searchTerms as $term) {
+ $escapedTerm = $db->escape($term);
+ $searchConditions[] = "(cco.firstname LIKE '%$escapedTerm%' OR
+ cco.lastname LIKE '%$escapedTerm%' OR
+ cc.name LIKE '%$escapedTerm%' OR
+ ccp.name LIKE '%$escapedTerm%')";
+ }
+
+ if (!empty($searchConditions)) {
+ $intelligentSearchWhere = "(" . implode(' AND ', $searchConditions) . ")";
+ if (isset($filter['add-where'])) {
+ $filter['add-where'] .= " AND $intelligentSearchWhere";
+ } else {
+ $filter['add-where'] = "AND $intelligentSearchWhere";
+ }
+ }
+
+ if (isset($filter['add-where'])) {
+ $filter['add-where'] .= " AND cco.firstname IS NOT NULL";
+ } else {
+ $filter['add-where'] = "AND cco.firstname IS NOT NULL";
+ }
+
+ $where = self::getSqlFilter($filter);
+
+ $sql = "SELECT cc.id AS consent_id, cc.name AS consent_name, cc.status AS consent_status,
+ cc.result AS consent_result, cc.ez, cc.kg, cc.gst, cc.gstnr,
+ ccp.id AS project_id, ccp.name AS project_name,
+ cco.id AS owner_id, cco.firstname, cco.lastname, cco.street, cco.zip, cco.city,
+ cco.email, cco.phone, cco.status AS owner_status, cco.result AS owner_result,
+ vh.strasse AS address_street, vh.hausnummer AS address_number,
+ vh.plz AS address_zip, vh.ortschaft AS address_city
+ FROM ConstructionConsent cc
+ LEFT JOIN ConstructionConsentProject ccp ON cc.constructionconsentproject_id = ccp.id
+ LEFT JOIN ConstructionConsentOwner cco ON cc.id = cco.constructionconsent_id
+ LEFT JOIN `".ADDRESSDB_DBNAME."`.view_hausnummer vh ON cc.adb_hausnummer_id = vh.hausnummer_id
+ WHERE $where
+ ORDER BY cc.edit DESC, cco.lastname ASC, cco.firstname ASC";
+
+ $res = $db->query($sql);
+ while ($data = $res->fetch_assoc()) {
+ $items[] = [
+ 'consent_id' => $data['consent_id'],
+ 'consent_name' => $data['consent_name'],
+ 'consent_status' => $data['consent_status'],
+ 'consent_result' => $data['consent_result'],
+ 'project_id' => $data['project_id'],
+ 'project_name' => $data['project_name'],
+ 'owner_id' => $data['owner_id'],
+ 'owner_name' => trim($data['firstname'] . ' ' . $data['lastname']),
+ 'owner_address' => $data['street'] . ', ' . $data['zip'] . ' ' . $data['city'],
+ 'owner_contact' => $data['email'] . ($data['phone'] ? ' / ' . $data['phone'] : ''),
+ 'owner_status' => $data['owner_status'],
+ 'owner_result' => $data['owner_result'],
+ 'property_info' => 'EZ:' . $data['ez'] . ' KG:' . $data['kg'] . ' GST:' . $data['gst'] . ($data['gstnr'] ? '-' . $data['gstnr'] : ''),
+ 'address' => $data['address_street'] . ' ' . $data['address_number'] . ', ' . $data['address_zip'] . ' ' . $data['address_city'],
+ ];
+ }
+
+ return $items;
+ }
+
+
+
+
+
+
+
+
}
diff --git a/application/ConstructionConsent/ConstructionConsentController.php b/application/ConstructionConsent/ConstructionConsentController.php
index 7e26efd95..003d6065f 100644
--- a/application/ConstructionConsent/ConstructionConsentController.php
+++ b/application/ConstructionConsent/ConstructionConsentController.php
@@ -168,6 +168,7 @@ class ConstructionConsentController extends mfBaseController {
protected function downloadAction()
{
$owner_id = $this->request->owner_id;
+ $open = $this->request->open;
if (!is_numeric($owner_id) || $owner_id < 1 || !($owner = new ConstructionConsentOwner($owner_id))->id) {
$this->layout()->setFlash("Besitzer nicht gefunden", "error");
@@ -184,7 +185,7 @@ class ConstructionConsentController extends mfBaseController {
$this->redirect("ConstructionConsent", "View", ["id" => $cc->id]);
}
- $this->sendPdfResponse($filename, "Zustimmungserklärung-{$cc->id}-{$owner_id}.pdf");
+ $this->sendPdfResponse($filename, "Zustimmungserklärung-{$cc->id}-{$owner_id}.pdf", $open === 'true');
}
protected function downloadMultipleAction()
@@ -224,10 +225,12 @@ class ConstructionConsentController extends mfBaseController {
$this->sendPdfResponse($filename, "Zustimmungserklärungen-".date('Y-m-d').".pdf");
}
- private function sendPdfResponse(string $filename, string $downloadName): void
+ private function sendPdfResponse(string $filename, string $downloadName, bool $open = false): void
{
header('Content-Type: ' . mime_content_type($filename));
- header('Content-disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
+ if ($open === true) header('Content-disposition: inline; filename="' . rawurlencode($downloadName) . '"');
+ else header('Content-disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
+
header('Content-Length: ' . filesize($filename));
readfile($filename);
exit;
@@ -1278,4 +1281,84 @@ class ConstructionConsentController extends mfBaseController {
fclose($output);
exit();
}
+
+ protected function searchTabletAction() {
+ $name = '';
+ $filter = [];
+
+ // Handle POST data - support both form data and JSON input
+ if($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $postData = [];
+
+ // Check for JSON input (Axios sends JSON)
+ $inputJSON = file_get_contents('php://input');
+ $jsonData = json_decode($inputJSON, true);
+
+ if($jsonData !== null) {
+ $postData = $jsonData;
+ } else {
+ $postData = $_POST;
+ }
+
+ if(isset($postData['name'])) {
+ $name = $postData['name'];
+ }
+
+ if(isset($postData['filter']) && is_array($postData['filter'])) {
+ $filter = $postData['filter'];
+ }
+ }
+
+ $results = ConstructionConsent::searchConstructionConsentBasedOnName($name, $filter);
+
+ self::returnJson([
+ 'success' => true,
+ 'count' => count($results),
+ 'results' => $results
+ ]);
+ }
+
+ protected function signTabletAction() {
+ $JSGlobals = ["BASE_PATH" => self::getUrl(""),
+ "DASHBOARD_URL" => self::getUrl("Dashboard"),
+ "MFAPPNAME" => MFAPPNAME_SLUG,
+ "PAGE_TITLE" => "Zustimmungserklärungen unterzeichnen",
+ "PATH" => [
+ ["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
+ ["text" => "Zustimmungserklärungen unterzeichnen", "href" => self::getUrl("ConstructionConsent", "SignTablet")],
+ ],
+ "IS_ADMIN" => $this->me->is("Admin"),
+ ];
+
+ $this->layout()->set("vueViewName", "ConstructionConsentSignTablet");
+ $this->layout()->set("JSGlobals", $JSGlobals);
+ $this->layout()->setTemplate("VueViews/Vue");
+ }
+
+ protected function signAction() {
+ $POST = json_decode(file_get_contents('php://input'), true);
+
+ $owner_id = $POST['owner_id'] ?? null;
+ $signature = $POST['signature'] ?? null;
+ $signature_name = $POST['signature_name'] ?? null;
+ $signature_date = $POST['signature_date'] ?? null;
+
+ if(!$owner_id || !$signature) {
+ self::sendError("Invalid request");
+ }
+
+ $owner = new ConstructionConsentOwner($owner_id);
+ if(!$owner->id) self::sendError("Owner not found");
+
+ if($signature_name) $owner->signature_name = $signature_name;
+ if($signature_date) $owner->signature_date = date("U", strtotime($signature_date));
+ if($signature) $owner->signature = $signature;
+
+ $owner->status = 'returned';
+ $owner->result = 'accepted';
+
+ if(!$owner->save()) self::sendError("Error saving signature");
+ self::returnJson(["success" => true]);
+ }
+
}
diff --git a/db/migrations/20250515135500_constr_cons_owner_add_signature.php b/db/migrations/20250515135500_constr_cons_owner_add_signature.php
new file mode 100644
index 000000000..a2e1f43b7
--- /dev/null
+++ b/db/migrations/20250515135500_constr_cons_owner_add_signature.php
@@ -0,0 +1,31 @@
+getEnvironment() == "addressdb") {
+ $ConstructionConsentOwner = $this->table("ConstructionConsentOwner");
+ $ConstructionConsentOwner->addColumn("signature", "text", ["null" => true]);
+ $ConstructionConsentOwner->addColumn("signature_name", "string", ["limit" => 255, "null" => true]);
+ $ConstructionConsentOwner->addColumn("signature_date", "integer", ["null" => true]);
+
+ $ConstructionConsentOwner->update();
+ }
+ }
+
+ public function down(): void
+ {
+ if($this->getEnvironment() == "addressdb") {
+ $ConstructionConsentOwner = $this->table("ConstructionConsentOwner");
+ $ConstructionConsentOwner->removeColumn("signature");
+ $ConstructionConsentOwner->removeColumn("signature_name");
+ $ConstructionConsentOwner->removeColumn("signature_date");
+
+ $ConstructionConsentOwner->update();
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignTablet.css b/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignTablet.css
new file mode 100644
index 000000000..907be3a87
--- /dev/null
+++ b/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignTablet.css
@@ -0,0 +1,83 @@
+.consent-signing-container {
+ padding: 15px;
+}
+
+.signatureModal .modal-dialog {
+ max-width: 90%;
+}
+
+.signature-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+}
+
+.signature-header {
+ text-align: center;
+ margin-bottom: 20px;
+ width: 100%;
+}
+
+.signature-options {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ margin-bottom: 15px;
+}
+
+.signature-area {
+ width: 100%;
+ margin-bottom: 20px;
+ border: 1px solid #ccc;
+ border-radius: 5px;
+ overflow: hidden;
+ background-color: #f9f9f9;
+}
+
+.signature-area.landscape {
+ height: 300px;
+}
+
+.signature-area.portrait {
+ height: 500px;
+}
+
+.signature-actions {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ width: 100%;
+}
+
+.notes-area {
+ width: 100%;
+}
+
+/* Touch-friendly styling for tablets */
+@media (max-width: 1024px) {
+ .btn, .form-control, select {
+ height: 46px;
+ padding: 10px 15px;
+ font-size: 16px;
+ }
+
+ .btn-sm {
+ height: 38px;
+ padding: 8px 12px;
+ }
+
+ .table th, .table td {
+ padding: 12px 8px;
+ font-size: 14px;
+ }
+
+ input[type=range] {
+ height: 30px;
+ }
+
+ input[type=color] {
+ height: 30px;
+ width: 50px;
+ }
+}
\ No newline at end of file
diff --git a/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignTablet.js b/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignTablet.js
new file mode 100644
index 000000000..43e6e4677
--- /dev/null
+++ b/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignTablet.js
@@ -0,0 +1,349 @@
+Vue.component('construction-consent-signature-pad', {
+ props: {
+ ownerId: {type: Number, required: true},
+ ownerName: {type: String, required: true}
+ },
+ data() {
+ return {
+ window: window,
+ signaturePad: null,
+ consent: null,
+ signatureDate: new Date().toLocaleDateString(),
+ notes: '',
+ penWidth: 2,
+ penColor: '#000000',
+ screenOrientation: 'landscape'
+ }
+ },
+ //language=Vue
+ template: `
+ Datum: {{ signatureDate }}Einwilligung für {{ ownerName }}
+
') : response.data.message || 'Ein Fehler ist aufgetreten');
+ }
+ } catch (error) {
+ console.error('Error submitting signature:', error);
+ this.window.notify('error', 'Ein Fehler ist beim Speichern der Unterschrift aufgetreten');
+ }
+ },
+ updatePenSettings() {
+ this.signaturePad.penColor = this.penColor;
+ this.signaturePad.penWidth = this.penWidth;
+ },
+ toggleOrientation() {
+ this.screenOrientation = this.screenOrientation === 'landscape' ? 'portrait' : 'landscape';
+ this.$nextTick(() => {
+ this.resizeCanvas();
+ });
+ },
+ resizeCanvas() {
+ const canvas = document.getElementById('consent-signature-pad');
+ const container = canvas.parentElement;
+
+ if (this.screenOrientation === 'landscape') {
+ canvas.width = container.offsetWidth - 20;
+ canvas.height = 300;
+ } else {
+ canvas.width = Math.min(container.offsetWidth - 20, 500);
+ canvas.height = 500;
+ }
+
+ // Important: maintain drawing after resize
+ const data = this.signaturePad.toData();
+ this.signaturePad.clear();
+ if (data) {
+ this.signaturePad.fromData(data);
+ }
+ }
+ },
+ mounted() {
+ this.$nextTick(() => {
+ const canvas = document.getElementById('consent-signature-pad');
+ this.signaturePad = new SignaturePad(canvas, {
+ penColor: this.penColor,
+ penWidth: this.penWidth,
+ velocityFilterWeight: 0.5 // Better for tablet input
+ });
+
+ // Initial canvas sizing
+ this.resizeCanvas();
+
+ // Resize on window change
+ window.addEventListener('resize', this.resizeCanvas);
+
+ // Handle tablet touch events better
+ canvas.addEventListener('touchstart', function(event) {
+ if (event.cancelable) {
+ event.preventDefault();
+ }
+ }, {passive: false});
+ });
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeCanvas);
+ }
+});
+
+// Main Component for Construction Consent Owner Signing
+Vue.component('construction-consent-sign-tablet', {
+ data() {
+ return {
+ window: window,
+ searchName: '',
+ searchFilter: {
+ consent_status: '',
+ owner_result: ''
+ },
+ results: [],
+ loading: false,
+ showSignaturePad: false,
+ selectedConsent: null,
+ filterOptions: {
+ consent_status: [
+ {text: 'Alle Status', value: ''},
+ {text: 'Offen', value: 'open'},
+ {text: 'In Bearbeitung', value: 'in_progress'},
+ {text: 'Abgeschlossen', value: 'completed'}
+ ],
+ owner_result: [
+ {text: 'Alle Ergebnisse', value: ''},
+ {text: 'Nicht signiert', value: ''},
+ {text: 'Zugestimmt', value: 'accepted'},
+ {text: 'Abgelehnt', value: 'rejected'}
+ ]
+ },
+ tableConfig: {
+ key: 'ConstructionConsentTable',
+ tableHeader: 'Bauvorhaben Eigentümer',
+ defaultPageSize: 10,
+ headers: [
+ {text: 'Bauvorhaben', key: 'consent_name', sortable: false, filter: false, class: 'text-nowrap', priority: 9},
+ {text: 'Projekt', key: 'project_name', sortable: false,filter: false, class: 'text-nowrap', priority: 7},
+ {text: 'Eigentümer', key: 'owner_name', sortable: false,filter: false, class: 'text-nowrap', priority: 6},
+ {text: 'Adresse', key: 'owner_address', sortable: false,filter: false, class: '', priority: 5},
+ {text: 'Eigentümer Status', key: 'owner_status', sortable: false,filter: false, class: 'text-nowrap', priority: 3},
+ {text: 'Eigentümer Ergebnis', key: 'owner_result', sortable: false,filter: false, class: 'text-nowrap', priority: 2},
+ {text: 'Aktionen', key: 'actions', sortable: false,filter: false, class: 'text-nowrap text-center', priority: 1}
+ ],
+ }
+ }
+ },
+ //language=Vue
+ template: `
+