From 36644284670e43c634292056504a23818d1f5596 Mon Sep 17 00:00:00 2001 From: ma0068 Date: Tue, 30 Sep 2025 10:50:01 +0200 Subject: [PATCH] add logic to edit and delete Personfoto --- .../api/frontend/v1/fotoHandling/Foto.php | 237 ++++++++++++++++++ application/models/person/Person_model.php | 4 + public/css/Studentenverwaltung.css | 4 + public/css/Vertragsverwaltung.css | 4 + public/js/api/factory/fotoHandling.js | 32 +++ .../components/DetailHeader/DetailHeader.js | 129 ++++++++-- .../DetailHeader/Modal/UploadFoto.js | 99 ++++++++ .../js/components/Stv/Studentenverwaltung.js | 2 +- .../Stv/Studentenverwaltung/Details.js | 6 +- .../Details/Dokumente/Modal/Upload.js | 1 + .../Vertraege/Vertragsverwaltung.js | 1 + system/phrasesupdate.php | 82 ++++++ 12 files changed, 581 insertions(+), 20 deletions(-) create mode 100644 application/controllers/api/frontend/v1/fotoHandling/Foto.php create mode 100644 public/js/api/factory/fotoHandling.js create mode 100644 public/js/components/DetailHeader/Modal/UploadFoto.js diff --git a/application/controllers/api/frontend/v1/fotoHandling/Foto.php b/application/controllers/api/frontend/v1/fotoHandling/Foto.php new file mode 100644 index 000000000..4945ddd85 --- /dev/null +++ b/application/controllers/api/frontend/v1/fotoHandling/Foto.php @@ -0,0 +1,237 @@ + ['admin:r', 'assistenz:r'], + 'deleteFoto' => ['admin:r', 'assistenz:r'], + ]); + + //Load Models and Libraries + $this->load->model('person/Person_model', 'PersonModel'); + $this->load->model("crm/Akte_model", "AkteModel"); + $this->load->model('person/Fotostatusperson_model', 'FotostatusPersonModel'); + + $this->loadPhrases([ + 'ui', + 'header' + ]); + } + + public function uploadFoto($person_id) + { + if(!$person_id) + { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Person_id']), self::ERROR_TYPE_GENERAL); + } + + $data = json_decode(file_get_contents("php://input"), true); + + if (!empty($data['image'])) + { + $base64 = $data['image']; + $resizedImage1 = $this->_resize($base64, 827, 1063); + + if (is_null($resizedImage1)) + return $this->terminateWithError($this->p->t('header', 'error_fotoupload'), self::ERROR_TYPE_GENERAL); + + $akte = $this->AkteModel->loadWhere(array('person_id' => $person_id, 'dokument_kurzbz' => 'Lichtbil')); + + $akteUpdateData = array( + 'dokument_kurzbz' => 'Lichtbil', + 'person_id' => $person_id, + 'inhalt' => $resizedImage1, + 'mimetype' => 'image/jpg', + 'erstelltam' => date('c'), + 'gedruckt' => false, + 'titel' => 'Lichtbild_' . $person_id . '.jpg', + 'bezeichnung' => 'Lichtbild gross', + 'insertamum' => date('c'), + 'insertvon' => getAuthUID(), + ); + + if (hasData($akte)) { + $akte_id = getData($akte)[0]->akte_id; + + $akteUpdateData['updateamum'] = date('c'); + $akteUpdateData['updatevon'] = getAuthUID(); + $akteResult = $this->AkteModel->update(array('akte_id' => $akte_id), $akteUpdateData); + } else { + $akteResult = $this->AkteModel->insert($akteUpdateData); + } + + if (isError($akteResult)) { + return $this->terminateWithError(getError($akteResult), self::ERROR_TYPE_GENERAL); + } + + $resizedImage2 = $this->_resize($base64, 101, 130); + + if (is_null($resizedImage2)) + return $this->terminateWithError($this->p->t('header', 'error_fotoupload'), self::ERROR_TYPE_GENERAL); + + $result = $this->_updateFoto($person_id, $resizedImage2); + + if (!isError($result)) { + $this->FotostatusPersonModel->insert(array( + 'person_id' => $person_id, + 'fotostatus_kurzbz' => 'hochgeladen', + 'datum' => date('Y-m-d'), + 'updateamum' => date('c'), + 'updatevon' => getAuthUID(), + 'insertamum' => date('c'), + 'insertvon' => getAuthUID(), + )); + + return $this->terminateWithSuccess($base64); + } + } + else + { + $this->terminateWithError($this->p->t('header', 'error_noPhoto'), self::ERROR_TYPE_GENERAL); + } + } + + public function deleteFoto($person_id) + { + if(!$person_id) + { + return $this->terminateWithError($this->p->t('ui', 'error_missingId', ['id'=> 'Person_id']), self::ERROR_TYPE_GENERAL); + } + + $result = $this->_deleteFoto($person_id); + + if (isError($result)) + { + return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); + } + return $this->terminateWithSuccess($result); + } + + private function _resize($imageData, $maxwidth, $maxheight, $quality = 90) + { + $meta = getimagesize($imageData); + if (!$meta) + { + return null; + } + + $src_width = $meta[0]; + $src_height = $meta[1]; + $mime = $meta['mime']; + + switch ($mime) { + case 'image/jpeg': + case 'image/jpg': + $imagecreated = imagecreatefromjpeg($imageData); + break; + case 'image/png': + $imagecreated = imagecreatefrompng($imageData); + break; + case 'image/gif': + $imagecreated = imagecreatefromgif($imageData); + break; + default: + return null; + } + + + if (!$imagecreated) + { + return null; + } + + $src_aspect_ratio = $src_width / $src_height; + $thu_aspect_ratio = $maxwidth / $maxheight; + + if ($src_width <= $maxwidth && $src_height <= $maxheight) + { + $thu_width = $src_width; + $thu_height = $src_height; + } + elseif ($thu_aspect_ratio > $src_aspect_ratio) + { + $thu_width = (int) ($maxheight * $src_aspect_ratio); + $thu_height = $maxheight; + } + else + { + $thu_width = $maxwidth; + $thu_height = (int) ($maxwidth / $src_aspect_ratio); + } + + $imageScaled = imagecreatetruecolor($thu_width, $thu_height); + + if ($mime === 'image/png') + { + $background = imagecolorallocate($imageScaled , 0, 0, 0); + imagecolortransparent($imageScaled, $background); + imagealphablending($imageScaled, false); + imagesavealpha($imageScaled, true); + } + + imagecopyresampled($imageScaled, $imagecreated, 0, 0, 0, 0, $thu_width, $thu_height, $src_width, $src_height); + + if ($mime === "image/gif") + { + $background = imagecolorallocate($imageScaled, 0, 0, 0); + imagecolortransparent($imageScaled, $background); + } + + if (!empty($imageScaled)) + { + ob_start(); + + if ($mime == 'image/png') + imagepng($imageScaled, NULL); + else if ($mime === 'image/gif') + imagegif($imageScaled, NULL); + else + imagejpeg($imageScaled, NULL, $quality); + + $resizedImageData = ob_get_contents(); + ob_end_clean(); + @imagedestroy($imagecreated); + @imagedestroy($imageScaled); + + + if (!empty($resizedImageData)) + { + return base64_encode($resizedImageData); + } + return null; + } + return null; + } + + private function _updateFoto($person_id, $foto) + { + $personJson['foto'] = $foto; + $result = $this->PersonModel->update($person_id, $personJson); + + if (isError($result)) + { + return error($result->msg, EXIT_ERROR); + } + + return $result; + } + + private function _deleteFoto($person_id) + { + $personJson['foto'] = null; + $result = $this->PersonModel->update($person_id, $personJson); + + if (isError($result)) + { + return error($result->msg, EXIT_ERROR); + } + + return $result; + } +} diff --git a/application/models/person/Person_model.php b/application/models/person/Person_model.php index 997048972..35a0f6144 100644 --- a/application/models/person/Person_model.php +++ b/application/models/person/Person_model.php @@ -423,4 +423,8 @@ class Person_model extends DB_Model return success($result); } } + + + + } \ No newline at end of file diff --git a/public/css/Studentenverwaltung.css b/public/css/Studentenverwaltung.css index bb2588926..05b28d314 100644 --- a/public/css/Studentenverwaltung.css +++ b/public/css/Studentenverwaltung.css @@ -158,4 +158,8 @@ html { .tiny-90 div.tox.tox-tinymce { height: 90% !important; +} + +.foto-container:hover .fotoedit { + opacity: 1 !important; } \ No newline at end of file diff --git a/public/css/Vertragsverwaltung.css b/public/css/Vertragsverwaltung.css index 7b2d71481..300ee5d55 100644 --- a/public/css/Vertragsverwaltung.css +++ b/public/css/Vertragsverwaltung.css @@ -18,3 +18,7 @@ html { .vv { margin-left: 0 !important; } + +.foto-container:hover .fotoedit { + opacity: 1 !important; +} diff --git a/public/js/api/factory/fotoHandling.js b/public/js/api/factory/fotoHandling.js new file mode 100644 index 000000000..8fffaef07 --- /dev/null +++ b/public/js/api/factory/fotoHandling.js @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + uploadFoto(person_id, params) { + return { + method: 'post', + url: 'api/frontend/v1/fotoHandling/Foto/uploadFoto/' + person_id, + params + }; + }, + deleteFoto(person_id){ + return { + method: 'post', + url: 'api/frontend/v1/fotoHandling/Foto/deleteFoto/' + person_id + }; + } +} \ No newline at end of file diff --git a/public/js/components/DetailHeader/DetailHeader.js b/public/js/components/DetailHeader/DetailHeader.js index ae5eef53e..b821620e1 100644 --- a/public/js/components/DetailHeader/DetailHeader.js +++ b/public/js/components/DetailHeader/DetailHeader.js @@ -1,7 +1,12 @@ import ApiDetailHeader from "../../api/factory/detailHeader.js"; +import ApiHandleFoto from "../../api/factory/fotoHandling.js"; +import ModalUploadFoto from "./Modal/UploadFoto.js"; export default { name: 'DetailHeader', + components: { + ModalUploadFoto + }, inject: { domain: { from: 'configDomain', @@ -21,6 +26,11 @@ export default { type: String, required: false }, + fotoEditable: { + type: Boolean, + required: false, + default: false + }, typeHeader: { type: String, default: 'student', @@ -44,26 +54,14 @@ export default { }, created(){ if(this.person_id) { - this.getHeader(this.person_id); - this.loadDepartmentData(this.mitarbeiter_uid) - .then(() => { - // Call getLeitungOrg only after departmentData is loaded - this.getLeitungOrg(this.departmentData.oe_kurzbz); - }) - .catch((error) => { - console.error("Error loading department data: ", error); - }); + this.loadHeaderData(); } }, watch: { person_id: { handler(newVal) { if (newVal) { - this.getHeader(this.person_id); - this.loadDepartmentData(this.mitarbeiter_uid). - then(() => { - this.getLeitungOrg(this.departmentData.oe_kurzbz); - }); + this.loadHeaderData(); } }, deep: true, @@ -74,9 +72,21 @@ export default { headerDataMa: {}, departmentData: {}, leitungData: {}, + isFetchingIssues: false }; }, methods: { + loadHeaderData(){ + this.getHeader(this.person_id); + this.loadDepartmentData(this.mitarbeiter_uid) + .then(() => { + // Call getLeitungOrg only after departmentData is loaded + this.getLeitungOrg(this.departmentData.oe_kurzbz); + }) + .catch((error) => { + console.error("Error loading header data: ", error); + }); + }, getHeader(person_id) { return this.$api .call(ApiDetailHeader.getHeader(person_id)) @@ -107,17 +117,63 @@ export default { person_id: this.leitungData.person_id, uid: this.leitungData.uid }); - } + }, + showModal(person_id){ + this.$refs.modalFoto.open(person_id); + }, + showDeleteModal(person_id){ + this.$fhcAlert + .confirmDelete() + .then(result => result + ? person_id + : Promise.reject({handled: true})) + .then(this.deleteFoto) + .catch(this.$fhcAlert.handleSystemError); + }, + deleteFoto(person_id){ + return this.$api + .call(ApiHandleFoto.deleteFoto(person_id)) + .then(result => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete')); + }) + .catch(this.$fhcAlert.handleSystemError) + .finally(()=> { + this.reload(); + }); + }, + reload() { + if(this.person_id) { + this.loadHeaderData(); + } + else { + this.$emit('reload'); + } + }, }, template: `
+ + + + + + {{person.uid}}
@@ -171,8 +243,8 @@ export default { + @@ -213,6 +301,11 @@ export default {
+

PNr

{{ headerDataMa?.person_id }}
diff --git a/public/js/components/DetailHeader/Modal/UploadFoto.js b/public/js/components/DetailHeader/Modal/UploadFoto.js new file mode 100644 index 000000000..34f569828 --- /dev/null +++ b/public/js/components/DetailHeader/Modal/UploadFoto.js @@ -0,0 +1,99 @@ +import BsModal from "../../Bootstrap/Modal.js"; +import FormForm from "../../Form/Form.js"; +import FormInput from "../../Form/Input.js"; + +import ApiHandleFoto from "../../../../js/api/factory/fotoHandling.js"; + +export default { + name: "modalUploadFoto", + components: { + BsModal, + FormForm, + FormInput, + }, + props: { + person_id: { + type: Number, + required: true + } + }, + data(){ + return{ + formData: { + file: null, + preview: null, + base64Image: null, + }, + } + }, + methods:{ + open(person_id){ + this.$refs.modalUploadFoto.show(person_id); + }, + onFileChange(e) { + this.file = e.target.files[0]; + if (!this.file) return; + + // convert File in base64 + const reader = new FileReader(); + reader.onload = (event) => { + this.base64Image = event.target.result; + this.preview = this.base64Image; + }; + reader.readAsDataURL(this.file); + }, + + async uploadImage(person_id) { + if (!this.base64Image) { + this.$fhcAlert.alertInfo(this.$p.t('header', 'alert_chooseFoto')); + return; + } + + return this.$api + .call(ApiHandleFoto.uploadFoto(person_id, { + image: this.base64Image, // Base64 String + filename: this.file.name // Original name of file + })) + .then(result => { + this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successFotoUpload')); + this.resetModal(); + this.$refs.modalUploadFoto.hide(); + this.$emit('reload'); + }) + .catch(this.$fhcAlert.handleSystemError); + }, + resetModal(){ + this.formData.foto = []; + } + }, + template: ` + + + + + +
+ +
+

Preview:

+ +
+
+
+ + + +
+ `, +} \ No newline at end of file diff --git a/public/js/components/Stv/Studentenverwaltung.js b/public/js/components/Stv/Studentenverwaltung.js index d0543ad59..1cfcbb1d7 100644 --- a/public/js/components/Stv/Studentenverwaltung.js +++ b/public/js/components/Stv/Studentenverwaltung.js @@ -371,7 +371,7 @@ export default { diff --git a/public/js/components/Stv/Studentenverwaltung/Details.js b/public/js/components/Stv/Studentenverwaltung/Details.js index 036435dd8..dadb5ea6f 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details.js +++ b/public/js/components/Stv/Studentenverwaltung/Details.js @@ -41,7 +41,10 @@ export default { reload() { if (this.$refs.tabs?.$refs?.current?.reload) this.$refs.tabs.$refs.current.reload(); - } + }, + reloadList() { + this.$emit('reload'); + }, }, created() { this.$api @@ -67,6 +70,7 @@ export default { 'core', + 'category' => 'header', + 'phrase' => 'error_fotoupload', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Fehler beim Speichern des Fotos', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Error saving photo', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'ui', + 'phrase' => 'successFotoUpload', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Foto erfolgreich hochgeladen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Photoupload successful', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'header', + 'phrase' => 'alert_chooseFoto', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Bitte zuerst ein Bild auswählen!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Please select an image first!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'header', + 'phrase' => 'error_noPhoto', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Kein Bild empfangen', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'No picture received', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + //**************************** DetailHeader end );