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: ` + +
+
+

Einwilligung für {{ ownerName }}

+

Datum: {{ signatureDate }}

+
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+
+ `, + methods: { + async submit() { + try { + if (this.signaturePad.isEmpty()) { + this.window.notify('error', 'Bitte eine Unterschrift hinzufügen'); + return; + } + + const data = this.signaturePad.toDataURL(); + const response = await axios.post(window.TT_CONFIG['BASE_PATH'] + '/ConstructionConsent/sign', { + owner_id: this.ownerId, + signature: data, + signature_name: this.notes, + signature_date: this.signatureDate + }); + + if (response.data.success) { + this.window.notify('success', response.data.message || 'Unterschrift erfolgreich gespeichert'); + this.$emit('close', true); // Pass true to indicate successful signing + } else { + this.window.notify('error', + response.data.errors ? Object.values(response.data.errors).join('
') : 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: ` + + `, + methods: { + async searchOwners() { + this.loading = true; + try { + const response = await axios.post( + window.TT_CONFIG['BASE_PATH'] + '/ConstructionConsent/searchTablet', + { + name: this.searchName, + filter: this.searchFilter + } + ); + + if (response.data.success) { + this.results = response.data.results; + } else { + this.window.notify('error', response.data.message || 'Fehler bei der Suche'); + this.results = []; + } + } catch (error) { + console.error('Error searching owners:', error); + this.window.notify('error', 'Ein Fehler ist bei der Suche aufgetreten'); + this.results = []; + } finally { + this.loading = false; + } + }, + openSignaturePad(consent) { + this.selectedConsent = consent; + this.showSignaturePad = true; + }, + handleSignatureClose(success) { + this.showSignaturePad = false; + if (success) { + // Refresh the data after successful signing + this.searchOwners(); + } + }, + getStatusBadgeClass(status) { + switch (status) { + case 'open': return 'badge-primary'; + case 'in_progress': return 'badge-warning'; + case 'completed': return 'badge-success'; + default: return 'badge-secondary'; + } + }, + getStatusText(status) { + switch (status) { + case 'open': return 'Offen'; + case 'in_progress': return 'In Bearbeitung'; + case 'completed': return 'Abgeschlossen'; + default: return status; + } + }, + getOwnerStatusBadgeClass(status) { + switch (status) { + case 'pending': return 'badge-warning'; + case 'contacted': return 'badge-info'; + case 'signed': return 'badge-success'; + case 'returned': return 'badge-success'; + default: return 'badge-secondary'; + } + }, + getOwnerStatusText(status) { + switch (status) { + case 'pending': return 'Ausstehend'; + case 'contacted': return 'Kontaktiert'; + case 'signed': return 'Unterschrieben'; + case 'returned': return 'Zurückgegeben'; + default: return status; + } + }, + getOwnerResultBadgeClass(result) { + if (!result) return 'badge-primary'; + switch (result) { + case 'accepted': return 'badge-success'; + case 'rejected': return 'badge-danger'; + case 'denied': return 'badge-danger'; + default: return 'badge-secondary'; + } + }, + getOwnerResultText(result) { + if (!result) return 'Nicht signiert'; + switch (result) { + case 'accepted': return 'Zugestimmt'; + case 'rejected': return 'Abgelehnt'; + default: return result; + } + } + }, + mounted() { + // Initialize with empty search to show all results + this.searchOwners(); + } +}); \ No newline at end of file diff --git a/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignaturePad.js b/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignaturePad.js new file mode 100644 index 000000000..21e6507e6 --- /dev/null +++ b/public/js/pages/ConstructionConsentSignTablet/ConstructionConsentSignaturePad.js @@ -0,0 +1,6 @@ +/*! + * Signature Pad v4.1.7 | https://github.com/szimek/signature_pad + * (c) 2023 Szymon Nowak | Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).SignaturePad=e()}(this,(function(){"use strict";class t{constructor(t,e,i,s){if(isNaN(t)||isNaN(e))throw new Error(`Point is invalid: (${t}, ${e})`);this.x=+t,this.y=+e,this.pressure=i||0,this.time=s||Date.now()}distanceTo(t){return Math.sqrt(Math.pow(this.x-t.x,2)+Math.pow(this.y-t.y,2))}equals(t){return this.x===t.x&&this.y===t.y&&this.pressure===t.pressure&&this.time===t.time}velocityFrom(t){return this.time!==t.time?this.distanceTo(t)/(this.time-t.time):0}}class e{static fromPoints(t,i){const s=this.calculateControlPoints(t[0],t[1],t[2]).c2,n=this.calculateControlPoints(t[1],t[2],t[3]).c1;return new e(t[1],s,n,t[2],i.start,i.end)}static calculateControlPoints(e,i,s){const n=e.x-i.x,o=e.y-i.y,h=i.x-s.x,r=i.y-s.y,a=(e.x+i.x)/2,c=(e.y+i.y)/2,d=(i.x+s.x)/2,l=(i.y+s.y)/2,u=Math.sqrt(n*n+o*o),v=Math.sqrt(h*h+r*r),_=v/(u+v),p=d+(a-d)*_,m=l+(c-l)*_,g=i.x-p,w=i.y-m;return{c1:new t(a+g,c+w),c2:new t(d+g,l+w)}}constructor(t,e,i,s,n,o){this.startPoint=t,this.control2=e,this.control1=i,this.endPoint=s,this.startWidth=n,this.endWidth=o}length(){let t,e,i=0;for(let s=0;s<=10;s+=1){const n=s/10,o=this.point(n,this.startPoint.x,this.control1.x,this.control2.x,this.endPoint.x),h=this.point(n,this.startPoint.y,this.control1.y,this.control2.y,this.endPoint.y);if(s>0){const s=o-t,n=h-e;i+=Math.sqrt(s*s+n*n)}t=o,e=h}return i}point(t,e,i,s,n){return e*(1-t)*(1-t)*(1-t)+3*i*(1-t)*(1-t)*t+3*s*(1-t)*t*t+n*t*t*t}}class i{constructor(){try{this._et=new EventTarget}catch(t){this._et=document}}addEventListener(t,e,i){this._et.addEventListener(t,e,i)}dispatchEvent(t){return this._et.dispatchEvent(t)}removeEventListener(t,e,i){this._et.removeEventListener(t,e,i)}}class s extends i{constructor(t,e={}){super(),this.canvas=t,this._drawingStroke=!1,this._isEmpty=!0,this._lastPoints=[],this._data=[],this._lastVelocity=0,this._lastWidth=0,this._handleMouseDown=t=>{1===t.buttons&&this._strokeBegin(t)},this._handleMouseMove=t=>{this._strokeMoveUpdate(t)},this._handleMouseUp=t=>{1===t.buttons&&this._strokeEnd(t)},this._handleTouchStart=t=>{if(t.cancelable&&t.preventDefault(),1===t.targetTouches.length){const e=t.changedTouches[0];this._strokeBegin(e)}},this._handleTouchMove=t=>{t.cancelable&&t.preventDefault();const e=t.targetTouches[0];this._strokeMoveUpdate(e)},this._handleTouchEnd=t=>{if(t.target===this.canvas){t.cancelable&&t.preventDefault();const e=t.changedTouches[0];this._strokeEnd(e)}},this._handlePointerStart=t=>{t.preventDefault(),this._strokeBegin(t)},this._handlePointerMove=t=>{this._strokeMoveUpdate(t)},this._handlePointerEnd=t=>{this._drawingStroke&&(t.preventDefault(),this._strokeEnd(t))},this.velocityFilterWeight=e.velocityFilterWeight||.7,this.minWidth=e.minWidth||.5,this.maxWidth=e.maxWidth||2.5,this.throttle="throttle"in e?e.throttle:16,this.minDistance="minDistance"in e?e.minDistance:5,this.dotSize=e.dotSize||0,this.penColor=e.penColor||"black",this.backgroundColor=e.backgroundColor||"rgba(0,0,0,0)",this.compositeOperation=e.compositeOperation||"source-over",this._strokeMoveUpdate=this.throttle?function(t,e=250){let i,s,n,o=0,h=null;const r=()=>{o=Date.now(),h=null,i=t.apply(s,n),h||(s=null,n=[])};return function(...a){const c=Date.now(),d=e-(c-o);return s=this,n=a,d<=0||d>e?(h&&(clearTimeout(h),h=null),o=c,i=t.apply(s,n),h||(s=null,n=[])):h||(h=window.setTimeout(r,d)),i}}(s.prototype._strokeUpdate,this.throttle):s.prototype._strokeUpdate,this._ctx=t.getContext("2d"),this.clear(),this.on()}clear(){const{_ctx:t,canvas:e}=this;t.fillStyle=this.backgroundColor,t.clearRect(0,0,e.width,e.height),t.fillRect(0,0,e.width,e.height),this._data=[],this._reset(this._getPointGroupOptions()),this._isEmpty=!0}fromDataURL(t,e={}){return new Promise(((i,s)=>{const n=new Image,o=e.ratio||window.devicePixelRatio||1,h=e.width||this.canvas.width/o,r=e.height||this.canvas.height/o,a=e.xOffset||0,c=e.yOffset||0;this._reset(this._getPointGroupOptions()),n.onload=()=>{this._ctx.drawImage(n,a,c,h,r),i()},n.onerror=t=>{s(t)},n.crossOrigin="anonymous",n.src=t,this._isEmpty=!1}))}toDataURL(t="image/png",e){return"image/svg+xml"===t?("object"!=typeof e&&(e=void 0),`data:image/svg+xml;base64,${btoa(this.toSVG(e))}`):("number"!=typeof e&&(e=void 0),this.canvas.toDataURL(t,e))}on(){this.canvas.style.touchAction="none",this.canvas.style.msTouchAction="none",this.canvas.style.userSelect="none";const t=/Macintosh/.test(navigator.userAgent)&&"ontouchstart"in document;window.PointerEvent&&!t?this._handlePointerEvents():(this._handleMouseEvents(),"ontouchstart"in window&&this._handleTouchEvents())}off(){this.canvas.style.touchAction="auto",this.canvas.style.msTouchAction="auto",this.canvas.style.userSelect="auto",this.canvas.removeEventListener("pointerdown",this._handlePointerStart),this.canvas.removeEventListener("pointermove",this._handlePointerMove),this.canvas.ownerDocument.removeEventListener("pointerup",this._handlePointerEnd),this.canvas.removeEventListener("mousedown",this._handleMouseDown),this.canvas.removeEventListener("mousemove",this._handleMouseMove),this.canvas.ownerDocument.removeEventListener("mouseup",this._handleMouseUp),this.canvas.removeEventListener("touchstart",this._handleTouchStart),this.canvas.removeEventListener("touchmove",this._handleTouchMove),this.canvas.removeEventListener("touchend",this._handleTouchEnd)}isEmpty(){return this._isEmpty}fromData(t,{clear:e=!0}={}){e&&this.clear(),this._fromData(t,this._drawCurve.bind(this),this._drawDot.bind(this)),this._data=this._data.concat(t)}toData(){return this._data}_getPointGroupOptions(t){return{penColor:t&&"penColor"in t?t.penColor:this.penColor,dotSize:t&&"dotSize"in t?t.dotSize:this.dotSize,minWidth:t&&"minWidth"in t?t.minWidth:this.minWidth,maxWidth:t&&"maxWidth"in t?t.maxWidth:this.maxWidth,velocityFilterWeight:t&&"velocityFilterWeight"in t?t.velocityFilterWeight:this.velocityFilterWeight,compositeOperation:t&&"compositeOperation"in t?t.compositeOperation:this.compositeOperation}}_strokeBegin(t){if(!this.dispatchEvent(new CustomEvent("beginStroke",{detail:t,cancelable:!0})))return;this._drawingStroke=!0;const e=this._getPointGroupOptions(),i=Object.assign(Object.assign({},e),{points:[]});this._data.push(i),this._reset(e),this._strokeUpdate(t)}_strokeUpdate(t){if(!this._drawingStroke)return;if(0===this._data.length)return void this._strokeBegin(t);this.dispatchEvent(new CustomEvent("beforeUpdateStroke",{detail:t}));const e=t.clientX,i=t.clientY,s=void 0!==t.pressure?t.pressure:void 0!==t.force?t.force:0,n=this._createPoint(e,i,s),o=this._data[this._data.length-1],h=o.points,r=h.length>0&&h[h.length-1],a=!!r&&n.distanceTo(r)<=this.minDistance,c=this._getPointGroupOptions(o);if(!r||!r||!a){const t=this._addPoint(n,c);r?t&&this._drawCurve(t,c):this._drawDot(n,c),h.push({time:n.time,x:n.x,y:n.y,pressure:n.pressure})}this.dispatchEvent(new CustomEvent("afterUpdateStroke",{detail:t}))}_strokeEnd(t){this._drawingStroke&&(this._strokeUpdate(t),this._drawingStroke=!1,this.dispatchEvent(new CustomEvent("endStroke",{detail:t})))}_handlePointerEvents(){this._drawingStroke=!1,this.canvas.addEventListener("pointerdown",this._handlePointerStart),this.canvas.addEventListener("pointermove",this._handlePointerMove),this.canvas.ownerDocument.addEventListener("pointerup",this._handlePointerEnd)}_handleMouseEvents(){this._drawingStroke=!1,this.canvas.addEventListener("mousedown",this._handleMouseDown),this.canvas.addEventListener("mousemove",this._handleMouseMove),this.canvas.ownerDocument.addEventListener("mouseup",this._handleMouseUp)}_handleTouchEvents(){this.canvas.addEventListener("touchstart",this._handleTouchStart),this.canvas.addEventListener("touchmove",this._handleTouchMove),this.canvas.addEventListener("touchend",this._handleTouchEnd)}_reset(t){this._lastPoints=[],this._lastVelocity=0,this._lastWidth=(t.minWidth+t.maxWidth)/2,this._ctx.fillStyle=t.penColor,this._ctx.globalCompositeOperation=t.compositeOperation}_createPoint(e,i,s){const n=this.canvas.getBoundingClientRect();return new t(e-n.left,i-n.top,s,(new Date).getTime())}_addPoint(t,i){const{_lastPoints:s}=this;if(s.push(t),s.length>2){3===s.length&&s.unshift(s[0]);const t=this._calculateCurveWidths(s[1],s[2],i),n=e.fromPoints(s,t);return s.shift(),n}return null}_calculateCurveWidths(t,e,i){const s=i.velocityFilterWeight*e.velocityFrom(t)+(1-i.velocityFilterWeight)*this._lastVelocity,n=this._strokeWidth(s,i),o={end:n,start:this._lastWidth};return this._lastVelocity=s,this._lastWidth=n,o}_strokeWidth(t,e){return Math.max(e.maxWidth/(t+1),e.minWidth)}_drawCurveSegment(t,e,i){const s=this._ctx;s.moveTo(t,e),s.arc(t,e,i,0,2*Math.PI,!1),this._isEmpty=!1}_drawCurve(t,e){const i=this._ctx,s=t.endWidth-t.startWidth,n=2*Math.ceil(t.length());i.beginPath(),i.fillStyle=e.penColor;for(let i=0;i0?e.dotSize:(e.minWidth+e.maxWidth)/2;i.beginPath(),this._drawCurveSegment(t.x,t.y,s),i.closePath(),i.fillStyle=e.penColor,i.fill()}_fromData(e,i,s){for(const n of e){const{points:e}=n,o=this._getPointGroupOptions(n);if(e.length>1)for(let s=0;s{const i=document.createElement("path");if(!(isNaN(t.control1.x)||isNaN(t.control1.y)||isNaN(t.control2.x)||isNaN(t.control2.y))){const s=`M ${t.startPoint.x.toFixed(3)},${t.startPoint.y.toFixed(3)} C ${t.control1.x.toFixed(3)},${t.control1.y.toFixed(3)} ${t.control2.x.toFixed(3)},${t.control2.y.toFixed(3)} ${t.endPoint.x.toFixed(3)},${t.endPoint.y.toFixed(3)}`;i.setAttribute("d",s),i.setAttribute("stroke-width",(2.25*t.endWidth).toFixed(3)),i.setAttribute("stroke",e),i.setAttribute("fill","none"),i.setAttribute("stroke-linecap","round"),o.appendChild(i)}}),((t,{penColor:e,dotSize:i,minWidth:s,maxWidth:n})=>{const h=document.createElement("circle"),r=i>0?i:(s+n)/2;h.setAttribute("r",r.toString()),h.setAttribute("cx",t.x.toString()),h.setAttribute("cy",t.y.toString()),h.setAttribute("fill",e),o.appendChild(h)})),o.outerHTML}}return s})); +//# sourceMappingURL=signature_pad.umd.min.js.map \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-table.js b/public/plugins/vue/tt-components/tt-table.js index 47c86c48f..46f1348ed 100644 --- a/public/plugins/vue/tt-components/tt-table.js +++ b/public/plugins/vue/tt-components/tt-table.js @@ -162,7 +162,7 @@ Vue.component('tt-table', { - + @@ -559,6 +559,25 @@ Vue.component('tt-table', { } }, watch: { + data: { + handler: function () { + // we need to refresh the table if the prop data changes + this.loading = true; + this.rawRows = this.data; + this.pagination = { + page: Math.max(this.pagination.page, 1), + per_page: this.pagination?.per_page ? parseInt(this.pagination.per_page) : 10, + total_rows: this.rawRows.length || 0, + total_pages: this.rawRows.length / this.pagination?.per_page, + filtered_available: this.rawRows.length + } + this.loading = false; + this.isInitialised = true; + this.$nextTick(() => { + this.handleResponsiveColumns() + }) + } + }, filters: { handler: function () { if (!this.isInitialised) return;