feat: open_workshop_employee_imagegenerator - Thekenheld Badge-Generator als Odoo Modul
- OWL Component mit 3-step Wizard (Upload → Crop → Text/Preview) - Cropper.js für Bildausschnitt (369×492px) - html2canvas für PNG-Generierung (794×1123px A4) - Badge-Template mit originalen CSS von Thekenheld - Speicherung in hr.employee.image_1920
This commit is contained in:
parent
1abeb97afa
commit
1c9829a7aa
139
open_workshop_employee_imagegenerator/README.md
Normal file
139
open_workshop_employee_imagegenerator/README.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# Open Workshop - Employee Image Generator
|
||||
|
||||
Erstelle professionelle Namensschilder (Thekenschilder) für Mitarbeiter direkt in Odoo.
|
||||
|
||||
## Beschreibung
|
||||
|
||||
Dieses Modul ersetzt die externe "thekenheld" WebApp und integriert die Funktionalität direkt in Odoo's HR-Modul. Mitarbeiter-Namensschilder können nun bequem aus dem Employee-Formular heraus erstellt werden.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Foto-Upload & Zuschnitt**: Cropper.js Integration für professionelle Bildbearbeitung
|
||||
- ✅ **Festes Seitenverhältnis**: 369x492 Pixel (optimiert für Thekenschilder)
|
||||
- ✅ **Text-Overlay**: Name und Schwerpunkte werden automatisch hinzugefügt
|
||||
- ✅ **Direkte Integration**: Button im Employee-Formular
|
||||
- ✅ **Sofortige Verfügbarkeit**: Generiertes Bild wird direkt als Employee Avatar gespeichert
|
||||
- ✅ **POS Integration**: Bilder werden automatisch im Customer Display angezeigt (via open_workshop_pos_customer_display)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Modul in Odoo installieren
|
||||
2. Berechtigungen: HR / Mitarbeiter Module erforderlich
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Namensschild erstellen:
|
||||
|
||||
1. Öffne einen Mitarbeiter-Datensatz (HR → Mitarbeiter)
|
||||
2. Klicke auf den Button **"Namensschild erstellen"** im Header
|
||||
3. **Schritt 1**: Wähle ein Foto aus (mindestens 369x492 Pixel empfohlen)
|
||||
4. **Schritt 2**: Schneide das Foto zu - bewege und zoome für den perfekten Ausschnitt
|
||||
5. **Schritt 3**: Überprüfe Name und Schwerpunkte, passe bei Bedarf an
|
||||
6. **Vorschau**: Sieh dir das finale Namensschild an
|
||||
7. Klicke **"Speichern"** - das Bild wird als Employee Avatar gesetzt
|
||||
|
||||
### Schwerpunkte definieren:
|
||||
|
||||
Im Employee-Formular gibt es ein neues Feld **"Schwerpunkte"** (job_focus):
|
||||
- Trage hier die Tätigkeitsschwerpunkte ein
|
||||
- Z.B. "Fahrrad-Reparatur, E-Bikes, Werkstatt"
|
||||
- Max. 2 Zeilen für optimale Darstellung
|
||||
- Falls leer, wird alternativ das job_title Feld verwendet
|
||||
|
||||
## Technische Details
|
||||
|
||||
### Architektur
|
||||
|
||||
**Frontend-only Lösung** mit:
|
||||
- **OWL Component**: Client Action für Dialog
|
||||
- **Cropper.js**: Professionelles Bild-Cropping
|
||||
- **Canvas API**: Text-Overlay Generation
|
||||
- **Base64 Upload**: Direktes Speichern als image_1920
|
||||
|
||||
### Dateien
|
||||
|
||||
```
|
||||
open_workshop_employee_imagegenerator/
|
||||
├── __init__.py
|
||||
├── __manifest__.py
|
||||
├── models/
|
||||
│ ├── __init__.py
|
||||
│ └── hr_employee.py # Erweiterung von hr.employee
|
||||
├── views/
|
||||
│ └── hr_employee_views.xml # Button im Form + job_focus Feld
|
||||
├── static/
|
||||
│ ├── src/
|
||||
│ │ ├── js/
|
||||
│ │ │ └── employee_image_widget.js # OWL Component
|
||||
│ │ ├── xml/
|
||||
│ │ │ └── employee_image_widget.xml # Template
|
||||
│ │ └── css/
|
||||
│ │ └── employee_image_widget.css # Styles
|
||||
│ └── lib/
|
||||
│ └── cropperjs/
|
||||
│ ├── cropper.js # Cropper.js Library
|
||||
│ └── cropper.css # Cropper.js Styles
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
1. User klickt "Namensschild erstellen"
|
||||
2. Python Action öffnet Client Action mit Context (employee_id, name, job_focus)
|
||||
3. JavaScript Dialog (3 Schritte):
|
||||
- Upload → FileReader lädt Bild als Data URL
|
||||
- Crop → Cropper.js erzeugt zugeschnittenes Bild (369x492)
|
||||
- Text → Canvas API zeichnet Text-Overlay
|
||||
4. Base64 Image wird zu hr.employee.image_1920 gespeichert
|
||||
5. Page Reload zeigt neues Bild
|
||||
|
||||
### Canvas Text-Overlay
|
||||
|
||||
Das generierte Namensschild hat:
|
||||
- **Foto**: 369x492 Pixel (Vollbild)
|
||||
- **Text-Overlay**: 120px Höhe am unteren Rand
|
||||
- **Hintergrund**: Halbtransparent weiß (92% Opazität)
|
||||
- **Name**: Fett, 24px, zentriert
|
||||
- **Schwerpunkte**: Normal, 16px, zentriert, max. 2 Zeilen
|
||||
|
||||
## Integration
|
||||
|
||||
### Mit POS Customer Display
|
||||
|
||||
Dieses Modul arbeitet nahtlos mit `open_workshop_pos_customer_display` zusammen:
|
||||
- Generierte Bilder werden automatisch im Customer Display angezeigt
|
||||
- Verwendung der hr.employee.public URL
|
||||
- Keine zusätzliche Konfiguration nötig
|
||||
|
||||
### Ersetzt thekenheld WebApp
|
||||
|
||||
Die externe Docker-Container WebApp ist nicht mehr nötig:
|
||||
- ✅ Keine separate Instanz erforderlich
|
||||
- ✅ Alle Daten in Odoo
|
||||
- ✅ Berechtigungsverwaltung über Odoo
|
||||
- ✅ Besserer Workflow
|
||||
|
||||
## Migration von thekenheld
|
||||
|
||||
Falls bereits Bilder in der alten WebApp erstellt wurden:
|
||||
1. Bilder aus `/var/www/html/uploads/` kopieren
|
||||
2. Manuell in Odoo Employee-Datensätze hochladen
|
||||
3. Oder: Neues Namensschild mit diesem Tool erstellen
|
||||
|
||||
## Abhängigkeiten
|
||||
|
||||
- `hr` - Human Resources Module
|
||||
- `web` - Odoo Web Framework
|
||||
- Cropper.js v1.6.1 (inkludiert)
|
||||
|
||||
## Version
|
||||
|
||||
18.0.1.0.0
|
||||
|
||||
## Lizenz
|
||||
|
||||
LGPL-3
|
||||
|
||||
## Credits
|
||||
|
||||
Entwickelt für Open Workshop / Hobbyhimmel
|
||||
Basierend auf der thekenheld WebApp
|
||||
2
open_workshop_employee_imagegenerator/__init__.py
Normal file
2
open_workshop_employee_imagegenerator/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
53
open_workshop_employee_imagegenerator/__manifest__.py
Normal file
53
open_workshop_employee_imagegenerator/__manifest__.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Open Workshop - Employee Image Generator',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Human Resources',
|
||||
'summary': 'Generate professional employee name badges with photo cropping',
|
||||
'description': """
|
||||
Employee Image Generator
|
||||
=========================
|
||||
|
||||
Creates professional employee name badges (Thekenschilder) directly in Odoo.
|
||||
|
||||
Features:
|
||||
---------
|
||||
* Upload and crop employee photos with fixed aspect ratio (369x492)
|
||||
* Add employee name and job description
|
||||
* Integrated Cropper.js for professional image editing
|
||||
* Direct integration in Employee form
|
||||
* Replaces external thekenheld webapp
|
||||
|
||||
Usage:
|
||||
------
|
||||
1. Go to Employee form
|
||||
2. Click "Namensschild erstellen" button
|
||||
3. Upload photo and crop it
|
||||
4. Enter name and job focus areas
|
||||
5. Save - the generated image becomes the employee avatar
|
||||
""",
|
||||
'author': 'Open Workshop',
|
||||
'website': 'https://www.open-workshop.de',
|
||||
'license': 'LGPL-3',
|
||||
'depends': ['hr', 'web'],
|
||||
'data': [
|
||||
'views/hr_employee_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
# Cropper.js CSS
|
||||
'open_workshop_employee_imagegenerator/static/lib/cropperjs/cropper.css',
|
||||
'open_workshop_employee_imagegenerator/static/src/css/employee_image_widget.css',
|
||||
'open_workshop_employee_imagegenerator/static/src/css/badge_template.css',
|
||||
# Libraries
|
||||
'open_workshop_employee_imagegenerator/static/lib/cropperjs/cropper.js',
|
||||
'open_workshop_employee_imagegenerator/static/lib/html2canvas.min.js',
|
||||
# Our custom widget
|
||||
'open_workshop_employee_imagegenerator/static/src/js/employee_image_widget.js',
|
||||
'open_workshop_employee_imagegenerator/static/src/xml/employee_image_widget.xml',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
}
|
||||
2
open_workshop_employee_imagegenerator/models/__init__.py
Normal file
2
open_workshop_employee_imagegenerator/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import hr_employee
|
||||
32
open_workshop_employee_imagegenerator/models/hr_employee.py
Normal file
32
open_workshop_employee_imagegenerator/models/hr_employee.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
# Zusätzliches Feld für Schwerpunkte (falls nicht job_title verwendet wird)
|
||||
job_focus = fields.Char(
|
||||
string='Schwerpunkte',
|
||||
help='Tätigkeitsschwerpunkte für Namensschild'
|
||||
)
|
||||
|
||||
def action_open_image_generator(self):
|
||||
"""Öffnet den Image Generator Dialog"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'employee_image_generator',
|
||||
'name': 'Namensschild erstellen',
|
||||
'target': 'new',
|
||||
'params': {
|
||||
'employee_id': self.id,
|
||||
'employee_name': self.name,
|
||||
'job_focus': self.job_focus or self.job_title or '',
|
||||
},
|
||||
'context': {
|
||||
'employee_id': self.id,
|
||||
'employee_name': self.name,
|
||||
'job_focus': self.job_focus or self.job_title or '',
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/* Badge Template CSS - 1:1 von thekenheld print.html */
|
||||
|
||||
.badge-template {
|
||||
position: relative;
|
||||
background: white;
|
||||
width: 21cm;
|
||||
height: 29.7cm;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.badge-template header,
|
||||
.badge-template footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgb(64, 64, 64);
|
||||
padding-right: 1.5cm;
|
||||
padding-left: 1.5cm;
|
||||
height: 30mm;
|
||||
}
|
||||
|
||||
.badge-template header {
|
||||
top: 0;
|
||||
padding-top: 5mm;
|
||||
padding-bottom: 3mm;
|
||||
}
|
||||
|
||||
.badge-template footer {
|
||||
bottom: 0;
|
||||
color: rgb(241, 245, 247);
|
||||
padding-top: 3mm;
|
||||
padding-bottom: 5mm;
|
||||
}
|
||||
|
||||
.badge-template .content {
|
||||
padding-top: 40mm;
|
||||
padding-bottom: 40mm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-template .logo {
|
||||
width: 43mm;
|
||||
height: 20mm;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.badge-template .headline1 {
|
||||
height: 13mm;
|
||||
font-size: 10mm;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-template .headline2 {
|
||||
height: 9mm;
|
||||
font-size: 6mm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-template .headline3 {
|
||||
height: 10mm;
|
||||
font-size: 7mm;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-template .centered-image {
|
||||
height: 130mm;
|
||||
width: 100mm;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 5mm 0 0 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.badge-template .your-name {
|
||||
height: 22mm;
|
||||
font-size: 19mm;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.badge-template .topics {
|
||||
height: 10mm;
|
||||
font-size: 8mm;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-template .warnung {
|
||||
font-size: 10mm;
|
||||
color: red;
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/* Employee Image Generator Styles */
|
||||
|
||||
.o_employee_image_generator {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .img-container {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .img-container img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.o_employee_image_generator canvas {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Cropper.js Container Anpassungen */
|
||||
.o_employee_image_generator .cropper-container {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.o_employee_image_generator .progress {
|
||||
height: 3px;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .progress-bar {
|
||||
background-color: #0d6efd;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Step Indicators */
|
||||
.o_employee_image_generator .step-indicator {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Upload Area */
|
||||
.o_employee_image_generator .upload-area {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .upload-area:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Canvas Preview */
|
||||
.o_employee_image_generator canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Modal Anpassungen */
|
||||
.o_employee_image_generator .modal-dialog {
|
||||
margin: 1.75rem auto;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-content {
|
||||
border: none;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-footer {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.o_employee_image_generator .modal-dialog {
|
||||
max-width: 95%;
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
.o_employee_image_generator .modal-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component, useState, useRef, onMounted } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
/**
|
||||
* Employee Image Generator Client Action
|
||||
*
|
||||
* Ermöglicht das Erstellen von professionellen Namensschildern für Mitarbeiter.
|
||||
* Verwendet Cropper.js für Bildbearbeitung und Canvas API für Text-Overlay.
|
||||
*/
|
||||
class EmployeeImageGenerator extends Component {
|
||||
static template = "open_workshop_employee_imagegenerator.ImageGeneratorDialog";
|
||||
static props = {
|
||||
"*": true,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.notification = useService("notification");
|
||||
|
||||
// Hole Context aus props oder action
|
||||
const context = this.props.action?.context || this.props.context || {};
|
||||
|
||||
this.state = useState({
|
||||
step: 1, // 1: Upload, 2: Crop, 3: Text, 4: Preview
|
||||
employeeId: context.employee_id,
|
||||
employeeName: context.employee_name || '',
|
||||
jobFocus: context.job_focus || '',
|
||||
companyLogo: null,
|
||||
imageDataUrl: null,
|
||||
croppedImageDataUrl: null,
|
||||
finalImageDataUrl: null,
|
||||
});
|
||||
|
||||
this.fileInput = useRef("fileInput");
|
||||
this.imagePreview = useRef("imagePreview");
|
||||
this.cropperImage = useRef("cropperImage");
|
||||
this.badgeTemplate = useRef("badgeTemplate");
|
||||
|
||||
this.cropper = null;
|
||||
// A4-Format wie Original: 794×1123px (wie thekenheld)
|
||||
this.targetWidth = 794;
|
||||
this.targetHeight = 1123;
|
||||
// Umrechnungsfaktor: 793px / 210mm = 3.776 px/mm
|
||||
this.pxPerMm = this.targetWidth / 210;
|
||||
|
||||
// Lade Company-Logo beim Start
|
||||
this.loadCompanyLogo();
|
||||
}
|
||||
|
||||
async loadCompanyLogo() {
|
||||
try {
|
||||
// Hole das Logo der aktuellen Company
|
||||
const company = await this.orm.call('res.company', 'search_read', [[]], {
|
||||
fields: ['logo'],
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (company && company.length > 0 && company[0].logo) {
|
||||
// Company-Logo ist base64-codiert, füge data:image/png prefix hinzu
|
||||
this.state.companyLogo = `data:image/png;base64,${company[0].logo}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Konnte Company-Logo nicht laden:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 1: Datei-Upload
|
||||
onFileSelect(ev) {
|
||||
const files = ev.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
// Prüfe ob es ein Bild ist
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.notification.add('Bitte wähle eine Bilddatei aus', {
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Lade Bild als Data URL
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.state.imageDataUrl = e.target.result;
|
||||
this.state.step = 2;
|
||||
|
||||
// Initialisiere Cropper im nächsten Tick
|
||||
setTimeout(() => this.initCropper(), 100);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 2: Initialisiere Cropper.js
|
||||
initCropper() {
|
||||
if (this.cropper) {
|
||||
this.cropper.destroy();
|
||||
}
|
||||
|
||||
const imageElement = this.cropperImage.el;
|
||||
if (!imageElement || !window.Cropper) {
|
||||
console.error('Cropper.js not loaded or image element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const aspectRatio = this.targetWidth / this.targetHeight;
|
||||
|
||||
this.cropper = new window.Cropper(imageElement, {
|
||||
aspectRatio: aspectRatio,
|
||||
viewMode: 3,
|
||||
dragMode: 'move',
|
||||
autoCropArea: 1,
|
||||
restore: false,
|
||||
guides: true,
|
||||
center: true,
|
||||
highlight: false,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Bild zuschneiden und weiter zu Schritt 3
|
||||
cropImage() {
|
||||
if (!this.cropper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.cropper.getCroppedCanvas({
|
||||
width: this.targetWidth,
|
||||
height: this.targetHeight,
|
||||
});
|
||||
|
||||
if (canvas) {
|
||||
this.state.croppedImageDataUrl = canvas.toDataURL('image/jpeg', 0.9);
|
||||
this.state.step = 3;
|
||||
|
||||
// Destroy cropper um Ressourcen freizugeben
|
||||
this.cropper.destroy();
|
||||
this.cropper = null;
|
||||
|
||||
// Gehe zu Schritt 3 (das Template ist bereits sichtbar via t-if)
|
||||
// Die Vorschau wird automatisch angezeigt durch das HTML-Template
|
||||
}
|
||||
}
|
||||
|
||||
// Schritt 3: Text-Änderung → Vorschau ist Live durch HTML-Template
|
||||
onTextChange() {
|
||||
// Das Template aktualisiert sich automatisch durch t-model
|
||||
// Keine zusätzliche Logik nötig - OWL reactive state macht das automatisch!
|
||||
}
|
||||
|
||||
// Schritt 4: Finale Speicherung - Konvertiere HTML-Template zu Bild mit html2canvas
|
||||
async saveFinalImage() {
|
||||
try {
|
||||
const badgeElement = this.badgeTemplate.el;
|
||||
if (!badgeElement) {
|
||||
this.notification.add('Badge-Template nicht gefunden', { type: 'danger' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Konvertiere HTML-Element zu Canvas mit html2canvas (exakt wie thekenheld)
|
||||
const canvas = await window.html2canvas(badgeElement, {
|
||||
width: this.targetWidth,
|
||||
height: this.targetHeight,
|
||||
});
|
||||
|
||||
// Konvertiere Canvas zu Base64 PNG (wie Original thekenheld)
|
||||
const finalImageBase64 = canvas.toDataURL('image/png');
|
||||
|
||||
// Speichere in Odoo (entferne data:image/jpeg;base64, prefix)
|
||||
const imageData = finalImageBase64.split(',')[1];
|
||||
|
||||
await this.orm.write('hr.employee', [this.state.employeeId], {
|
||||
image_1920: imageData,
|
||||
});
|
||||
|
||||
this.notification.add('Namensschild erfolgreich gespeichert!', {
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
// Schließe Dialog und kehre zurück zum Mitarbeiter-Formular
|
||||
await this.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'hr.employee',
|
||||
res_id: this.state.employeeId,
|
||||
views: [[false, 'form']],
|
||||
target: 'current',
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
this.notification.add(`Fehler beim Speichern: ${error.message}`, {
|
||||
type: 'danger',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Abbrechen
|
||||
cancel() {
|
||||
if (this.props.close) {
|
||||
this.props.close();
|
||||
} else {
|
||||
this.action.doAction({
|
||||
type: 'ir.actions.act_window_close'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Zurück zum vorherigen Schritt
|
||||
previousStep() {
|
||||
if (this.state.step > 1) {
|
||||
this.state.step--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Registriere als Client Action
|
||||
registry.category("actions").add("employee_image_generator", EmployeeImageGenerator);
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="open_workshop_employee_imagegenerator.ImageGeneratorDialog">
|
||||
<div class="o_employee_image_generator modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-image me-2"/>
|
||||
Namensschild erstellen
|
||||
</h4>
|
||||
<button type="button" class="btn-close" t-on-click="cancel"/>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<!-- Schritt-Anzeige -->
|
||||
<div class="mb-3">
|
||||
<div class="progress" style="height: 3px;">
|
||||
<div class="progress-bar"
|
||||
role="progressbar"
|
||||
t-att-style="'width: ' + (state.step * 33) + '%'"
|
||||
aria-valuenow="state.step"
|
||||
aria-valuemin="1"
|
||||
aria-valuemax="3"/>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-2 text-muted small">
|
||||
<span t-att-class="{'text-primary fw-bold': state.step === 1}">1. Bild hochladen</span>
|
||||
<span t-att-class="{'text-primary fw-bold': state.step === 2}">2. Bild zuschneiden</span>
|
||||
<span t-att-class="{'text-primary fw-bold': state.step === 3}">3. Text & Vorschau</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 1: Upload -->
|
||||
<div t-if="state.step === 1" class="text-center p-5">
|
||||
<i class="fa fa-cloud-upload fa-5x text-muted mb-3"/>
|
||||
<h5>Wähle ein Foto aus</h5>
|
||||
<p class="text-muted">Das Bild sollte mindestens 369x492 Pixel groß sein</p>
|
||||
<input type="file"
|
||||
t-ref="fileInput"
|
||||
class="d-none"
|
||||
accept="image/*"
|
||||
t-on-change="onFileSelect"/>
|
||||
<button class="btn btn-primary btn-lg"
|
||||
t-on-click="() => this.fileInput.el.click()">
|
||||
<i class="fa fa-folder-open me-2"/>
|
||||
Datei auswählen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 2: Crop -->
|
||||
<div t-if="state.step === 2">
|
||||
<div class="alert alert-info mb-3">
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
Bewege und zoome das Bild, um den gewünschten Ausschnitt zu wählen
|
||||
</div>
|
||||
<div class="img-container" style="max-height: 500px; overflow: hidden;">
|
||||
<img t-ref="cropperImage"
|
||||
t-att-src="state.imageDataUrl"
|
||||
style="max-width: 100%; display: block;"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 3: Text und Vorschau -->
|
||||
<div t-if="state.step === 3">
|
||||
<div class="row">
|
||||
<!-- Linke Spalte: Eingabefelder -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">Textinformationen</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="employeeName" class="form-label">Name</label>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="employeeName"
|
||||
t-model="state.employeeName"
|
||||
t-on-input="onTextChange"
|
||||
placeholder="Max Mustermann"/>
|
||||
<small class="form-text text-muted">
|
||||
Dieser Name wird auf dem Namensschild angezeigt
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="jobFocus" class="form-label">Schwerpunkte</label>
|
||||
<textarea class="form-control"
|
||||
id="jobFocus"
|
||||
rows="3"
|
||||
t-model="state.jobFocus"
|
||||
t-on-input="onTextChange"
|
||||
placeholder="z.B. Fahrrad-Reparatur, E-Bikes, Werkstatt"/>
|
||||
<small class="form-text text-muted">
|
||||
Tätigkeitsbereiche oder Spezialisierung (optional)
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-light">
|
||||
<strong>Tipp:</strong> Kurze Texte sehen besser aus.
|
||||
Maximal 2 Zeilen für Schwerpunkte.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Spalte: Vorschau -->
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">Vorschau</h5>
|
||||
<div class="border rounded p-2 bg-light" style="overflow-y: auto; max-height: 600px;">
|
||||
<!-- HTML Badge Template (wird mit html2canvas konvertiert) -->
|
||||
<div t-ref="badgeTemplate" class="badge-template">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img t-if="state.companyLogo" t-att-src="state.companyLogo" alt="Logo" class="logo"/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content">
|
||||
<div class="headline1">An der Theke für Dich</div>
|
||||
<div class="headline2">Werkstattaufsicht und Werkzeugausgabe</div>
|
||||
<img t-if="state.croppedImageDataUrl"
|
||||
t-att-src="state.croppedImageDataUrl"
|
||||
alt="avatar"
|
||||
class="centered-image"/>
|
||||
<div class="your-name" t-esc="state.employeeName || 'Dein Name'"/>
|
||||
<hr/>
|
||||
<div class="headline3">Themenschwerpunkte:</div>
|
||||
<div class="topics" t-esc="state.jobFocus || 'Deine Schwerpunkte'"/>
|
||||
<div class="warnung">! Zugang zur Theke nur für Berechtigte !</div>
|
||||
</div>
|
||||
<footer></footer>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2 text-center">
|
||||
A4-Format (793 × 1122 Pixel) - 50% Vorschau
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-footer">
|
||||
<button type="button"
|
||||
class="btn btn-secondary"
|
||||
t-on-click="cancel">
|
||||
Abbrechen
|
||||
</button>
|
||||
|
||||
<button t-if="state.step > 1 and state.step < 3"
|
||||
type="button"
|
||||
class="btn btn-light"
|
||||
t-on-click="previousStep">
|
||||
<i class="fa fa-arrow-left me-2"/>
|
||||
Zurück
|
||||
</button>
|
||||
|
||||
<button t-if="state.step === 2"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
t-on-click="cropImage">
|
||||
Weiter
|
||||
<i class="fa fa-arrow-right ms-2"/>
|
||||
</button>
|
||||
|
||||
<button t-if="state.step === 3"
|
||||
type="button"
|
||||
class="btn btn-success"
|
||||
t-on-click="saveFinalImage">
|
||||
<i class="fa fa-check me-2"/>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Erweitere das Employee Form View -->
|
||||
<record id="view_employee_form_image_generator" model="ir.ui.view">
|
||||
<field name="name">hr.employee.form.image.generator</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Füge Button zum Header hinzu -->
|
||||
<xpath expr="//sheet" position="before">
|
||||
<header>
|
||||
<button name="action_open_image_generator"
|
||||
type="object"
|
||||
string="Namensschild erstellen"
|
||||
class="oe_highlight"
|
||||
icon="fa-image"
|
||||
help="Erstelle ein professionelles Namensschild mit Foto"/>
|
||||
</header>
|
||||
</xpath>
|
||||
|
||||
<!-- Füge job_focus Feld nach job_title ein -->
|
||||
<xpath expr="//field[@name='job_title']" position="after">
|
||||
<field name="job_focus" placeholder="z.B. Fahrrad-Reparatur, E-Bikes, Werkstatt"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user