Version 2.0.0: PWA Mobile App + Produktliste-Verbesserungen
PWA (neue Dateien): - Vollständige Progressive Web App mit Token-basierter Auth - 4 Swipe-Panels: Alle STZ, Stundenzettel, Produktliste, Lieferauflistung - Kundensuche, Leistungen-Accordion, Mehraufwand-Sektion - Produkt-Übernahme aus Auftrag + Mehraufwand in STZ - Service Worker, Manifest, App-Icons für Installation Desktop-Änderungen: - Produktliste: Checkboxen immer sichtbar (außer bereits auf STZ) - Lieferauflistung: Vereinfachte Ansicht (nur Verbaut-Spalte) - Admin: PWA-Link in Einstellungen - Sprachdatei: PWA-Übersetzungen Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6257713e5b
commit
292db5d40c
32 changed files with 5262 additions and 96 deletions
22
admin/setup.php
Normal file → Executable file
22
admin/setup.php
Normal file → Executable file
|
|
@ -212,6 +212,28 @@ print '</tr>';
|
|||
|
||||
print '</table>';
|
||||
|
||||
// PWA Mobile App Bereich
|
||||
print '<br>';
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th class="titlefield">'.$langs->trans("PWAMobileApp").'</th>';
|
||||
print '<th style="width: 300px;">'.$langs->trans("Value").'</th>';
|
||||
print '<th class="right" style="width: 100px;"></th>';
|
||||
print '</tr>';
|
||||
|
||||
$pwaUrl = dol_buildpath('/stundenzettel/pwa.php', 2);
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.$langs->trans("PWADescription").'<br><small class="opacitymedium">'.$langs->trans("PWAInstallHint").'</small></td>';
|
||||
print '<td>';
|
||||
print '<a href="'.$pwaUrl.'" target="_blank" class="butAction small" style="display: inline-block; text-decoration: none;">';
|
||||
print $langs->trans("PWALink").' ↗';
|
||||
print '</a>';
|
||||
print '</td>';
|
||||
print '<td></td>';
|
||||
print '</tr>';
|
||||
|
||||
print '</table>';
|
||||
|
||||
print dol_get_fiche_end();
|
||||
|
||||
llxFooter();
|
||||
|
|
|
|||
0
ajax/add_leistung.php
Normal file → Executable file
0
ajax/add_leistung.php
Normal file → Executable file
0
ajax/add_product.php
Normal file → Executable file
0
ajax/add_product.php
Normal file → Executable file
1564
ajax/pwa_api.php
Normal file
1564
ajax/pwa_api.php
Normal file
File diff suppressed because it is too large
Load diff
144
ajax/pwa_auth.php
Normal file
144
ajax/pwa_auth.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* Stundenzettel PWA - Token-basierte Authentifizierung
|
||||
*/
|
||||
|
||||
if (!defined('NOLOGIN')) {
|
||||
define('NOLOGIN', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREMENU')) {
|
||||
define('NOREQUIREMENU', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREHTML')) {
|
||||
define('NOREQUIREHTML', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREAJAX')) {
|
||||
define('NOREQUIREAJAX', '1');
|
||||
}
|
||||
|
||||
// Dolibarr-Umgebung laden
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
|
||||
if (!$res) die(json_encode(array('success' => false, 'error' => 'Dolibarr nicht geladen')));
|
||||
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
|
||||
$action = GETPOST('action', 'aZ09');
|
||||
$response = array('success' => false);
|
||||
|
||||
switch ($action) {
|
||||
case 'login':
|
||||
$username = GETPOST('username', 'alphanohtml');
|
||||
$password = GETPOST('password', 'none');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$response['error'] = 'Benutzername und Passwort erforderlich';
|
||||
break;
|
||||
}
|
||||
|
||||
// Brute-Force-Schutz
|
||||
usleep(100000); // 100ms Verzoegerung
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||
$userLogin = new User($db);
|
||||
|
||||
// Benutzer per Login suchen
|
||||
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."user WHERE login = '".$db->escape($username)."' AND statut = 1";
|
||||
$result = $db->query($sql);
|
||||
|
||||
if ($result && $db->num_rows($result) > 0) {
|
||||
$obj = $db->fetch_object($result);
|
||||
$userLogin->fetch($obj->rowid);
|
||||
$userLogin->getrights();
|
||||
|
||||
// Passwort pruefen
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/security2.lib.php';
|
||||
|
||||
$passOk = false;
|
||||
if (!empty($userLogin->pass_indatabase_crypted)) {
|
||||
$passOk = dol_verifyHash($password, $userLogin->pass_indatabase_crypted);
|
||||
}
|
||||
|
||||
if ($passOk) {
|
||||
// Stundenzettel-Berechtigung pruefen
|
||||
if ($userLogin->hasRight('stundenzettel', 'read')) {
|
||||
// Token generieren (15 Tage gueltig)
|
||||
$tokenData = array(
|
||||
'user_id' => $userLogin->id,
|
||||
'login' => $userLogin->login,
|
||||
'created' => time(),
|
||||
'expires' => time() + (15 * 24 * 60 * 60),
|
||||
'hash' => md5($userLogin->id . $userLogin->login . getDolGlobalString('MAIN_SECURITY_SALT', 'defaultsalt'))
|
||||
);
|
||||
$token = base64_encode(json_encode($tokenData));
|
||||
|
||||
$response['success'] = true;
|
||||
$response['token'] = $token;
|
||||
$response['user'] = array(
|
||||
'id' => $userLogin->id,
|
||||
'login' => $userLogin->login,
|
||||
'name' => $userLogin->getFullName($langs)
|
||||
);
|
||||
} else {
|
||||
$response['error'] = 'Keine Berechtigung fuer Stundenzettel';
|
||||
}
|
||||
} else {
|
||||
$response['error'] = 'Falsches Passwort';
|
||||
}
|
||||
} else {
|
||||
$response['error'] = 'Benutzer nicht gefunden';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'verify':
|
||||
$token = GETPOST('token', 'none');
|
||||
|
||||
if (empty($token)) {
|
||||
$response['error'] = 'Kein Token';
|
||||
break;
|
||||
}
|
||||
|
||||
$tokenData = json_decode(base64_decode($token), true);
|
||||
|
||||
if (!$tokenData || empty($tokenData['user_id']) || empty($tokenData['expires'])) {
|
||||
$response['error'] = 'Ungueltiges Token';
|
||||
break;
|
||||
}
|
||||
|
||||
// Ablaufdatum pruefen
|
||||
if ($tokenData['expires'] < time()) {
|
||||
$response['error'] = 'Token abgelaufen';
|
||||
break;
|
||||
}
|
||||
|
||||
// Hash verifizieren
|
||||
$expectedHash = md5($tokenData['user_id'] . $tokenData['login'] . getDolGlobalString('MAIN_SECURITY_SALT', 'defaultsalt'));
|
||||
if ($tokenData['hash'] !== $expectedHash) {
|
||||
$response['error'] = 'Token manipuliert';
|
||||
break;
|
||||
}
|
||||
|
||||
// Benutzer noch aktiv?
|
||||
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
|
||||
$userCheck = new User($db);
|
||||
if ($userCheck->fetch($tokenData['user_id']) > 0 && $userCheck->statut == 1) {
|
||||
$response['success'] = true;
|
||||
$response['user'] = array(
|
||||
'id' => $userCheck->id,
|
||||
'login' => $userCheck->login,
|
||||
'name' => $userCheck->getFullName($langs)
|
||||
);
|
||||
} else {
|
||||
$response['error'] = 'Benutzer nicht mehr aktiv';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$response['error'] = 'Unbekannte Aktion';
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
$db->close();
|
||||
0
card.php
Normal file → Executable file
0
card.php
Normal file → Executable file
0
class/stundenzettel.class.php
Normal file → Executable file
0
class/stundenzettel.class.php
Normal file → Executable file
1200
css/pwa.css
Normal file
1200
css/pwa.css
Normal file
File diff suppressed because it is too large
Load diff
0
debug_netto.php
Normal file → Executable file
0
debug_netto.php
Normal file → Executable file
BIN
img/icon-192.png
Normal file
BIN
img/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9 KiB |
BIN
img/icon-512.png
Normal file
BIN
img/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
0
index.php
Normal file → Executable file
0
index.php
Normal file → Executable file
6
langs/de_DE/stundenzettel.lang
Normal file → Executable file
6
langs/de_DE/stundenzettel.lang
Normal file → Executable file
|
|
@ -282,3 +282,9 @@ incl = inkl.
|
|||
# Extrafields Aufträge
|
||||
NettoSTZ = Netto STZ
|
||||
NettoSTZHelp = Netto-Wert aller freigegebenen Stundenzettel (Produkte + Arbeitsstunden)
|
||||
|
||||
# PWA Mobile App
|
||||
PWAMobileApp = PWA Mobile App
|
||||
PWALink = Stundenzettel PWA öffnen
|
||||
PWAInstallHint = Am Handy öffnen und "Zum Startbildschirm hinzufügen" wählen
|
||||
PWADescription = Stundenzettel als installierbare Mobile-App für unterwegs
|
||||
|
|
|
|||
0
lib/stundenzettel.lib.php
Normal file → Executable file
0
lib/stundenzettel.lib.php
Normal file → Executable file
0
list.php
Normal file → Executable file
0
list.php
Normal file → Executable file
26
manifest.json
Normal file
26
manifest.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "Stundenzettel",
|
||||
"short_name": "STZ",
|
||||
"description": "Stundenzettel Mobile App - Arbeitszeiten und Material erfassen",
|
||||
"start_url": "./pwa.php",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#1d1e20",
|
||||
"theme_color": "#1d1e20",
|
||||
"lang": "de-DE",
|
||||
"categories": ["business", "productivity"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "img/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "img/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
174
pwa.php
Normal file
174
pwa.php
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
|
||||
*
|
||||
* Stundenzettel PWA - Standalone Mobile App
|
||||
*/
|
||||
|
||||
// Kein Dolibarr-Login erforderlich - eigenes Token-System
|
||||
if (!defined('NOLOGIN')) {
|
||||
define('NOLOGIN', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREMENU')) {
|
||||
define('NOREQUIREMENU', '1');
|
||||
}
|
||||
if (!defined('NOREQUIREHTML')) {
|
||||
define('NOREQUIREHTML', '1');
|
||||
}
|
||||
|
||||
// Dolibarr-Umgebung laden (fuer Theme-Color und Config)
|
||||
$res = 0;
|
||||
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
|
||||
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
|
||||
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
|
||||
if (!$res) die("Dolibarr konnte nicht geladen werden");
|
||||
|
||||
// Theme-Farbe aus Dolibarr
|
||||
$themeColor = getDolGlobalString('THEME_ELDY_TOPMENU_BACK1', '#4390dc');
|
||||
|
||||
?><!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#1d1e20">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="STZ">
|
||||
<title>Stundenzettel</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="img/icon-192.png">
|
||||
<link rel="apple-touch-icon" href="img/icon-192.png">
|
||||
<link rel="stylesheet" href="css/pwa.css?v=2.6">
|
||||
<style>:root { --primary: <?php echo htmlspecialchars($themeColor); ?>; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app">
|
||||
|
||||
<!-- Toast-Container -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<!-- Loading-Overlay -->
|
||||
<div id="loading-overlay" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm-Dialog -->
|
||||
<div id="confirm-dialog" class="confirm-dialog">
|
||||
<div class="confirm-box">
|
||||
<div id="confirm-title" class="confirm-title"></div>
|
||||
<div id="confirm-text" class="confirm-text"></div>
|
||||
<div class="confirm-actions">
|
||||
<button id="confirm-cancel" class="btn btn-ghost">Abbrechen</button>
|
||||
<button id="confirm-ok" class="btn btn-danger">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom-Sheet (wiederverwendbar) -->
|
||||
<div id="bottom-sheet-overlay" class="bottom-sheet-overlay"></div>
|
||||
<div id="bottom-sheet" class="bottom-sheet">
|
||||
<div class="bottom-sheet-handle"></div>
|
||||
<div id="bottom-sheet-header" class="bottom-sheet-header"></div>
|
||||
<div id="bottom-sheet-body" class="bottom-sheet-body"></div>
|
||||
<div id="bottom-sheet-footer" class="bottom-sheet-footer"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== LOGIN SCREEN ===== -->
|
||||
<div id="screen-login" class="screen login-screen active">
|
||||
<div class="login-container">
|
||||
<div class="login-logo">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="login-title">Stundenzettel</h1>
|
||||
<p class="login-subtitle">Mobile Zeiterfassung</p>
|
||||
|
||||
<form id="login-form" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="login-user">Benutzername</label>
|
||||
<input type="text" id="login-user" autocomplete="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-pass">Passwort</label>
|
||||
<input type="password" id="login-pass" autocomplete="current-password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
<p id="login-error" class="error-text"></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== SEARCH SCREEN ===== -->
|
||||
<div id="screen-search" class="screen search-screen">
|
||||
<div class="search-header">
|
||||
<div class="search-input-wrap">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input type="search" id="search-input" placeholder="Kunde suchen..." autocomplete="off">
|
||||
</div>
|
||||
<button id="btn-logout" class="btn btn-ghost btn-small">Abmelden</button>
|
||||
</div>
|
||||
<div id="search-results" class="search-results">
|
||||
<div class="search-empty">
|
||||
<p>Kundenname eingeben um Aufträge zu finden</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== MAIN SCREEN (Swipe-Panels) ===== -->
|
||||
<div id="screen-main" class="screen main-screen">
|
||||
<!-- Tab-Bar -->
|
||||
<div class="tab-bar">
|
||||
<div id="btn-back" class="back-btn">←</div>
|
||||
<div class="tab-items">
|
||||
<div class="tab-item" data-panel="0">Alle STZ</div>
|
||||
<div class="tab-item" data-panel="1">Stundenzettel</div>
|
||||
<div class="tab-item active" data-panel="2">Produktliste</div>
|
||||
<div class="tab-item" data-panel="3">Lieferauflistung</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Swipe-Viewport -->
|
||||
<div class="swipe-viewport">
|
||||
<div id="swipe-container" class="swipe-container">
|
||||
<!-- Panel 0: Alle Stundenzettel -->
|
||||
<div id="panel-stzlist" class="swipe-panel"></div>
|
||||
|
||||
<!-- Panel 1: Stundenzettel (Leistungen + Merkzettel) -->
|
||||
<div id="panel-stundenzettel" class="swipe-panel"></div>
|
||||
|
||||
<!-- Panel 2: Produktliste (Standard) -->
|
||||
<div id="panel-products" class="swipe-panel"></div>
|
||||
|
||||
<!-- Panel 3: Lieferauflistung -->
|
||||
<div id="panel-tracking" class="swipe-panel"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAB -->
|
||||
<button id="fab-add" class="fab hidden">+</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="<?php echo DOL_URL_ROOT; ?>/includes/jquery/js/jquery.min.js"></script>
|
||||
<script>
|
||||
window.STZ_CONFIG = {
|
||||
moduleUrl: '<?php echo dol_buildpath('/stundenzettel/', 1); ?>',
|
||||
apiUrl: '<?php echo dol_buildpath('/stundenzettel/ajax/pwa_api.php', 1); ?>',
|
||||
authUrl: '<?php echo dol_buildpath('/stundenzettel/ajax/pwa_auth.php', 1); ?>'
|
||||
};
|
||||
</script>
|
||||
<script src="js/pwa.js?v=2.6"></script>
|
||||
|
||||
<!-- Service Worker Registration -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('sw.js')
|
||||
.then(function(reg) { console.log('[STZ-PWA] Service Worker registriert'); })
|
||||
.catch(function(err) { console.error('[STZ-PWA] SW Registrierung fehlgeschlagen:', err); });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
0
sql/dolibarr_allversions.sql
Normal file → Executable file
0
sql/dolibarr_allversions.sql
Normal file → Executable file
0
sql/llx_product_services.sql
Normal file → Executable file
0
sql/llx_product_services.sql
Normal file → Executable file
0
sql/llx_stundenzettel.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel.sql
Normal file → Executable file
0
sql/llx_stundenzettel.sql
Normal file → Executable file
0
sql/llx_stundenzettel_leistung.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel_leistung.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel_leistung.sql
Normal file → Executable file
0
sql/llx_stundenzettel_leistung.sql
Normal file → Executable file
0
sql/llx_stundenzettel_note.sql
Normal file → Executable file
0
sql/llx_stundenzettel_note.sql
Normal file → Executable file
0
sql/llx_stundenzettel_product.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel_product.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel_product.sql
Normal file → Executable file
0
sql/llx_stundenzettel_product.sql
Normal file → Executable file
0
sql/llx_stundenzettel_tracking.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel_tracking.key.sql
Normal file → Executable file
0
sql/llx_stundenzettel_tracking.sql
Normal file → Executable file
0
sql/llx_stundenzettel_tracking.sql
Normal file → Executable file
0
sql/update_1.2.0.sql
Normal file → Executable file
0
sql/update_1.2.0.sql
Normal file → Executable file
|
|
@ -1865,12 +1865,12 @@ if ($tab == 'products') {
|
|||
|
||||
print '<tr class="oddeven'.$sectionClass.'">';
|
||||
|
||||
// Checkbox (nur bei offenen anzeigen UND wenn noch nicht auf diesem Stundenzettel)
|
||||
// Checkbox immer anzeigen, ausser wenn bereits auf diesem Stundenzettel
|
||||
$isAlreadyOnStz = isset($alreadyOnStundenzettel[$obj->rowid]);
|
||||
print '<td class="center"'.$styleFirst.'>';
|
||||
if (!$isDone && !$isAlreadyOnStz) {
|
||||
if (!$isAlreadyOnStz) {
|
||||
print '<input type="checkbox" name="selected[]" value="'.$obj->rowid.'" class="product-checkbox">';
|
||||
} elseif ($isAlreadyOnStz) {
|
||||
} else {
|
||||
print '<span class="fas fa-check" style="color: #28a745;" title="'.$langs->trans("AlreadyOnStundenzettel").'"></span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
|
@ -2456,9 +2456,7 @@ if ($tab == 'products') {
|
|||
// TAB: LIEFERAUFLISTUNG / TRACKING
|
||||
// =============================================
|
||||
if ($tab == 'tracking') {
|
||||
$total_ordered = 0;
|
||||
$total_delivered = 0;
|
||||
$total_remaining = 0;
|
||||
|
||||
// Alle Stundenzettel-Details pro Produkt laden (für ausklappbare Ansicht)
|
||||
$trackingDetails = array(); // Array[fk_commandedet] => array of entries
|
||||
|
|
@ -2503,10 +2501,7 @@ if ($tab == 'tracking') {
|
|||
print '<table class="noborder centpercent" id="tracking-table">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Product").'</th>';
|
||||
print '<th class="right">'.$langs->trans("QtyOrdered").'</th>';
|
||||
print '<th class="right">'.$langs->trans("QtyDelivered").'</th>';
|
||||
print '<th class="right">'.$langs->trans("QtyRemaining").'</th>';
|
||||
print '<th>'.$langs->trans("Status").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
// Live-Berechnung aus allen Stundenzetteln (inkl. Entwürfe)
|
||||
|
|
@ -2585,22 +2580,7 @@ if ($tab == 'tracking') {
|
|||
}
|
||||
print '</td>';
|
||||
|
||||
// Bestellt (mit Badges für Mehraufwand/Entfällt/Rücknahmen)
|
||||
print '<td class="right">';
|
||||
print '<strong>'.formatQty($effective_ordered).'</strong>';
|
||||
if ($qty_additional > 0) {
|
||||
print ' <span class="badge" style="background-color: #28a745; color: #fff; font-size: 0.75em;" title="'.$langs->trans("Mehraufwand").'">+'.formatQty($qty_additional).'</span>';
|
||||
}
|
||||
if ($qty_omitted > 0) {
|
||||
print ' <span class="badge" style="background-color: #dc3545; color: #fff; font-size: 0.75em;" title="'.$langs->trans("Entfaellt").'">-'.formatQty($qty_omitted).'</span>';
|
||||
}
|
||||
if ($qty_returned > 0) {
|
||||
print ' <span class="badge badge-danger" title="'.$langs->trans("Ruecknahme").'">-'.formatQty($qty_returned).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
$total_ordered += $effective_ordered;
|
||||
|
||||
// Geliefert/Erfasst (mit Rücknahme-Hinweis)
|
||||
// Verbaut (mit Rücknahme-Hinweis)
|
||||
print '<td class="right">';
|
||||
print formatQty($effective_delivered);
|
||||
if ($qty_returned > 0) {
|
||||
|
|
@ -2609,38 +2589,12 @@ if ($tab == 'tracking') {
|
|||
print '</td>';
|
||||
$total_delivered += $effective_delivered;
|
||||
|
||||
// Verbleibend
|
||||
print '<td class="right">';
|
||||
if ($qty_remaining > 0) {
|
||||
print '<span class="badge badge-warning">'.formatQty($qty_remaining).'</span>';
|
||||
} elseif ($qty_remaining == 0) {
|
||||
print '<span class="badge badge-success">0</span>';
|
||||
} else {
|
||||
print '<span class="badge badge-info">'.formatQty($qty_remaining).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
$total_remaining += $qty_remaining;
|
||||
|
||||
// Status
|
||||
print '<td>';
|
||||
if ($effective_ordered <= 0 && $qty_returned > 0) {
|
||||
// Alles zurückgenommen - Erledigt
|
||||
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
||||
} elseif ($qty_remaining <= 0) {
|
||||
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
||||
} elseif ($effective_delivered > 0) {
|
||||
print '<span class="badge badge-warning">'.$langs->trans("TrackingPartial").'</span>';
|
||||
} else {
|
||||
print '<span class="badge badge-secondary">'.$langs->trans("TrackingOpen").'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
// Detail-Zeile (standardmäßig eingeklappt)
|
||||
if ($hasDetails) {
|
||||
print '<tr id="tdetail_'.$detailRowId.'" class="tracking-detail-row" style="display:none;">';
|
||||
print '<td class="stz-subtable-bg" colspan="5" style="padding: 0 0 0 30px;">';
|
||||
print '<td class="stz-subtable-bg" colspan="2" style="padding: 0 0 0 30px;">';
|
||||
|
||||
// Sub-Tabelle für Details
|
||||
print '<table class="noborder" style="width:100%; margin: 5px 0;">';
|
||||
|
|
@ -2707,14 +2661,11 @@ if ($tab == 'tracking') {
|
|||
// Summenzeile
|
||||
print '<tr class="liste_total">';
|
||||
print '<td><strong>'.$langs->trans("Total").'</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_ordered).'</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_delivered).'</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_remaining).'</strong></td>';
|
||||
print '<td></td>';
|
||||
print '</tr>';
|
||||
|
||||
} else {
|
||||
print '<tr class="oddeven"><td colspan="5" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
print '<tr class="oddeven"><td colspan="2" class="opacitymedium">'.$langs->trans("NoRecordFound").'</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2771,7 +2722,7 @@ if ($tab == 'tracking') {
|
|||
if ($resqlMehr && $db->num_rows($resqlMehr) > 0) {
|
||||
// Separator-Zeile für Mehraufwand
|
||||
print '<tr class="liste_titre">';
|
||||
print '<td class="stz-mehraufwand-header" colspan="5"><strong>'.$langs->trans("Mehraufwand").'</strong> <span class="opacitymedium">('.$langs->trans("MehraufwandDesc").')</span></td>';
|
||||
print '<td class="stz-mehraufwand-header" colspan="2"><strong>'.$langs->trans("Mehraufwand").'</strong> <span class="opacitymedium">('.$langs->trans("MehraufwandDesc").')</span></td>';
|
||||
print '</tr>';
|
||||
|
||||
while ($objMehr = $db->fetch_object($resqlMehr)) {
|
||||
|
|
@ -2835,16 +2786,7 @@ if ($tab == 'tracking') {
|
|||
}
|
||||
print '</td>';
|
||||
|
||||
// Bestellt (Mehraufwand-Menge, MINUS Rücknahmen)
|
||||
print '<td class="right">';
|
||||
print '<strong>'.formatQty($qty_ordered_mehr).'</strong>';
|
||||
if ($qtyReturned > 0) {
|
||||
print ' <span class="badge badge-danger" title="'.$langs->trans("QtyReturned").'">-'.formatQty($qtyReturned).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
$total_ordered += $qty_ordered_mehr;
|
||||
|
||||
// Geliefert (mit Rücknahme-Hinweis wenn vorhanden)
|
||||
// Verbaut (mit Rücknahme-Hinweis wenn vorhanden)
|
||||
print '<td class="right">';
|
||||
print formatQty($qty_delivered_mehr);
|
||||
if ($qtyReturned > 0) {
|
||||
|
|
@ -2853,38 +2795,12 @@ if ($tab == 'tracking') {
|
|||
print '</td>';
|
||||
$total_delivered += $qty_delivered_mehr;
|
||||
|
||||
// Verbleibend
|
||||
print '<td class="right">';
|
||||
if ($qty_remaining_mehr > 0) {
|
||||
print '<span class="badge badge-warning">'.formatQty($qty_remaining_mehr).'</span>';
|
||||
} elseif ($qty_remaining_mehr == 0) {
|
||||
print '<span class="badge badge-success">0</span>';
|
||||
} else {
|
||||
print '<span class="badge badge-info">'.formatQty($qty_remaining_mehr).'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
$total_remaining += $qty_remaining_mehr;
|
||||
|
||||
// Status
|
||||
print '<td>';
|
||||
if ($qty_ordered_mehr <= 0 && $qtyReturned > 0) {
|
||||
// Alles zurückgenommen - Erledigt
|
||||
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
||||
} elseif ($qty_remaining_mehr <= 0) {
|
||||
print '<span class="badge badge-success">'.$langs->trans("TrackingDone").'</span>';
|
||||
} elseif ($qty_delivered_mehr > 0) {
|
||||
print '<span class="badge badge-warning">'.$langs->trans("TrackingPartial").'</span>';
|
||||
} else {
|
||||
print '<span class="badge badge-secondary">'.$langs->trans("TrackingOpen").'</span>';
|
||||
}
|
||||
print '</td>';
|
||||
|
||||
print '</tr>';
|
||||
|
||||
// Detail-Zeile für Mehraufwand
|
||||
if ($hasDetailsMehr) {
|
||||
print '<tr id="tdetail_'.$detailRowId.'" class="tracking-detail-row" style="display:none;">';
|
||||
print '<td class="stz-subtable-bg" colspan="5" style="padding: 0 0 0 30px;">';
|
||||
print '<td class="stz-subtable-bg" colspan="2" style="padding: 0 0 0 30px;">';
|
||||
print '<table class="noborder" style="width:100%; margin: 5px 0;">';
|
||||
print '<tr class="liste_titre stz-subtable-header">';
|
||||
print '<th style="width:120px;">'.$langs->trans("Stundenzettel").'</th>';
|
||||
|
|
@ -2913,10 +2829,7 @@ if ($tab == 'tracking') {
|
|||
// Neue Summenzeile mit Mehraufwand
|
||||
print '<tr class="liste_total">';
|
||||
print '<td><strong>'.$langs->trans("Total").' ('.$langs->trans("Mehraufwand").' '.$langs->trans("incl").')</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_ordered).'</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_delivered).'</strong></td>';
|
||||
print '<td class="right"><strong>'.formatQty($total_remaining).'</strong></td>';
|
||||
print '<td></td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
|
|
|
|||
30
sw.js
Normal file
30
sw.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Stundenzettel PWA - Minimaler Service Worker
|
||||
* Nur fuer Installierbarkeit, kein Offline-Caching
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = 'stundenzettel-pwa-v1.0';
|
||||
|
||||
self.addEventListener('install', function(event) {
|
||||
// Sofort aktivieren, nicht auf andere Tabs warten
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function(event) {
|
||||
// Alte Caches loeschen
|
||||
event.waitUntil(
|
||||
caches.keys().then(function(cacheNames) {
|
||||
return Promise.all(
|
||||
cacheNames.filter(function(name) {
|
||||
return name.startsWith('stundenzettel-pwa-') && name !== CACHE_VERSION;
|
||||
}).map(function(name) {
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
}).then(function() {
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Kein Fetch-Intercepting - alle Requests gehen direkt ans Netzwerk
|
||||
Loading…
Reference in a new issue