add logic to edit and delete Personfoto

This commit is contained in:
ma0068
2025-09-30 10:50:01 +02:00
parent 5326f961c6
commit 3664428467
12 changed files with 581 additions and 20 deletions
@@ -0,0 +1,237 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
use \DateTime as DateTime;
class Foto extends FHCAPI_Controller
{
public function __construct()
{
parent::__construct([
'uploadFoto' => ['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;
}
}
@@ -423,4 +423,8 @@ class Person_model extends DB_Model
return success($result);
}
}
}
+4
View File
@@ -158,4 +158,8 @@ html {
.tiny-90 div.tox.tox-tinymce {
height: 90% !important;
}
.foto-container:hover .fotoedit {
opacity: 1 !important;
}
+4
View File
@@ -18,3 +18,7 @@ html {
.vv {
margin-left: 0 !important;
}
.foto-container:hover .fotoedit {
opacity: 1 !important;
}
+32
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
};
}
}
+111 -18
View File
@@ -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: `
<div class="core-header d-flex justify-content-start align-items-center w-100 overflow-auto pb-3 gap-3" style="max-height:9rem; min-width: 37.5rem;">
<modal-upload-foto
v-if="person_id"
ref="modalFoto"
:person_id="person_id"
@reload="reload"
>
</modal-upload-foto>
<modal-upload-foto
v-else
ref="modalFoto"
:person_id="headerData[0].person_id"
@reload="reload"
>
</modal-upload-foto>
<template v-if="typeHeader==='student'">
<div
v-for="person in headerData"
:key="person.person_id"
class="d-flex flex-column align-items-center h-100 position-relative d-inline-block"
class="foto-container d-flex flex-column align-items-center h-100 position-relative d-inline-block"
>
<img
class="d-block h-100 rounded"
@@ -131,6 +187,22 @@ export default {
style="z-index: 1; font-size: 1rem; width: 1.25rem; height: 1.25rem;"
></i>
</template>
<template v-if="fotoEditable">
<button
type="button"
class="fotoedit btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute start-0"
style="z-index: 4; font-size: 1rem; width: 1.5rem; height: 1.5rem; opacity:0; transition: opacity 0.2s; top:20%; left:2rem;"
@click="showDeleteModal(headerData[0].person_id)">
<i class="fa fa-xmark"></i>
</button>
<button
type="button"
class="fotoedit btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute end-0"
style="z-index: 4; font-size: 1rem; width: 1.5rem; height: 1.5rem; opacity:0; transition: opacity 0.2s; top:20%; right:2rem;"
@click="showModal(headerData[0].person_id)">
<i class="fa fa-pen"></i>
</button>
</template>
<small class="text-muted">{{person.uid}}</small>
</div>
@@ -171,8 +243,8 @@ export default {
</template>
<template v-if="typeHeader==='mitarbeiter'">
<div class="col-md-2 d-flex justify-content-start align-items-center w-30 pb-3 gap-3 mt-3 position-relative" style="max-height: 8rem; max-width: 6rem; overflow: hidden;">
<div class="foto-container col-md-2 d-flex justify-content-start align-items-center w-30 pb-3 gap-3 mt-3 position-relative" style="max-height: 8rem; max-width: 6rem; overflow: hidden;">
<img
class="d-block w-100 h-100 rounded"
alt="Profilbild"
@@ -185,6 +257,22 @@ export default {
style="z-index: 1; font-size: 1rem; width: 1.25rem; height: 1.25rem;"
></i>
</template>
<template v-if="fotoEditable">
<button
type="button"
class="fotoedit btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute start-0"
style="z-index: 104; font-size: 1rem; width: 2.5rem; height: 2.5rem; opacity:0; transition: opacity 0.2s; top:13%;"
@click="showDeleteModal(person_id)">
<i class="fa fa-xmark"></i>
</button>
<button
type="button"
class="fotoedit btn btn-outline-dark btn-sm d-flex justify-content-center align-items-center position-absolute end-0"
style="z-index: 104; font-size: 1rem; width: 2.5rem; height: 2.5rem; opacity:0; transition: opacity 0.2s; top:13%;"
@click="showModal(person_id)">
<i class="fa fa-pen"></i>
</button>
</template>
</div>
<!--show Ma-Details-->
@@ -213,6 +301,11 @@ export default {
<div class="col-md-1 d-flex flex-column align-items-end justify-content-start ms-auto">
<div class="d-flex py-1">
<!-- TODO slots for Issue Tracking and Tags
<class="px-2">
<h4 class="mb-1">Issues<a class="refresh-issues" title="checkIssues Person" @click="checkPerson"><i class="fas fa-sync"></i></a></h4>
<h6 v-if="!isFetchingIssues" class="text-muted">122</h6>
</div> -->
<div class="px-2">
<h4 class="mb-1">PNr</h4>
<h6 class="text-muted">{{ headerDataMa?.person_id }}</h6>
@@ -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: `
<bs-modal
ref="modalUploadFoto"
dialog-class="modal-dialog-scrollable"
>
<template #title>
Upload Foto
</template>
<form-form
ref="formUploadFoto"
>
<div>
<input type="file" @change="onFileChange" accept="image/*" />
<div v-if="preview">
<p>Preview:</p>
<img :src="preview" width="200" />
</div>
</div>
</form-form>
<template #footer>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="button" class="btn btn-primary" @click="uploadImage(person_id)">{{$p.t('ui', 'hochladen')}}</button>
</div>
</template>
</bs-modal>
`,
}
@@ -371,7 +371,7 @@ export default {
<stv-list ref="stvList" v-model:selected="selected" :studiengang-kz="studiengangKz" :studiensemester-kurzbz="studiensemesterKurzbz"></stv-list>
</template>
<template #bottom>
<stv-details ref="details" :students="selected"></stv-details>
<stv-details ref="details" :students="selected" @reload="reloadList"></stv-details>
</template>
</vertical-split>
</main>
@@ -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 {
<fhc-header
:headerData="students"
typeHeader="student"
@reload="reloadList"
>
</fhc-header>
<fhc-tabs
@@ -24,6 +24,7 @@ export default {
}
},
methods:{
open(prestudent_id, dokument_kurzbz){
this.formData.dokument_kurzbz = dokument_kurzbz;
this.formData.prestudent_id = prestudent_id;
@@ -63,6 +63,7 @@ export default {
:person_id="person_id"
:mitarbeiter_uid="this.mitarbeiter_uid"
typeHeader="mitarbeiter"
fotoEditable
@redirectToLeitung="redirectToLeitung"
></fhc-header>
<vertraege-mitarbeiter
+82
View File
@@ -48911,6 +48911,88 @@ and represent the current state of research on the topic. The prescribed citatio
)
),
//**************************** CORE/search end
//**************************** DetailHeader start
array(
'app' => '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
);