| @@ -12,13 +12,17 @@ RUN apt-get install -y \ | |||||
| libjpeg62-turbo-dev \ | libjpeg62-turbo-dev \ | ||||
| libpng-dev \ | libpng-dev \ | ||||
| libicu-dev \ | libicu-dev \ | ||||
| libzip-dev \ | |||||
| zip \ | |||||
| && docker-php-ext-install -j$(nproc) iconv \ | && docker-php-ext-install -j$(nproc) iconv \ | ||||
| && docker-php-ext-configure gd --with-jpeg=/usr/include/ --with-freetype=/usr/include/ \ | && docker-php-ext-configure gd --with-jpeg=/usr/include/ --with-freetype=/usr/include/ \ | ||||
| && docker-php-ext-install -j$(nproc) gd \ | && docker-php-ext-install -j$(nproc) gd \ | ||||
| && docker-php-ext-install -j$(nproc) intl \ | && docker-php-ext-install -j$(nproc) intl \ | ||||
| && docker-php-ext-install -j$(nproc) pdo_mysql | |||||
| && docker-php-ext-install -j$(nproc) pdo_mysql \ | |||||
| && docker-php-ext-install zip | |||||
| RUN apt-get install -y git zip | RUN apt-get install -y git zip | ||||
| RUN apt-get install mariadb-client -y | |||||
| RUN apt install nano | RUN apt install nano | ||||
| @@ -6,4 +6,6 @@ | |||||
| - info@coaching4dogs.xx (kein Admin-Account) | - info@coaching4dogs.xx (kein Admin-Account) | ||||
| - benny@probuddy.de | - benny@probuddy.de | ||||
| - timo@tbuddy.de | - timo@tbuddy.de | ||||
| - tretslag@gmail.com | |||||
| - tretslag@gmail.com | |||||
| - `docker exec -it pb-php /bin/bash` | |||||
| - SELECT * FROM profile WHERE JSON_CONTAINS( `teams_js`, '{"team_id":"75"}' ); | |||||
| @@ -541,7 +541,8 @@ app.core.Dict = { | |||||
| "APPOINTMENT_CATEGORIES_MISSING_FOR_ADMIN" : "Deinem Gruppenprofil wurden keine Kategorien zugewiesen. Bitte benachrichtige den Gruppeninhaber.", | "APPOINTMENT_CATEGORIES_MISSING_FOR_ADMIN" : "Deinem Gruppenprofil wurden keine Kategorien zugewiesen. Bitte benachrichtige den Gruppeninhaber.", | ||||
| "NOTE_NO_SERIAL_APPOINTMENT_EDITING_AFTER_PUBLISHING" : "Hinweis: Serientermine können nach ihrer Veröffentlichung nur noch einzelnd bearbeitet werden.", | "NOTE_NO_SERIAL_APPOINTMENT_EDITING_AFTER_PUBLISHING" : "Hinweis: Serientermine können nach ihrer Veröffentlichung nur noch einzelnd bearbeitet werden.", | ||||
| "STATS_REPORTING" : 'Reporting', | "STATS_REPORTING" : 'Reporting', | ||||
| 'STATS_PLANNING' : 'Planung' | |||||
| 'STATS_PLANNING' : 'Planung', | |||||
| "DO_YOU_REALLY_WANT_TO_DELETE_THIS_CONTRACT" : "Möchtest Du diesen Vertrag wirklich löschen?", | |||||
| }, | }, | ||||
| "en" : { | "en" : { | ||||
| } | } | ||||
| @@ -5,9 +5,33 @@ const AppointmentSelectTable = { | |||||
| var app = this.app, | var app = this.app, | ||||
| $container = $( this.container ); | $container = $( this.container ); | ||||
| this.render( { apps: appointments.getAll(), activeGroup : this.app.user.getActiveGroup() } ); | |||||
| let date = new Date(); | |||||
| let strFrom = ''; | |||||
| date.setMonth(date.getMonth() - 3); | |||||
| let dd = String(date.getDate()).padStart(2, '0'); | |||||
| let mm = String(date.getMonth() + 1).padStart(2, '0'); //January is 0! | |||||
| let yyyy = date.getFullYear(); | |||||
| strFrom = yyyy + '-' + mm + '-' + dd; | |||||
| $container.find( '.datatable' ).DataTable( { | |||||
| date = new Date(); | |||||
| let strTo = ''; | |||||
| date.setMonth(date.getMonth() + 3); | |||||
| dd = String(date.getDate()).padStart(2, '0'); | |||||
| mm = String(date.getMonth() + 1).padStart(2, '0'); //January is 0! | |||||
| yyyy = date.getFullYear(); | |||||
| strTo = yyyy + '-' + mm + '-' + dd; | |||||
| let appointmentItems = appointments.getAll(); | |||||
| this.render( | |||||
| { apps: appointmentItems, | |||||
| activeGroup : this.app.user.getActiveGroup(), | |||||
| strFrom: strFrom, | |||||
| strTo: strTo | |||||
| } | |||||
| ); | |||||
| let table = $container.find( '.datatable' ).DataTable( { | |||||
| pageLength: 50, | pageLength: 50, | ||||
| responsive: true, | responsive: true, | ||||
| dom: '<"html5buttons"B>lTfgitp', | dom: '<"html5buttons"B>lTfgitp', | ||||
| @@ -78,6 +102,31 @@ const AppointmentSelectTable = { | |||||
| app.openInApp( '/#/appointment/' + $( this ).attr( 'data-appointment-id' ) + '/chat' ); | app.openInApp( '/#/appointment/' + $( this ).attr( 'data-appointment-id' ) + '/chat' ); | ||||
| e.stopPropagation(); | e.stopPropagation(); | ||||
| }); | }); | ||||
| $('input.dateStart').on('change', function() { | |||||
| table.draw(); | |||||
| }); | |||||
| $('input.dateEnd').on('change', function() { | |||||
| table.draw(); | |||||
| }); | |||||
| $.fn.dataTable.ext.search.push( | |||||
| function(settings, searchData, index, rowData, counter) { | |||||
| const dateFrom = $('input.dateStart').val(); | |||||
| const dateTo = $('input.dateEnd').val(); | |||||
| const item = appointmentItems[index]; | |||||
| const startDate = item['start_dt']; | |||||
| const strStartDate = startDate.date.substring(0,10); | |||||
| if (strStartDate >= dateFrom && strStartDate <= dateTo) { | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| ); | |||||
| // Initial drawing needed to filter by date interval | |||||
| table.draw(); | |||||
| }, | }, | ||||
| getSelectedIds : function() | getSelectedIds : function() | ||||
| @@ -1,4 +1,6 @@ | |||||
| <div class="table-responsive"> | <div class="table-responsive"> | ||||
| <label style="margin-left: 15px">von:<input type="date" style="margin-left: 12px;margin-right: 5px;" class="dateStart" name="search-start" value="<%= strFrom %>"></label> | |||||
| <label>bis:<input type="date" style="margin-left: 12px;margin-right: 5px;"class="dateEnd" name="search-end" value="<%= strTo %>"></label> | |||||
| <table class="table table-striped table-bordered table-hover datatable" > | <table class="table table-striped table-bordered table-hover datatable" > | ||||
| <thead> | <thead> | ||||
| <tr> | <tr> | ||||
| @@ -12,6 +12,11 @@ const ContractAppointmentAssignDualList = { | |||||
| self.contracts = props.get( 'contracts'); | self.contracts = props.get( 'contracts'); | ||||
| self.attendedAppointments = props.get( 'attendedAppointments' ); | self.attendedAppointments = props.get( 'attendedAppointments' ); | ||||
| self.childObj = {}; | |||||
| for (const prop in profile.child_profile_js) { | |||||
| self.childObj[profile.child_profile_js[prop]['id']] = profile.child_profile_js[prop]['name']; | |||||
| } | |||||
| this.render( | this.render( | ||||
| { | { | ||||
| profile : profile, | profile : profile, | ||||
| @@ -131,6 +136,10 @@ const ContractAppointmentAssignDualList = { | |||||
| } | } | ||||
| aSubject += a.subject + ', ' + moment( a.start_dt ).format( 'DD.MM.YYYY' ); | aSubject += a.subject + ', ' + moment( a.start_dt ).format( 'DD.MM.YYYY' ); | ||||
| if (a.attendee_child_id !== null) { | |||||
| aSubject += ' (' + self.childObj[a.attendee_child_id] + ')'; | |||||
| } | |||||
| if ( self.selectedContractId && self.attendedAppointments[ aai ].contractId == self.selectedContractId ) | if ( self.selectedContractId && self.attendedAppointments[ aai ].contractId == self.selectedContractId ) | ||||
| { | { | ||||
| $select.append( | $select.append( | ||||
| @@ -4,11 +4,13 @@ const ContractUnchargedMemberDataTable = { | |||||
| { | { | ||||
| var app = this.app, | var app = this.app, | ||||
| groupId = props.get( 'groupId' ), | groupId = props.get( 'groupId' ), | ||||
| items = props.get( 'unchargedItems' ), | |||||
| $container = $( this.container ); | $container = $( this.container ); | ||||
| this.render( { ui: props.get( 'unchargedItems' ), activeGroup : groupId } ); | |||||
| this.render( { ui: items, activeGroup : groupId } ); | |||||
| $container.find( '.datatable' ).DataTable( { | |||||
| let table = $container.find( '.datatable' ).DataTable( { | |||||
| pageLength: 50, | pageLength: 50, | ||||
| responsive: true, | responsive: true, | ||||
| dom: '<"html5buttons"B>lTfgitp', | dom: '<"html5buttons"B>lTfgitp', | ||||
| @@ -49,6 +51,32 @@ const ContractUnchargedMemberDataTable = { | |||||
| { | { | ||||
| app.redirect( '/contract/charge-appointment/' + groupId + '/' + $( e.currentTarget ).attr( 'data-member-id' ) ); | app.redirect( '/contract/charge-appointment/' + groupId + '/' + $( e.currentTarget ).attr( 'data-member-id' ) ); | ||||
| }); | }); | ||||
| $('input.filterContract').on('change', function() { | |||||
| table.draw(); | |||||
| }); | |||||
| $('input.filterOpenClaim').on('change', function() { | |||||
| table.draw(); | |||||
| }); | |||||
| $.fn.dataTable.ext.search.push( | |||||
| function(settings, searchData, index, rowData, counter) { | |||||
| const contractChecked = $('.filterContract:checked').length === 1; | |||||
| const openClaimChecked = $('.filterOpenClaim:checked').length === 1; | |||||
| const item = items[index]; | |||||
| if (contractChecked && openClaimChecked) { | |||||
| return item['hasActiveContract'] === false && parseInt(item['count']) !== 0; | |||||
| } | |||||
| if (contractChecked) { | |||||
| return item['hasActiveContract'] === false; | |||||
| } | |||||
| if (openClaimChecked) { | |||||
| return parseInt(item['count']) !== 0; | |||||
| } | |||||
| return true; | |||||
| } | |||||
| ); | |||||
| }, | }, | ||||
| destroy : function() | destroy : function() | ||||
| @@ -1,4 +1,6 @@ | |||||
| <div class="table-responsive"> | <div class="table-responsive"> | ||||
| <label><input type="checkbox" style="margin-right: 5px;" class="filterContract" value="Active"/>ohne aktiven Vertag</label> | |||||
| <label><input type="checkbox" style="margin-left: 12px;margin-right: 5px;" class="filterOpenClaim" value="Active"/>nur offene Verrechnungen</label> | |||||
| <table class="table table-striped table-bordered table-hover datatable" > | <table class="table table-striped table-bordered table-hover datatable" > | ||||
| <thead> | <thead> | ||||
| <tr> | <tr> | ||||
| @@ -8,6 +10,7 @@ | |||||
| <th>PLZ</th> | <th>PLZ</th> | ||||
| <th>Stadt</th> | <th>Stadt</th> | ||||
| <th>Offene Verrechnungen</th> | <th>Offene Verrechnungen</th> | ||||
| <th>Aktiver Vertrag vorhanden</th> | |||||
| </tr> | </tr> | ||||
| </thead> | </thead> | ||||
| <tbody> | <tbody> | ||||
| @@ -19,7 +22,7 @@ | |||||
| <%= ui[ i ].member ? ui[ i ].member.id : '-' %> | <%= ui[ i ].member ? ui[ i ].member.id : '-' %> | ||||
| </td> | </td> | ||||
| <td> | <td> | ||||
| <%= ui[ i ].member ? ui[ i ].member.getName() : '[gelöscht]' %> | |||||
| <%= ui[ i ].member ? ui[ i ].member.getName() : '[gelöscht - nicht mehr Mitglied der Gruppe]' %> | |||||
| </td> | </td> | ||||
| <td> | <td> | ||||
| <%= ui[ i ].member && ui[ i ].member.street ? ui[ i ].member.street : '---' %> | <%= ui[ i ].member && ui[ i ].member.street ? ui[ i ].member.street : '---' %> | ||||
| @@ -30,9 +33,12 @@ | |||||
| <td> | <td> | ||||
| <%= ui[ i ].member && ui[ i ].member.city ? ui[ i ].member.city : '---' %> | <%= ui[ i ].member && ui[ i ].member.city ? ui[ i ].member.city : '---' %> | ||||
| </td> | </td> | ||||
| <td class="text-center"> | |||||
| <td> | |||||
| <%= ui[ i ].count %> | <%= ui[ i ].count %> | ||||
| </td> | </td> | ||||
| <td> | |||||
| <%= ui[ i ].hasActiveContract ? 'ja' : 'nein' %> | |||||
| </td> | |||||
| </tr> | </tr> | ||||
| <% } %> | <% } %> | ||||
| </tbody> | </tbody> | ||||
| @@ -36,7 +36,7 @@ const AccessDataTable = { | |||||
| }, | }, | ||||
| }, | }, | ||||
| buttons: [ | buttons: [ | ||||
| { extend: 'copy'}, | |||||
| {extend: 'copy'}, | |||||
| {extend: 'csv'}, | {extend: 'csv'}, | ||||
| {extend: 'excel', title: 'Mitgliederliste'}, | {extend: 'excel', title: 'Mitgliederliste'}, | ||||
| {extend: 'pdf', title: 'Mitgliederliste'}, | {extend: 'pdf', title: 'Mitgliederliste'}, | ||||
| @@ -59,6 +59,8 @@ const AppointmentList = { | |||||
| } | } | ||||
| } | } | ||||
| let compAst = this.createComponent( | let compAst = this.createComponent( | ||||
| 'appointment-select-table', | 'appointment-select-table', | ||||
| $container.find( '[f-id="container-appointment-data-table"]' ).first().get( 0 ), | $container.find( '[f-id="container-appointment-data-table"]' ).first().get( 0 ), | ||||
| @@ -16,6 +16,7 @@ const ContractCharging = { | |||||
| app = this.app, | app = this.app, | ||||
| members = [], | members = [], | ||||
| groupId = p.get( 'groupId' ), | groupId = p.get( 'groupId' ), | ||||
| profileIdsWithActiveContract = [], | |||||
| self = this; | self = this; | ||||
| function getMemberById( mId ) | function getMemberById( mId ) | ||||
| @@ -30,7 +31,6 @@ const ContractCharging = { | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| return m; | return m; | ||||
| } | } | ||||
| @@ -56,49 +56,63 @@ const ContractCharging = { | |||||
| app.rpc.call( | app.rpc.call( | ||||
| 'Contract', | 'Contract', | ||||
| 'getUnchargedProfileIds', | |||||
| 'getList', | |||||
| { | { | ||||
| teamId : groupId | |||||
| teamId : groupId, | |||||
| state : 'active' | |||||
| }, | }, | ||||
| function( res ) | |||||
| { | |||||
| let unchargedItems = [], | |||||
| memberIdsInList = []; | |||||
| if ( res && res.hasOwnProperty( 'data' ) && res.data.hasOwnProperty( 'unchargedProfileIds' ) ) | |||||
| { | |||||
| const up = res.data.unchargedProfileIds; | |||||
| for( let upi = 0; upi < up.length; upi++ ) | |||||
| { | |||||
| memberIdsInList.push( +up[ upi ].profile_id ); | |||||
| unchargedItems.push({ | |||||
| member : getMemberById( up[ upi ].profile_id ), | |||||
| count : up[ upi ].cnt | |||||
| }); | |||||
| function( res ) { | |||||
| if (res && res.hasOwnProperty('data') && res.data.hasOwnProperty('contractData')) { | |||||
| const cs = res.data.contractData; | |||||
| for (let ci = 0; ci < cs.length; ci++) { | |||||
| profileIdsWithActiveContract[cs[ci]['profile_id']] = 1; | |||||
| } | } | ||||
| } | } | ||||
| for ( let mi = 0; mi < members.length; mi++ ) | |||||
| { | |||||
| if ( -1 === memberIdsInList.indexOf( +members[ mi ].id ) ) | |||||
| app.rpc.call( | |||||
| 'Contract', | |||||
| 'getUnchargedProfileIds', | |||||
| { | { | ||||
| unchargedItems.push({ | |||||
| member : members[ mi ], | |||||
| count : 0 | |||||
| }); | |||||
| } | |||||
| } | |||||
| teamId: groupId | |||||
| }, | |||||
| function (res) { | |||||
| let unchargedItems = [], | |||||
| memberIdsInList = []; | |||||
| if (res && res.hasOwnProperty('data') && res.data.hasOwnProperty('unchargedProfileIds')) { | |||||
| const up = res.data.unchargedProfileIds; | |||||
| for (let upi = 0; upi < up.length; upi++) { | |||||
| memberIdsInList.push(+up[upi].profile_id); | |||||
| unchargedItems.push({ | |||||
| member: getMemberById(up[upi].profile_id), | |||||
| count: up[upi].cnt, | |||||
| hasActiveContract: profileIdsWithActiveContract.hasOwnProperty(up[upi].profile_id) | |||||
| }); | |||||
| } | |||||
| } | |||||
| let st = self.createComponent( | |||||
| 'contract-uncharged-member-data-table', | |||||
| $container.find( '[f-id="container-contract-uncharged-member-table"]' ).first().get( 0 ), | |||||
| { | |||||
| unchargedItems : unchargedItems, | |||||
| groupId : groupId | |||||
| } | |||||
| ); | |||||
| for (let mi = 0; mi < members.length; mi++) { | |||||
| if (-1 === memberIdsInList.indexOf(+members[mi].id)) { | |||||
| unchargedItems.push({ | |||||
| member: members[mi], | |||||
| count: 0, | |||||
| hasActiveContract: -1 !== profileIdsWithActiveContract.indexOf(+members[mi].id) | |||||
| }); | |||||
| } | |||||
| } | |||||
| $container.find( '.sk-loading' ).toggleClass( 'sk-loading' ); | |||||
| }.bind ( this ) | |||||
| let st = self.createComponent( | |||||
| 'contract-uncharged-member-data-table', | |||||
| $container.find('[f-id="container-contract-uncharged-member-table"]').first().get(0), | |||||
| { | |||||
| unchargedItems: unchargedItems, | |||||
| groupId: groupId | |||||
| } | |||||
| ); | |||||
| $container.find('.sk-loading').toggleClass('sk-loading'); | |||||
| }.bind(this) | |||||
| ); | |||||
| } | |||||
| ); | ); | ||||
| } | } | ||||
| } | } | ||||
| @@ -21,8 +21,7 @@ const ContractEdit = { | |||||
| self = this; | self = this; | ||||
| self.contract = null; | self.contract = null; | ||||
| this.render( { groupId : groupId } ); | |||||
| self.contractIsDeletable = false; | |||||
| app.rpc.call( | app.rpc.call( | ||||
| 'Contract', | 'Contract', | ||||
| @@ -32,11 +31,16 @@ const ContractEdit = { | |||||
| }, | }, | ||||
| function( res ) | function( res ) | ||||
| { | { | ||||
| if ( res && res.hasOwnProperty( 'data' ) && res.data.hasOwnProperty( 'contractData' ) ) | |||||
| if ( res && res.hasOwnProperty( 'data' )) | |||||
| { | { | ||||
| self.contract = self.createInstance( 'Contract', res.data.contractData ); | |||||
| if ( res.data.hasOwnProperty( 'contractData' ) ) { | |||||
| self.contract = self.createInstance( 'Contract', res.data.contractData ); | |||||
| } | |||||
| if ( res.data.hasOwnProperty( 'contractIsDeletable' ) ) { | |||||
| self.contractIsDeletable = res.data.contractIsDeletable; | |||||
| } | |||||
| } | } | ||||
| this.render( { groupId : groupId, contractIsDeletable: self.contractIsDeletable } ); | |||||
| const $compContractForm = self.createComponent( | const $compContractForm = self.createComponent( | ||||
| 'contract-form', | 'contract-form', | ||||
| $container.find( '[f-id="container-contract-form"]' ).first().get( 0 ), | $container.find( '[f-id="container-contract-form"]' ).first().get( 0 ), | ||||
| @@ -89,10 +93,69 @@ const ContractEdit = { | |||||
| } | } | ||||
| }); | }); | ||||
| if (self.contractIsDeletable) { | |||||
| $container.find( '[data-id="btnDeleteContract"]').click( function() | |||||
| { | |||||
| let $modalRoot = $container.find( '[data-id="modal-confirm-delete-contract"]' ).first(); | |||||
| $modalRoot.find( '[data-id="section-confirm-delete-contract"]' ).first().show(); | |||||
| $modalRoot.find( '[data-id="section-loader"]' ).first().hide(); | |||||
| $modalRoot.on( 'shown.bs.modal', function() | |||||
| { | |||||
| $modalRoot.find( '[data-id="btnDelete"]' ).first().off( "click" ); | |||||
| $modalRoot.find( '[data-id="btnDelete"]' ).first().click( function() | |||||
| { | |||||
| $(this).off( "click" ); | |||||
| $modalRoot.find( '[data-id="section-confirm-delete-contract"]' ).first().hide(); | |||||
| $modalRoot.find( '[data-id="section-loader"]' ).first().show(); | |||||
| app.rpc.call( | |||||
| 'Contract', | |||||
| 'delete', | |||||
| { | |||||
| contractId : contractId | |||||
| }, | |||||
| function( resp ) | |||||
| { | |||||
| if ( resp && resp.hasOwnProperty( 'code' ) && resp.code == 200 ) | |||||
| { | |||||
| app.UI.toastSuccess( 'Vertrag wurde erfolgreich gelöscht.', 'Vertrag gelöscht' ); | |||||
| if ( redirectPath ) | |||||
| { | |||||
| app.redirect( redirectPath ); | |||||
| } | |||||
| else | |||||
| { | |||||
| app.redirect( 'contract/list/' + groupId ); | |||||
| } | |||||
| } | |||||
| else | |||||
| { | |||||
| app.UI.toastError( 'Der Vertrag konnte nicht gelöscht werden, ' + | |||||
| 'da er entweder mindestens eine verknüpfte Verrechnung oder bereits ' + | |||||
| 'eine Einzahlung stattgefunden hat', | |||||
| 'Fehler' ); | |||||
| } | |||||
| $modalRoot.modal( 'hide' ); | |||||
| } | |||||
| ); | |||||
| }); | |||||
| }); | |||||
| $modalRoot.on( 'hidden.bs.modal', function() | |||||
| { | |||||
| $modalRoot.modal( 'dispose' ); | |||||
| }); | |||||
| $modalRoot.modal( 'show' ); | |||||
| } | |||||
| ); | |||||
| } | |||||
| $container.find( '.sk-loading' ).toggleClass( 'sk-loading' ); | $container.find( '.sk-loading' ).toggleClass( 'sk-loading' ); | ||||
| }.bind ( this ) | }.bind ( this ) | ||||
| ); | ); | ||||
| }, | }, | ||||
| destroy : function() | destroy : function() | ||||
| @@ -40,10 +40,62 @@ | |||||
| href="javascript:history.back()"> | href="javascript:history.back()"> | ||||
| Abbrechen | Abbrechen | ||||
| </a> | </a> | ||||
| <% if ( true === contractIsDeletable ) { %> | |||||
| <button type="button" | |||||
| class="btn btn-danger" | |||||
| data-id="btnDeleteContract"> | |||||
| Löschen | |||||
| </button> | |||||
| <% } %> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | |||||
| <div class="modal inmodal fade" | |||||
| data-id="modal-confirm-delete-contract" | |||||
| tabindex="-1" | |||||
| role="dialog" | |||||
| aria-hidden="true"> | |||||
| <div class="modal-dialog"> | |||||
| <div class="modal-content"> | |||||
| <div class="modal-header"> | |||||
| <button type="button" | |||||
| class="close" | |||||
| data-dismiss="modal"> | |||||
| <span aria-hidden="true">×</span><span class="sr-only">Close</span> | |||||
| </button> | |||||
| <h4 class="modal-title">Vertrag löschen</h4> | |||||
| </div> | |||||
| <div class="modal-body"> | |||||
| <section data-id="section-confirm-delete-contract" | |||||
| style="display: none"> | |||||
| <h5>Vertrag löschen?</h5> | |||||
| <p> | |||||
| Möchten Sie den Vertrag wirklich löschen? | |||||
| </p> | |||||
| </section> | |||||
| <section data-id="section-loader" | |||||
| style="display: none"> | |||||
| <div style="height: 200px; padding-top: 70px"> | |||||
| <div class="sk-spinner sk-spinner-double-bounce"> | |||||
| <div class="sk-double-bounce1"></div> | |||||
| <div class="sk-double-bounce2"></div> | |||||
| </div> | |||||
| </div> | |||||
| </section> | |||||
| </div> | |||||
| <div class="modal-footer"> | |||||
| <button type="button" | |||||
| class="btn btn-white" | |||||
| data-dismiss="modal">Abbrechen</button> | |||||
| <button type="button" | |||||
| data-id="btnDelete" | |||||
| class="btn btn-danger">Ja, löschen</button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| @@ -118,6 +118,49 @@ | |||||
| </select> | </select> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="form-group row"> | |||||
| <label class="col-sm-2 col-form-label"> | |||||
| BLZ, IBAN | |||||
| </label> | |||||
| <div class="col-sm-3"> | |||||
| <input type="text" | |||||
| name="bank_code" | |||||
| class="form-control" | |||||
| value="<%= group.getContracteeData( 'bank_code' ) ? group.getContracteeData( 'bank_code' ) : '' %>" | |||||
| placeholder="BLZ" /> | |||||
| </div> | |||||
| <div class="col-sm-7"> | |||||
| <input type="text" | |||||
| name="account_number" | |||||
| class="form-control" | |||||
| value="<%= group.getContracteeData( 'account_number' ) ? group.getContracteeData( 'account_number' ) : '' %>" | |||||
| placeholder="IBAN" /> | |||||
| </div> | |||||
| </div> | |||||
| <div class="form-group row"> | |||||
| <label class="col-sm-2 col-form-label"> | |||||
| Bankinstitut | |||||
| </label> | |||||
| <div class="col-sm-10"> | |||||
| <input type="text" | |||||
| name="bank_name" | |||||
| class="form-control" | |||||
| value="<%= group.getContracteeData( 'bank_name' ) ? group.getContracteeData( 'bank_name' ) : '' %>" | |||||
| placeholder="Bankinstitut" /> | |||||
| </div> | |||||
| </div> | |||||
| <div class="form-group row"> | |||||
| <label class="col-sm-2 col-form-label"> | |||||
| PaypalMe | |||||
| </label> | |||||
| <div class="col-sm-10"> | |||||
| <input type="text" | |||||
| name="paypal_me" | |||||
| class="form-control" | |||||
| value="<%= group.getContracteeData( 'paypal_me' ) ? group.getContracteeData( 'paypal_me' ) : '' %>" | |||||
| placeholder="PaypalMe" /> | |||||
| </div> | |||||
| </div> | |||||
| <div class="hr-line-dashed"></div> | <div class="hr-line-dashed"></div> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| @@ -21,7 +21,9 @@ Francis_Utils_Config::set( 'db.tbteamdata.pass', 'root' ); | |||||
| // Paths | // Paths | ||||
| Francis_Utils_Config::set( 'path.logfile', '/var/www/html/logs/tbserver-log.txt' ); | Francis_Utils_Config::set( 'path.logfile', '/var/www/html/logs/tbserver-log.txt' ); | ||||
| Francis_Utils_Config::set( 'path.cron_logfile', '/var/www/html/logs/tbserver-cron-log.txt' ); | Francis_Utils_Config::set( 'path.cron_logfile', '/var/www/html/logs/tbserver-cron-log.txt' ); | ||||
| Francis_Utils_Config::set( 'path.backup_folder', '/temp' ); | |||||
| Francis_Utils_Config::set( 'path.backup_folder', '/var/www/html/temp' ); | |||||
| Francis_Utils_Config::set( 'path.root', realpath( dirname( __FILE__ ) . '/../../../..' ) ); | Francis_Utils_Config::set( 'path.root', realpath( dirname( __FILE__ ) . '/../../../..' ) ); | ||||
| Francis_Core_Autoloader::attachLocator( new Francis_Core_Locator( 'TB', Francis_Utils_Config::get( 'path.root' ) . '/src/server' ) ); | Francis_Core_Autoloader::attachLocator( new Francis_Core_Locator( 'TB', Francis_Utils_Config::get( 'path.root' ) . '/src/server' ) ); | ||||
| Francis_Utils_Config::set( 'path.server.controller', Francis_Utils_Config::get( 'path.root' ) . '/src/server/server/control' ); | Francis_Utils_Config::set( 'path.server.controller', Francis_Utils_Config::get( 'path.root' ) . '/src/server/server/control' ); | ||||
| @@ -151,7 +151,7 @@ class TB_Server_Control_Contract | |||||
| { | { | ||||
| $resp->addData( 'contractAppointments', [] ); | $resp->addData( 'contractAppointments', [] ); | ||||
| } | } | ||||
| $resp->addData('contractIsDeletable', $contract->isDeletable()); | |||||
| return $resp; | return $resp; | ||||
| } | } | ||||
| @@ -515,6 +515,32 @@ class TB_Server_Control_Contract | |||||
| return $resp; | return $resp; | ||||
| } | } | ||||
| public static function delete( TB_Server_Core_RequestData $params ) | |||||
| { | |||||
| $resp = new TB_Server_Core_Response(); | |||||
| $sessionProfile = TB_Server_Core_Session::get()->getProfile(); | |||||
| $contractId = $params->get('contractId' ); | |||||
| $contract = TB_Shared_Ent_TeamData_Contract::get( $contractId ); | |||||
| if ( NULL === $contract ) | |||||
| { | |||||
| throw new \Exception( 'Contract not found.' ); | |||||
| } | |||||
| if ( false === $sessionProfile->isAdminOfTeam( $contract->team_id ) && $sessionProfile->id != $contract->profile_id ) | |||||
| { | |||||
| throw new \Exception('Profile is not allowed to delete contract'); | |||||
| } | |||||
| if ( !$contract->isDeletable() ) | |||||
| { | |||||
| throw new \Exception('Contract is not deletable'); | |||||
| } | |||||
| $contract->delete(); | |||||
| return $resp; | |||||
| } | |||||
| public static function updateAttendances( TB_Server_Core_RequestData $params ) | public static function updateAttendances( TB_Server_Core_RequestData $params ) | ||||
| { | { | ||||
| $resp = new TB_Server_Core_Response(); | $resp = new TB_Server_Core_Response(); | ||||
| @@ -912,6 +912,11 @@ class TB_Server_Control_Team { | |||||
| $cZipCode = $params->get( 'zip_code' ); | $cZipCode = $params->get( 'zip_code' ); | ||||
| $cCity = $params->get( 'city' ); | $cCity = $params->get( 'city' ); | ||||
| $cCountry = $params->get( 'country' ); | $cCountry = $params->get( 'country' ); | ||||
| $cBankCode = $params->get( 'bank_code' ); | |||||
| $cAccountNumber = $params->get( 'account_number' ); | |||||
| $cBankName = $params->get( 'bank_name' ); | |||||
| $cPaypalMe = $params->get( 'paypal_me' ); | |||||
| if ( !$cName || !$cStreet || !$cZipCode || !$cCity || !$cCountry ) | if ( !$cName || !$cStreet || !$cZipCode || !$cCity || !$cCountry ) | ||||
| { | { | ||||
| @@ -923,7 +928,11 @@ class TB_Server_Control_Team { | |||||
| 'street' => $cStreet, | 'street' => $cStreet, | ||||
| 'zip_code' => $cZipCode, | 'zip_code' => $cZipCode, | ||||
| 'city' => $cCity, | 'city' => $cCity, | ||||
| 'country' => $cCountry | |||||
| 'country' => $cCountry, | |||||
| 'bank_code' => $cBankCode, | |||||
| 'account_number' => $cAccountNumber, | |||||
| 'bank_name' => $cBankName, | |||||
| 'paypal_me' => $cPaypalMe | |||||
| ); | ); | ||||
| $team->save(); | $team->save(); | ||||
| @@ -0,0 +1,80 @@ | |||||
| <?php | |||||
| /******************************************************************************** | |||||
| * (c)1337 aheadware.com - All rights reserved | |||||
| ********************************************************************************/ | |||||
| class TB_Server_Job_DailyDbBackup extends TB_Server_Job_Base | |||||
| { | |||||
| public function __construct( TB_Shared_Ent_TeamData_Job $jobEntity ) | |||||
| { | |||||
| $this->entity = $jobEntity; | |||||
| } | |||||
| public function execute() | |||||
| { | |||||
| // Remove old DB dumps | |||||
| $dayInSeconds = 60 * 60 * 24; | |||||
| $pastDays = 7; | |||||
| $dumpFile = Francis_Utils_Config::get( 'path.backup_folder' ) . '/dbdump'; | |||||
| while ( file_exists( $dumpFile . date( 'Y-m-d', ( time() - ( $pastDays * $dayInSeconds ) ) ) . '.zip' ) ) { | |||||
| unlink( $dumpFile . date( 'Y-m-d', ( time() - ( $pastDays * $dayInSeconds ) ) ) . '.zip' ); | |||||
| $pastDays++; | |||||
| } | |||||
| // DB Core | |||||
| $db_host = Francis_Utils_Config::get( 'db.tbcore.host' ); | |||||
| $db_user = Francis_Utils_Config::get( 'db.tbcore.user' ); | |||||
| $db_name = Francis_Utils_Config::get( 'db.tbcore.name' ); | |||||
| $db_passwd = Francis_Utils_Config::get( 'db.tbcore.pass' ); | |||||
| $sql_file = Francis_Utils_Config::get( 'path.backup_folder' ) . "/tbcore.sql"; | |||||
| //exec("mysql -u $db_name -p'$db_passwd' --allow-keywords --add-drop-table --complete-insert --quote-names --routines $db_name > $sql_file"); | |||||
| exec("mysqldump -h $db_host -u $db_user -p'$db_passwd' $db_name > $sql_file"); | |||||
| // DB Teamdata | |||||
| $db_host = Francis_Utils_Config::get( 'db.tbteamdata.host' ); | |||||
| $db_user = Francis_Utils_Config::get( 'db.tbteamdata.user' ); | |||||
| $db_name = Francis_Utils_Config::get( 'db.tbteamdata.name' ); | |||||
| $db_passwd = Francis_Utils_Config::get( 'db.tbteamdata.pass' ); | |||||
| $sql_file = Francis_Utils_Config::get( 'path.backup_folder' ) . "/tbteamdata.sql"; | |||||
| exec("mysqldump -h $db_host -u $db_user -p'$db_passwd' $db_name > $sql_file"); | |||||
| //exec("mysql -u $db_name -p'$db_passwd' --allow-keywords --add-drop-table --complete-insert --quote-names --routines $db_name > $sql_file"); | |||||
| // Zip | |||||
| $zipFilename = Francis_Utils_Config::get( 'path.backup_folder' ) . '/dbdump' . date( 'Y-m-d' ) . '.zip'; | |||||
| $zip = new ZipArchive(); | |||||
| if ( !$zip->open( $zipFilename, ZipArchive::CREATE ) ) { | |||||
| TB_Server_Utils_Log::get()->log( "Cannot zip db dump." ); | |||||
| return; | |||||
| } | |||||
| $zip->addFile( Francis_Utils_Config::get( 'path.backup_folder' ) . '/tbcore.sql' , 'tbcore.sql' ); | |||||
| $zip->addFile( Francis_Utils_Config::get( 'path.backup_folder' ) . '/tbteamdata.sql' , 'tbteamdata.sql' ); | |||||
| $zip->close(); | |||||
| // Cleanup | |||||
| $sql_file = Francis_Utils_Config::get( 'path.backup_folder' ) . "/tbcore.sql"; | |||||
| unlink( $sql_file ); | |||||
| $sql_file = Francis_Utils_Config::get( 'path.backup_folder' ) . "/tbteamdata.sql"; | |||||
| unlink( $sql_file ); | |||||
| echo 'bla'; | |||||
| } | |||||
| public function onDone() | |||||
| { | |||||
| // Reset event to start the next day | |||||
| $nowDt = new DateTime( 'now', new \DateTimeZone( "UTC" ) ); | |||||
| $nowDt->setTime( 2, 0, 0 ); | |||||
| $nowDt->modify( '+1 day' ); | |||||
| $this->entity->trigger_dt = $nowDt; | |||||
| $this->entity->job_status = TB_Shared_Ent_TeamData_Job::JOB_STATUS_READY; | |||||
| $this->entity->touch( 'job_status' ); | |||||
| $this->entity->save(); | |||||
| } | |||||
| public static function getRefId() | |||||
| { | |||||
| return 'system-daily-db-backup'; | |||||
| } | |||||
| } | |||||
| @@ -250,6 +250,8 @@ class TB_Shared_Ent_TeamData_Appointment extends Francis_Db_Row | |||||
| if ( false === $filterGetOld ) | if ( false === $filterGetOld ) | ||||
| { | { | ||||
| $sql .= "AND end_dt > UTC_TIMESTAMP() "; | $sql .= "AND end_dt > UTC_TIMESTAMP() "; | ||||
| } else { | |||||
| $sql .= "AND end_dt > UTC_TIMESTAMP() - INTERVAL 1 MONTH "; | |||||
| } | } | ||||
| $sql .= "ORDER BY start_dt ASC"; | $sql .= "ORDER BY start_dt ASC"; | ||||
| $stmt = $dbh->prepare( $sql ); | $stmt = $dbh->prepare( $sql ); | ||||
| @@ -514,7 +516,7 @@ class TB_Shared_Ent_TeamData_Appointment extends Francis_Db_Row | |||||
| public static function getTeamAppointmentsForAttendedProfileId( $teamId, $profileId ) | public static function getTeamAppointmentsForAttendedProfileId( $teamId, $profileId ) | ||||
| { | { | ||||
| $sql = 'SELECT a.id AS `attendee_id`, a.contract_id AS `attendee_contract_id`, app.* FROM `attendee` AS a '; | |||||
| $sql = 'SELECT a.id AS `attendee_id`, a.contract_id AS `attendee_contract_id`, a.`profile_child_id` AS `attendee_child_id`, app.* FROM `attendee` AS a '; | |||||
| $sql .= 'LEFT JOIN appointment AS `app` ON a.appointment_id = app.id '; | $sql .= 'LEFT JOIN appointment AS `app` ON a.appointment_id = app.id '; | ||||
| $sql .= 'WHERE '; | $sql .= 'WHERE '; | ||||
| $sql .= 'app.team_id = :team_id AND a.profile_id = :profile_id AND '; | $sql .= 'app.team_id = :team_id AND a.profile_id = :profile_id AND '; | ||||
| @@ -168,7 +168,6 @@ class TB_Shared_Ent_TeamData_Contract extends Francis_Db_Row | |||||
| $sql .= "AND app.state = 'open' "; | $sql .= "AND app.state = 'open' "; | ||||
| $sql .= 'AND app.start_dt < NOW() '; | $sql .= 'AND app.start_dt < NOW() '; | ||||
| $sql .= 'AND a.has_attended = "attended" AND a.contract_id IS NULL GROUP BY a.profile_id'; | $sql .= 'AND a.has_attended = "attended" AND a.contract_id IS NULL GROUP BY a.profile_id'; | ||||
| $dbh = self::getDbh(); | $dbh = self::getDbh(); | ||||
| $stmt = $dbh->prepare( $sql ); | $stmt = $dbh->prepare( $sql ); | ||||
| @@ -218,6 +217,12 @@ class TB_Shared_Ent_TeamData_Contract extends Francis_Db_Row | |||||
| TB_Shared_Ent_TeamData_Attendee::removeContractId( $this->id ); | TB_Shared_Ent_TeamData_Attendee::removeContractId( $this->id ); | ||||
| } | } | ||||
| public function isDeletable() | |||||
| { | |||||
| $attendees = TB_Shared_Ent_TeamData_Attendee::getAttendancesForContractId( $this->id ); | |||||
| return (int) $this->price_payed === 0 && count($attendees) === 0; | |||||
| } | |||||
| /** | /** | ||||
| * Return table name | * Return table name | ||||
| * | * | ||||