Merge branch 'ConstructionConsent/add-signature' into 'master'
added new signature action See merge request fronk/thetool!1340
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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: `
|
||||
<tt-modal class="signatureModal" :show="true" :delete="false" :save="false" @update:show="$emit('close')" :title="'Unterschrift für Bauvorhaben'">
|
||||
<div class="signature-container">
|
||||
<div class="signature-header">
|
||||
<h4>Einwilligung für {{ ownerName }}</h4>
|
||||
<p>Datum: {{ signatureDate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="signature-area" :class="screenOrientation">
|
||||
<canvas id="consent-signature-pad" width="800" height="300"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="notes-area mb-3">
|
||||
<tt-input v-model="notes" label="Unterschrieben von...." placeholder="Name oder i.V."></tt-input>
|
||||
</div>
|
||||
|
||||
<div class="signature-actions">
|
||||
<button class="btn btn-primary" @click="submit()">Speichern</button>
|
||||
<button class="btn btn-outline-secondary" @click="signaturePad.clear()">Zurücksetzen</button>
|
||||
<button class="btn btn-outline-danger" @click="$emit('close')">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</tt-modal>
|
||||
`,
|
||||
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('<br>') : 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: `
|
||||
<div class="consent-signing-container">
|
||||
<tt-card>
|
||||
<div class="search-area p-3">
|
||||
<div style="display: grid; grid-template-columns: 4fr 1fr; grid-gap: 16px; justify-content: space-between; align-items: center;">
|
||||
<tt-input
|
||||
v-model="searchName"
|
||||
label="Eigentümer suchen"
|
||||
placeholder="Name eingeben..."
|
||||
></tt-input>
|
||||
|
||||
<tt-button
|
||||
style="margin-top: 10px"
|
||||
@click="searchOwners"
|
||||
text="Suchen"
|
||||
additionalClass="btn-primary"
|
||||
icon="fas fa-search"
|
||||
:loading="loading"
|
||||
></tt-button>
|
||||
</div>
|
||||
</div>
|
||||
</tt-card>
|
||||
|
||||
<tt-card v-if="results.length > 0" class="mt-3">
|
||||
<tt-table
|
||||
:data="results"
|
||||
:config="tableConfig"
|
||||
excel-export
|
||||
>
|
||||
<template v-slot:owner_status="{ row }">
|
||||
<span class="badge" :class="getOwnerStatusBadgeClass(row.owner_status)">
|
||||
{{ getOwnerStatusText(row.owner_status) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:owner_result="{ row }">
|
||||
<span class="badge" :class="getOwnerResultBadgeClass(row.owner_result)">
|
||||
{{ getOwnerResultText(row.owner_result) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions="{ row }">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<tt-button
|
||||
@click="openSignaturePad(row)"
|
||||
text="Unterschreiben"
|
||||
:sm="true"
|
||||
additionalClass="btn-primary"
|
||||
icon="fas fa-signature"
|
||||
:disabled="row.owner_result === 'accepted'"
|
||||
></tt-button>
|
||||
<a :href="window.TT_CONFIG['BASE_PATH'] + '/ConstructionConsent/view/?id=' + row.consent_id"
|
||||
class="btn btn-sm btn-info"
|
||||
target="_blank"
|
||||
title="Ansehen">
|
||||
<i class="far fa-eye"></i>
|
||||
</a>
|
||||
<a :href="window.TT_CONFIG['BASE_PATH'] + '/ConstructionConsent/Download/?open=true&owner_id=' + row.owner_id"
|
||||
class="btn btn-sm btn-info"
|
||||
target="_blank"
|
||||
title="PDF">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</tt-table>
|
||||
</tt-card>
|
||||
|
||||
<tt-card v-else-if="results.length === 0 && !loading" class="mt-3">
|
||||
<div class="alert alert-info">
|
||||
Keine Ergebnisse gefunden. Bitte versuchen Sie eine andere Suche.
|
||||
</div>
|
||||
</tt-card>
|
||||
|
||||
<construction-consent-signature-pad
|
||||
v-if="showSignaturePad"
|
||||
:owner-id="selectedConsent.owner_id"
|
||||
:owner-name="selectedConsent.owner_name"
|
||||
@close="handleSignatureClose"
|
||||
></construction-consent-signature-pad>
|
||||
</div>
|
||||
`,
|
||||
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();
|
||||
}
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user