dolibarr.stundenzettel/ajax/pwa_api.php
data 292db5d40c 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>
2026-02-27 21:21:14 +01:00

1564 lines
53 KiB
PHP

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* Stundenzettel PWA - Zentrale JSON-API
* Alle CRUD-Operationen fuer die Mobile App
*/
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')));
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php';
dol_include_once('/stundenzettel/class/stundenzettel.class.php');
header('Content-Type: application/json; charset=UTF-8');
$action = GETPOST('action', 'aZ09');
$response = array('success' => false);
// ============================================================
// TOKEN-VALIDIERUNG
// ============================================================
$token = GETPOST('token', 'none');
if (empty($token)) {
http_response_code(401);
echo json_encode(array('success' => false, 'error' => 'Kein Token'));
exit;
}
$tokenData = json_decode(base64_decode($token), true);
if (!$tokenData || empty($tokenData['user_id']) || empty($tokenData['expires'])) {
http_response_code(401);
echo json_encode(array('success' => false, 'error' => 'Ungueltiges Token'));
exit;
}
if ($tokenData['expires'] < time()) {
http_response_code(401);
echo json_encode(array('success' => false, 'error' => 'Token abgelaufen'));
exit;
}
$expectedHash = md5($tokenData['user_id'] . $tokenData['login'] . getDolGlobalString('MAIN_SECURITY_SALT', 'defaultsalt'));
if ($tokenData['hash'] !== $expectedHash) {
http_response_code(401);
echo json_encode(array('success' => false, 'error' => 'Token manipuliert'));
exit;
}
// Benutzer laden
$user = new User($db);
if ($user->fetch($tokenData['user_id']) <= 0 || $user->statut != 1) {
http_response_code(401);
echo json_encode(array('success' => false, 'error' => 'Benutzer nicht aktiv'));
exit;
}
$user->getrights();
// Basis-Berechtigung
if (!$user->hasRight('stundenzettel', 'read')) {
http_response_code(403);
echo json_encode(array('success' => false, 'error' => 'Keine Berechtigung'));
exit;
}
$langs->loadLangs(array("stundenzettel@stundenzettel", "orders", "products"));
// Schreib-Berechtigung pruefen
$canWrite = $user->hasRight('stundenzettel', 'write');
$canWriteAll = $user->hasRight('stundenzettel', 'write', 'all') || $user->admin;
$canReadAll = $user->hasRight('stundenzettel', 'read', 'all') || $user->admin;
/**
* Hilfsfunktion: Mengen formatieren
*/
function pwaFormatQty($qty) {
$qty = (float)$qty;
if ($qty == floor($qty)) {
return number_format($qty, 0, ',', '.');
}
return rtrim(rtrim(number_format($qty, 2, ',', '.'), '0'), ',');
}
/**
* Hilfsfunktion: Pruefen ob User Schreibzugriff auf STZ hat
*/
function canEditStz($stz, $user, $canWriteAll) {
if ($stz->status != Stundenzettel::STATUS_DRAFT) return false;
if ($stz->fk_user_author == $user->id) return true;
if ($canWriteAll) return true;
return false;
}
/**
* Hilfsfunktion: Status-Label zurueckgeben
*/
function getStatusLabel($status) {
switch ((int)$status) {
case 0: return 'Entwurf';
case 1: return 'Freigegeben';
case 2: return 'Abgerechnet';
case 9: return 'Storniert';
default: return 'Unbekannt';
}
}
// ============================================================
// API ACTIONS
// ============================================================
switch ($action) {
// ---- Kundensuche ----
case 'search_customers':
$term = GETPOST('term', 'alphanohtml');
if (strlen($term) < 2) {
$response['error'] = 'Mindestens 2 Zeichen';
break;
}
$sql = "SELECT s.rowid as id, s.nom as name, COUNT(DISTINCT c.rowid) as order_count";
$sql .= " FROM ".MAIN_DB_PREFIX."societe as s";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."commande as c ON c.fk_soc = s.rowid";
$sql .= " WHERE s.nom LIKE '%".$db->escape($term)."%'";
$sql .= " AND c.fk_statut = 1"; // Nur validierte (laufende) Auftraege
$sql .= " AND s.entity IN (".getEntity('societe').")";
$sql .= " GROUP BY s.rowid, s.nom";
$sql .= " ORDER BY s.nom ASC";
$sql .= " LIMIT 20";
$resql = $db->query($sql);
$customers = array();
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$customers[] = array(
'id' => (int)$obj->id,
'name' => $obj->name,
'order_count' => (int)$obj->order_count
);
}
}
$response['success'] = true;
$response['customers'] = $customers;
break;
// ---- Auftraege eines Kunden ----
case 'get_customer_orders':
$customerId = GETPOST('customer_id', 'int');
if ($customerId <= 0) {
$response['error'] = 'Keine Kunden-ID';
break;
}
$sql = "SELECT c.rowid as id, c.ref, c.ref_client, c.date_commande, c.fk_statut as status";
$sql .= " FROM ".MAIN_DB_PREFIX."commande as c";
$sql .= " WHERE c.fk_soc = ".((int)$customerId);
$sql .= " AND c.fk_statut = 1"; // Nur validierte (laufende) Auftraege, nicht geliefert/fakturiert
$sql .= " AND c.entity IN (".getEntity('commande').")";
$sql .= " ORDER BY c.date_commande DESC, c.rowid DESC";
$resql = $db->query($sql);
$orders = array();
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
// Offener Stundenzettel pruefen
$sqlStz = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel";
$sqlStz .= " WHERE fk_commande = ".((int)$obj->id);
$sqlStz .= " AND status = 0";
if (!$canReadAll) {
$sqlStz .= " AND fk_user_author = ".((int)$user->id);
}
$sqlStz .= " ORDER BY date_stundenzettel DESC LIMIT 1";
$resStz = $db->query($sqlStz);
$hasDraftStz = false;
$draftStzId = 0;
if ($resStz && $db->num_rows($resStz) > 0) {
$objStz = $db->fetch_object($resStz);
$hasDraftStz = true;
$draftStzId = (int)$objStz->rowid;
}
$orders[] = array(
'id' => (int)$obj->id,
'ref' => $obj->ref,
'ref_client' => $obj->ref_client ?: '',
'date' => dol_print_date($db->jdate($obj->date_commande), 'day'),
'status' => (int)$obj->status,
'has_draft_stz' => $hasDraftStz,
'draft_stz_id' => $draftStzId
);
}
}
$response['success'] = true;
$response['orders'] = $orders;
break;
// ---- Komplett-Kontext fuer einen Auftrag ----
case 'get_order_context':
$orderId = GETPOST('order_id', 'int');
$stzId = GETPOST('stz_id', 'int');
if ($orderId <= 0) {
$response['error'] = 'Keine Auftrags-ID';
break;
}
// Auftrag laden
$order = new Commande($db);
if ($order->fetch($orderId) <= 0) {
$response['error'] = 'Auftrag nicht gefunden';
break;
}
// Kunde laden
$customer = new Societe($db);
$customer->fetch($order->socid);
// Auftragsdaten
$response['order'] = array(
'id' => (int)$order->id,
'ref' => $order->ref,
'date' => dol_print_date($order->date_commande, 'day'),
'customer_name' => $customer->name,
'customer_id' => (int)$customer->id,
'status' => (int)$order->statut
);
// Stundenzettel finden (per ID oder letzten Draft)
$stz = new Stundenzettel($db);
$stzFound = false;
if ($stzId > 0) {
if ($stz->fetch($stzId) > 0) {
$stzFound = true;
}
}
if (!$stzFound) {
// Letzten Draft fuer diesen Auftrag suchen
$sqlStz = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel";
$sqlStz .= " WHERE fk_commande = ".((int)$orderId);
if (!$canReadAll) {
$sqlStz .= " AND fk_user_author = ".((int)$user->id);
}
$sqlStz .= " ORDER BY status ASC, date_stundenzettel DESC, rowid DESC";
$sqlStz .= " LIMIT 1";
$resStz = $db->query($sqlStz);
if ($resStz && $db->num_rows($resStz) > 0) {
$objStz = $db->fetch_object($resStz);
$stz->fetch($objStz->rowid);
$stzFound = true;
}
}
// Prüfe ob User den aktuellen STZ editieren kann
$canEdit = false;
if ($stzFound) {
$canEdit = canEditStz($stz, $user, $canWriteAll);
}
$response['can_write'] = $canWrite; // Allgemeine Schreibberechtigung
$response['can_edit_stz'] = $canWrite && $canEdit; // Aktuellen STZ editierbar
// STZ-Daten
if ($stzFound) {
$response['stz'] = array(
'id' => (int)$stz->id,
'ref' => $stz->ref,
'date' => dol_print_date($stz->date_stundenzettel, 'day'),
'date_iso' => dol_print_date($stz->date_stundenzettel, '%Y-%m-%d'),
'status' => (int)$stz->status,
'status_label' => getStatusLabel($stz->status),
'hourly_rate' => $stz->hourly_rate,
'fk_user_author' => (int)$stz->fk_user_author
);
// Produkte laden
$stz->fetchProducts();
$products = array();
foreach ($stz->products as $p) {
$products[] = array(
'id' => (int)$p->rowid,
'fk_product' => (int)$p->fk_product,
'fk_commandedet' => (int)$p->fk_commandedet,
'ref' => $p->product_ref,
'label' => $p->product_label,
'description' => strip_tags($p->description),
'qty_original' => (float)$p->qty_original,
'qty_done' => (float)$p->qty_done,
'origin' => $p->origin
);
}
$response['products'] = $products;
// Leistungen laden
$stz->fetchLeistungen();
$leistungen = array();
foreach ($stz->leistungen as $l) {
$leistungen[] = array(
'id' => (int)$l->rowid,
'date' => dol_print_date($db->jdate($l->date_leistung), 'day'),
'date_iso' => dol_print_date($db->jdate($l->date_leistung), '%Y-%m-%d'),
'time_start' => substr($l->time_start, 0, 5),
'time_end' => substr($l->time_end, 0, 5),
'duration_minutes' => (int)$l->duration,
'description' => strip_tags($l->description ?: ''),
'service_name' => $l->product_label ?: '',
'fk_product' => (int)$l->fk_product
);
}
$response['leistungen'] = $leistungen;
// Notizen laden
$stz->fetchNotes();
$notes = array();
foreach ($stz->notes as $n) {
$notes[] = array(
'id' => (int)$n->rowid,
'note' => $n->note,
'checked' => (int)$n->checked,
'date' => dol_print_date($db->jdate($n->datec), 'dayhour')
);
}
$response['notes'] = $notes;
} else {
$response['stz'] = null;
$response['products'] = array();
$response['leistungen'] = array();
$response['notes'] = array();
}
// Tracking-Daten (Lieferauflistung) - Live-Berechnung wie Desktop
// Auftragspositionen mit Verbaut/Mehraufwand/Entfaellt/Ruecknahme berechnen
$tracking = array();
$sqlTrack = "SELECT cd.rowid, cd.fk_product, cd.qty as qty_ordered, cd.description,";
$sqlTrack .= " p.ref as product_ref, p.label as product_label,";
// Verbaut (origin order/added)
$sqlTrack .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
$sqlTrack .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
$sqlTrack .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_delivered,";
// Mehraufwand
$sqlTrack .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
$sqlTrack .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
$sqlTrack .= " WHERE sp2.fk_product = cd.fk_product AND cd.fk_product > 0 AND sp2.origin = 'additional' AND s2.fk_commande = ".((int)$orderId)."), 0) as qty_additional,";
// Entfaellt
$sqlTrack .= " COALESCE((SELECT SUM(sp3.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp3";
$sqlTrack .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s3 ON s3.rowid = sp3.fk_stundenzettel";
$sqlTrack .= " WHERE sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$orderId);
$sqlTrack .= " AND ((sp3.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp3.fk_commandedet = cd.rowid)), 0) as qty_omitted,";
// Ruecknahmen
$sqlTrack .= " COALESCE((SELECT SUM(sp4.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp4";
$sqlTrack .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s4 ON s4.rowid = sp4.fk_stundenzettel";
$sqlTrack .= " WHERE sp4.origin = 'returned' AND s4.fk_commande = ".((int)$orderId);
$sqlTrack .= " AND ((sp4.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp4.fk_commandedet = cd.rowid)), 0) as qty_returned";
$sqlTrack .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sqlTrack .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
$sqlTrack .= " WHERE cd.fk_commande = ".((int)$orderId);
$sqlTrack .= " AND (cd.fk_product > 0 OR ((cd.fk_product IS NULL OR cd.fk_product = 0) AND cd.description IS NOT NULL AND cd.description != ''))";
$sqlTrack .= " AND (cd.special_code IS NULL OR cd.special_code = 0)";
$sqlTrack .= " ORDER BY cd.rang";
$resTrack = $db->query($sqlTrack);
if ($resTrack) {
while ($obj = $db->fetch_object($resTrack)) {
$qtyAdditional = (float)$obj->qty_additional;
$qtyOmitted = (float)$obj->qty_omitted;
$qtyReturned = (float)$obj->qty_returned;
// Effektiv bestellt = Original + Mehraufwand - Entfaellt - Ruecknahmen
$effectiveOrdered = $obj->qty_ordered + $qtyAdditional - $qtyOmitted - $qtyReturned;
// Effektiv verbaut = Verbaut - Ruecknahmen
$effectiveDelivered = $obj->qty_delivered - $qtyReturned;
$qtyRemaining = $effectiveOrdered - $effectiveDelivered;
$label = '';
if ($obj->product_ref) {
$label = $obj->product_ref.' - '.$obj->product_label;
} else {
$label = strip_tags($obj->description ?: '');
if (strlen($label) > 80) $label = substr($label, 0, 77).'...';
}
$tracking[] = array(
'ref' => $obj->product_ref ?: '',
'label' => $label,
'qty_ordered' => (float)$effectiveOrdered,
'qty_delivered' => (float)$effectiveDelivered,
'qty_remaining' => (float)$qtyRemaining,
'qty_additional' => $qtyAdditional,
'qty_omitted' => $qtyOmitted,
'qty_returned' => $qtyReturned
);
}
}
$response['tracking'] = $tracking;
// Leistungen/Arbeitsstunden gruppiert nach Service (fuer Lieferauflistung)
$leistungenSummary = array();
$sqlLeist = "SELECT COALESCE(l.fk_product, 0) as service_id,";
$sqlLeist .= " p.ref as service_ref, p.label as service_label,";
$sqlLeist .= " SUM(l.duration) as total_minutes, COUNT(l.rowid) as entry_count";
$sqlLeist .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l";
$sqlLeist .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = l.fk_stundenzettel";
$sqlLeist .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product";
$sqlLeist .= " WHERE s.fk_commande = ".((int)$orderId);
if (!$canReadAll) {
$sqlLeist .= " AND s.fk_user_author = ".((int)$user->id);
}
$sqlLeist .= " GROUP BY COALESCE(l.fk_product, 0), p.ref, p.label";
$sqlLeist .= " ORDER BY p.ref, p.label";
$resLeist = $db->query($sqlLeist);
if ($resLeist) {
while ($obj = $db->fetch_object($resLeist)) {
$totalMin = (int)$obj->total_minutes;
$h = floor($totalMin / 60);
$m = $totalMin % 60;
$leistungenSummary[] = array(
'service_ref' => $obj->service_ref ?: '',
'service_label' => $obj->service_label ?: 'Nicht zugeordnet',
'total_minutes' => $totalMin,
'total_hours' => sprintf('%d:%02d h', $h, $m),
'entry_count' => (int)$obj->entry_count
);
}
}
$response['leistungen_summary'] = $leistungenSummary;
// Einzelne Leistungen aller STZ (fuer Lieferauflistung Detail)
$leistungenAll = array();
$sqlLeistAll = "SELECT l.rowid, l.fk_stundenzettel, l.date_leistung, l.time_start, l.time_end,";
$sqlLeistAll .= " l.duration, l.description, s.ref as stz_ref,";
$sqlLeistAll .= " p.ref as service_ref, p.label as service_label";
$sqlLeistAll .= " FROM ".MAIN_DB_PREFIX."stundenzettel_leistung l";
$sqlLeistAll .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = l.fk_stundenzettel";
$sqlLeistAll .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = l.fk_product";
$sqlLeistAll .= " WHERE s.fk_commande = ".((int)$orderId);
if (!$canReadAll) {
$sqlLeistAll .= " AND s.fk_user_author = ".((int)$user->id);
}
$sqlLeistAll .= " ORDER BY s.date_stundenzettel DESC, l.date_leistung, l.time_start";
$resLeistAll = $db->query($sqlLeistAll);
if ($resLeistAll) {
while ($obj = $db->fetch_object($resLeistAll)) {
$dur = (int)$obj->duration;
$h = floor($dur / 60);
$m = $dur % 60;
$leistungenAll[] = array(
'stz_ref' => $obj->stz_ref,
'date' => dol_print_date($db->jdate($obj->date_leistung), 'day'),
'time_start' => $obj->time_start ? substr($obj->time_start, 0, 5) : '',
'time_end' => $obj->time_end ? substr($obj->time_end, 0, 5) : '',
'duration' => sprintf('%d:%02d h', $h, $m),
'service' => $obj->service_ref ? $obj->service_ref : '',
'description' => strip_tags($obj->description ?: '')
);
}
}
$response['leistungen_all'] = $leistungenAll;
// Alle STZ fuer diesen Auftrag
$stzList = array();
$sqlList = "SELECT s.rowid, s.ref, s.date_stundenzettel, s.status, s.fk_user_author,";
$sqlList .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = s.rowid) as leistung_count,";
$sqlList .= " (SELECT COALESCE(SUM(duration), 0) FROM ".MAIN_DB_PREFIX."stundenzettel_leistung WHERE fk_stundenzettel = s.rowid) as total_minutes,";
$sqlList .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."stundenzettel_product WHERE fk_stundenzettel = s.rowid AND origin IN ('order','added')) as product_count";
$sqlList .= " FROM ".MAIN_DB_PREFIX."stundenzettel as s";
$sqlList .= " WHERE s.fk_commande = ".((int)$orderId);
if (!$canReadAll) {
$sqlList .= " AND s.fk_user_author = ".((int)$user->id);
}
$sqlList .= " ORDER BY s.date_stundenzettel DESC, s.rowid DESC";
$resList = $db->query($sqlList);
if ($resList) {
while ($obj = $db->fetch_object($resList)) {
$totalMin = (int)$obj->total_minutes;
$h = floor($totalMin / 60);
$m = $totalMin % 60;
$totalHours = ($h > 0 ? $h.'h' : '') . ($m > 0 ? ' '.$m.'min' : '');
if (!$totalHours) $totalHours = '0h';
$stzList[] = array(
'id' => (int)$obj->rowid,
'ref' => $obj->ref,
'date' => dol_print_date($db->jdate($obj->date_stundenzettel), 'day'),
'status' => (int)$obj->status,
'status_label' => getStatusLabel($obj->status),
'leistung_count' => (int)$obj->leistung_count,
'total_hours' => trim($totalHours),
'product_count' => (int)$obj->product_count
);
}
}
$response['stz_list'] = $stzList;
// Auftragspositionen fuer Produktliste-Tab (Uebernahme in STZ)
// Live-Berechnung wie im Desktop (stundenzettel_commande.php)
$orderLines = array();
$sqlOL = "SELECT cd.rowid, cd.fk_product, cd.qty, cd.description,";
$sqlOL .= " p.ref as product_ref, p.label as product_label,";
// Verbaut (origin order/added)
$sqlOL .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
$sqlOL .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
$sqlOL .= " WHERE sp.fk_commandedet = cd.rowid AND sp.origin IN ('order', 'added')), 0) as qty_delivered,";
// Mehraufwand (origin additional)
$sqlOL .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
$sqlOL .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
$sqlOL .= " WHERE sp2.fk_product = cd.fk_product AND cd.fk_product > 0 AND sp2.origin = 'additional' AND s2.fk_commande = ".((int)$orderId)."), 0) as qty_additional,";
// Entfaellt
$sqlOL .= " COALESCE((SELECT SUM(sp3.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp3";
$sqlOL .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s3 ON s3.rowid = sp3.fk_stundenzettel";
$sqlOL .= " WHERE sp3.origin = 'omitted' AND s3.fk_commande = ".((int)$orderId);
$sqlOL .= " AND ((sp3.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp3.fk_commandedet = cd.rowid)), 0) as qty_omitted,";
// Ruecknahmen
$sqlOL .= " COALESCE((SELECT SUM(sp4.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp4";
$sqlOL .= " JOIN ".MAIN_DB_PREFIX."stundenzettel s4 ON s4.rowid = sp4.fk_stundenzettel";
$sqlOL .= " WHERE sp4.origin = 'returned' AND s4.fk_commande = ".((int)$orderId);
$sqlOL .= " AND ((sp4.fk_product = cd.fk_product AND cd.fk_product > 0) OR sp4.fk_commandedet = cd.rowid)), 0) as qty_returned";
$sqlOL .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sqlOL .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
$sqlOL .= " WHERE cd.fk_commande = ".((int)$orderId);
$sqlOL .= " AND (cd.fk_product > 0 OR ((cd.fk_product IS NULL OR cd.fk_product = 0) AND cd.description IS NOT NULL AND cd.description != ''))";
$sqlOL .= " AND (cd.special_code IS NULL OR cd.special_code = 0)";
$sqlOL .= " ORDER BY cd.rang";
$resOL = $db->query($sqlOL);
if ($resOL) {
while ($obj = $db->fetch_object($resOL)) {
$label = '';
if ($obj->product_ref) {
$label = $obj->product_ref.' - '.$obj->product_label;
} else {
$label = strip_tags($obj->description ?: '');
if (strlen($label) > 80) $label = substr($label, 0, 77).'...';
}
$qtyDelivered = (float)$obj->qty_delivered;
$qtyAdditional = (float)$obj->qty_additional;
$qtyOmitted = (float)$obj->qty_omitted;
$qtyReturned = (float)$obj->qty_returned;
$effectiveDelivered = $qtyDelivered - $qtyReturned;
// Effektive Gesamtmenge = Original + Mehraufwand - Entfaellt - Ruecknahmen (wie Desktop)
$effectiveOrdered = $obj->qty + $qtyAdditional - $qtyOmitted - $qtyReturned;
$qtyRemaining = $effectiveOrdered - $effectiveDelivered;
// Pruefen ob bereits auf dem aktuellen STZ
$alreadyOnStz = false;
if ($stzFound) {
foreach ($stz->products as $sp) {
if ($sp->fk_commandedet == $obj->rowid && in_array($sp->origin, array('order', 'added'))) {
$alreadyOnStz = true;
break;
}
}
}
$orderLines[] = array(
'id' => (int)$obj->rowid,
'fk_product' => (int)$obj->fk_product,
'ref' => $obj->product_ref ?: '',
'label' => $label,
'description' => strip_tags($obj->description ?: ''),
'qty' => (float)$obj->qty,
'qty_effective' => (float)$effectiveOrdered,
'qty_delivered' => $effectiveDelivered,
'qty_remaining' => $qtyRemaining,
'qty_additional' => $qtyAdditional,
'qty_omitted' => $qtyOmitted,
'qty_returned' => $qtyReturned,
'already_on_stz' => $alreadyOnStz
);
}
}
$response['order_lines'] = $orderLines;
// Mehraufwand-Produkte laden (nicht aus Auftrag, wie Desktop stundenzettel_commande.php)
// origin='additional' (Mehraufwand-Bestellung) + origin='added' ohne fk_commandedet (manuell hinzugefuegt)
$mehraufwandLines = array();
// Ruecknahmen laden fuer Zuordnung
$returnedProducts = array();
$sqlRet = "SELECT sp.fk_product, sp.product_label, sp.description, SUM(sp.qty_done) as qty_returned";
$sqlRet .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
$sqlRet .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
$sqlRet .= " WHERE s.fk_commande = ".((int)$orderId);
$sqlRet .= " AND sp.origin = 'returned'";
$sqlRet .= " GROUP BY sp.fk_product, sp.product_label, sp.description";
$resRet = $db->query($sqlRet);
if ($resRet) {
while ($objRet = $db->fetch_object($resRet)) {
$key = '';
if ($objRet->fk_product > 0) {
$key = 'prod_'.$objRet->fk_product;
} else {
$key = 'desc_'.md5(trim(strip_tags($objRet->description)));
}
$returnedProducts[$key] = (float)$objRet->qty_returned;
}
}
// Mehraufwand-Produkte
$sqlMA = "SELECT sp.fk_product, sp.product_ref, sp.product_label, sp.description,";
$sqlMA .= " SUM(CASE WHEN sp.origin = 'additional' THEN sp.qty_done ELSE 0 END) as qty_additional,";
$sqlMA .= " SUM(CASE WHEN sp.origin = 'added' AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0) THEN sp.qty_done ELSE 0 END) as qty_added,";
$sqlMA .= " GROUP_CONCAT(DISTINCT s.ref SEPARATOR ', ') as stundenzettel_refs";
$sqlMA .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
$sqlMA .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
$sqlMA .= " WHERE s.fk_commande = ".((int)$orderId);
$sqlMA .= " AND (sp.origin = 'additional' OR (sp.origin = 'added' AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0)))";
$sqlMA .= " GROUP BY sp.fk_product, sp.product_ref, sp.product_label, sp.description";
$sqlMA .= " ORDER BY sp.product_ref, sp.description";
$resMA = $db->query($sqlMA);
if ($resMA) {
while ($objMA = $db->fetch_object($resMA)) {
$qtyAdditional = (float)$objMA->qty_additional;
$qtyAdded = (float)$objMA->qty_added;
// Ruecknahmen zuordnen
$returnKey = ($objMA->fk_product > 0) ? 'prod_'.$objMA->fk_product : 'desc_'.md5(trim(strip_tags($objMA->description)));
$qtyReturned = isset($returnedProducts[$returnKey]) ? $returnedProducts[$returnKey] : 0;
// Beauftragt (Zielmenge) und Verbaut berechnen (wie Desktop)
$qtyTarget = (($qtyAdditional > 0) ? $qtyAdditional : $qtyAdded) - $qtyReturned;
$qtyDone = $qtyAdded - $qtyReturned;
$qtyRemaining = $qtyTarget - $qtyDone;
$isDone = ($qtyRemaining <= 0) || ($qtyTarget <= 0 && $qtyReturned > 0);
// Label zusammenbauen
$label = '';
if ($objMA->product_ref) {
$label = $objMA->product_ref.' - '.$objMA->product_label;
} else {
$label = strip_tags($objMA->description ?: '');
if (strlen($label) > 80) $label = substr($label, 0, 77).'...';
}
$mehraufwandLines[] = array(
'fk_product' => (int)$objMA->fk_product,
'ref' => $objMA->product_ref ?: '',
'label' => $label,
'description' => strip_tags($objMA->description ?: ''),
'qty_target' => (float)$qtyTarget,
'qty_done' => (float)$qtyDone,
'qty_remaining' => (float)$qtyRemaining,
'qty_returned' => (float)$qtyReturned,
'stz_refs' => $objMA->stundenzettel_refs ?: '',
'is_done' => $isDone
);
}
}
$response['mehraufwand_lines'] = $mehraufwandLines;
$response['success'] = true;
break;
// ---- Neuen Stundenzettel erstellen ----
case 'create_stundenzettel':
if (!$canWrite) {
$response['error'] = 'Keine Schreibberechtigung';
break;
}
$orderId = GETPOST('order_id', 'int');
$date = GETPOST('date', 'alpha');
if ($orderId <= 0 || empty($date)) {
$response['error'] = 'Auftrags-ID und Datum erforderlich';
break;
}
// Auftrag pruefen
$order = new Commande($db);
if ($order->fetch($orderId) <= 0) {
$response['error'] = 'Auftrag nicht gefunden';
break;
}
// Prüfe ob fuer dieses Datum bereits ein STZ existiert
$sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel";
$sqlCheck .= " WHERE fk_commande = ".((int)$orderId);
$sqlCheck .= " AND date_stundenzettel = '".$db->escape($date)."'";
$sqlCheck .= " AND fk_user_author = ".((int)$user->id);
$resCheck = $db->query($sqlCheck);
if ($resCheck && $db->num_rows($resCheck) > 0) {
$objExist = $db->fetch_object($resCheck);
$response['success'] = true;
$response['stz_id'] = (int)$objExist->rowid;
$response['message'] = 'Stundenzettel fuer dieses Datum existiert bereits';
break;
}
$stz = new Stundenzettel($db);
$stz->fk_commande = $orderId;
$stz->fk_soc = $order->socid;
$stz->date_stundenzettel = $db->idate(strtotime($date));
$stz->fk_user_author = $user->id;
$result = $stz->create($user);
if ($result > 0) {
$response['success'] = true;
$response['stz_id'] = (int)$result;
} else {
$response['error'] = $stz->error ?: 'Fehler beim Erstellen';
}
break;
// ---- Leistung hinzufuegen ----
case 'add_leistung':
$stzId = GETPOST('stz_id', 'int');
$date = GETPOST('date', 'alpha');
$timeStart = GETPOST('time_start', 'alpha');
$timeEnd = GETPOST('time_end', 'alpha');
$description = GETPOST('description', 'restricthtml');
$fkProduct = GETPOST('fk_product', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->addLeistung($user, $date, $timeStart, $timeEnd, $description, $fkProduct > 0 ? $fkProduct : null);
if ($result > 0) {
$response['success'] = true;
$response['leistung_id'] = (int)$result;
} else {
$response['error'] = $stz->error ?: 'Fehler beim Hinzufuegen';
}
break;
// ---- Leistung aktualisieren ----
case 'update_leistung':
$stzId = GETPOST('stz_id', 'int');
$leistungId = GETPOST('leistung_id', 'int');
$date = GETPOST('date', 'alpha');
$timeStart = GETPOST('time_start', 'alpha');
$timeEnd = GETPOST('time_end', 'alpha');
$description = GETPOST('description', 'restricthtml');
$fkProduct = GETPOST('fk_product', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->updateLeistung($leistungId, $date, $timeStart, $timeEnd, $description, $fkProduct > 0 ? $fkProduct : null);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $stz->error ?: 'Fehler beim Aktualisieren';
}
break;
// ---- Leistung loeschen ----
case 'delete_leistung':
$stzId = GETPOST('stz_id', 'int');
$leistungId = GETPOST('leistung_id', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->deleteLeistung($leistungId);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Loeschen';
}
break;
// ---- Produktmenge aktualisieren ----
case 'update_qty':
$stzId = GETPOST('stz_id', 'int');
$lineId = GETPOST('line_id', 'int');
$qtyDone = GETPOST('qty_done', 'alpha');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->updateProductQty($lineId, (float)$qtyDone);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Aktualisieren';
}
break;
// ---- Produkt hinzufuegen ----
case 'add_product':
$stzId = GETPOST('stz_id', 'int');
$fkProduct = GETPOST('fk_product', 'int');
$qty = (float)GETPOST('qty', 'alpha');
$description = GETPOST('description', 'restricthtml');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
if ($fkProduct <= 0 && empty($description)) {
$response['error'] = 'Produkt oder Beschreibung erforderlich';
break;
}
// Pruefen ob Produkt im Auftrag vorhanden (fk_commandedet finden)
$fkCommandedet = 0;
if ($fkProduct > 0 && $stz->fk_commande > 0) {
$sqlCd = "SELECT cd.rowid FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sqlCd .= " WHERE cd.fk_commande = ".((int)$stz->fk_commande);
$sqlCd .= " AND cd.fk_product = ".((int)$fkProduct);
$sqlCd .= " LIMIT 1";
$resCd = $db->query($sqlCd);
if ($resCd && $db->num_rows($resCd) > 0) {
$objCd = $db->fetch_object($resCd);
$fkCommandedet = (int)$objCd->rowid;
}
}
// Pruefen ob bereits eine Zeile mit gleichem Produkt existiert -> addieren
$stz->fetchProducts();
$existingLine = null;
foreach ($stz->products as $p) {
if ($fkProduct > 0 && $p->fk_product == $fkProduct && in_array($p->origin, array('order', 'added'))) {
$existingLine = $p;
break;
}
}
if ($existingLine) {
// Menge addieren
$newQty = (float)$existingLine->qty_done + $qty;
$result = $stz->updateProductQty($existingLine->rowid, $newQty);
} else {
$result = $stz->addProduct(
$fkProduct > 0 ? $fkProduct : 0,
$fkCommandedet > 0 ? $fkCommandedet : null,
null,
0,
$qty,
'added',
$description
);
}
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $stz->error ?: 'Fehler beim Hinzufuegen';
}
break;
// ---- Produkt loeschen ----
case 'delete_product':
$stzId = GETPOST('stz_id', 'int');
$lineId = GETPOST('line_id', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->deleteProduct($lineId);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Loeschen';
}
break;
// ---- Mehraufwand hinzufuegen ----
case 'add_mehraufwand':
$stzId = GETPOST('stz_id', 'int');
$fkProduct = GETPOST('fk_product', 'int');
$qty = (float)GETPOST('qty', 'alpha');
$description = GETPOST('description', 'restricthtml');
$reason = GETPOST('reason', 'restricthtml');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$desc = $reason ?: $description;
$result = $stz->addProduct(
$fkProduct > 0 ? $fkProduct : 0,
null, null,
0, $qty,
'additional',
$desc
);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $stz->error ?: 'Fehler beim Hinzufuegen';
}
break;
// ---- Mehraufwand loeschen ----
case 'delete_mehraufwand':
$stzId = GETPOST('stz_id', 'int');
$lineId = GETPOST('line_id', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->deleteProduct($lineId);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Loeschen';
}
break;
// ---- Entfaellt hinzufuegen ----
case 'add_entfaellt':
$stzId = GETPOST('stz_id', 'int');
$source = GETPOST('source', 'alphanohtml'); // commandedet_ID oder mehraufwand_SPROWID
$qty = (float)GETPOST('qty', 'alpha');
$reason = GETPOST('reason', 'restricthtml');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
if (empty($source)) {
$response['error'] = 'Quelle erforderlich';
break;
}
// Source parsen: "cd_123" (Auftragszeile) oder "ma_456" (Mehraufwand)
$parts = explode('_', $source);
$sourceType = $parts[0];
$sourceId = isset($parts[1]) ? (int)$parts[1] : 0;
if ($sourceType === 'cd' && $sourceId > 0) {
// Auftragsposition
$sqlCd = "SELECT cd.rowid, cd.fk_product, cd.description, p.ref, p.label";
$sqlCd .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sqlCd .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
$sqlCd .= " WHERE cd.rowid = ".((int)$sourceId);
$resCd = $db->query($sqlCd);
if ($resCd && $db->num_rows($resCd) > 0) {
$objCd = $db->fetch_object($resCd);
$result = $stz->addProduct(
$objCd->fk_product > 0 ? (int)$objCd->fk_product : 0,
(int)$objCd->rowid,
null, 0, $qty, 'omitted',
$reason ?: '',
$objCd->label ?: strip_tags($objCd->description)
);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $stz->error ?: 'Fehler';
}
} else {
$response['error'] = 'Auftragsposition nicht gefunden';
}
} elseif ($sourceType === 'ma' && $sourceId > 0) {
// Mehraufwand-Zeile
$sqlMa = "SELECT sp.* FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp";
$sqlMa .= " WHERE sp.rowid = ".((int)$sourceId);
$sqlMa .= " AND sp.fk_stundenzettel = ".((int)$stz->id);
$sqlMa .= " AND sp.origin = 'additional'";
$resMa = $db->query($sqlMa);
if ($resMa && $db->num_rows($resMa) > 0) {
$objMa = $db->fetch_object($resMa);
// Mehraufwand-Menge reduzieren
$newMaQty = (float)$objMa->qty_done - $qty;
if ($newMaQty > 0) {
$stz->updateProductQty($sourceId, $newMaQty);
} else {
$stz->deleteProduct($sourceId);
}
// Entfaellt-Zeile anlegen
$result = $stz->addProduct(
$objMa->fk_product > 0 ? (int)$objMa->fk_product : 0,
$objMa->fk_commandedet > 0 ? (int)$objMa->fk_commandedet : null,
null, 0, $qty, 'omitted',
$reason ?: '',
$objMa->product_label
);
$response['success'] = ($result > 0);
} else {
$response['error'] = 'Mehraufwand nicht gefunden';
}
} else {
$response['error'] = 'Ungueltige Quelle';
}
break;
// ---- Entfaellt loeschen ----
case 'delete_entfaellt':
$stzId = GETPOST('stz_id', 'int');
$lineId = GETPOST('line_id', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->deleteProduct($lineId);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Loeschen';
}
break;
// ---- Ruecknahme hinzufuegen ----
case 'add_ruecknahme':
$stzId = GETPOST('stz_id', 'int');
$source = GETPOST('source', 'alphanohtml');
$qty = (float)GETPOST('qty', 'alpha');
$reason = GETPOST('reason', 'restricthtml');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
if (empty($source)) {
$response['error'] = 'Quelle erforderlich';
break;
}
// Source: "cd_123" (Auftragszeile) oder "sp_456" (Stundenzettel-Produkt)
$parts = explode('_', $source);
$sourceType = $parts[0];
$sourceId = isset($parts[1]) ? (int)$parts[1] : 0;
if ($sourceType === 'cd' && $sourceId > 0) {
// Auftragsposition - Ruecknahme
$sqlCd = "SELECT cd.rowid, cd.fk_product, cd.description, p.ref, p.label";
$sqlCd .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sqlCd .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
$sqlCd .= " WHERE cd.rowid = ".((int)$sourceId);
$resCd = $db->query($sqlCd);
if ($resCd && $db->num_rows($resCd) > 0) {
$objCd = $db->fetch_object($resCd);
$result = $stz->addProduct(
$objCd->fk_product > 0 ? (int)$objCd->fk_product : 0,
(int)$objCd->rowid,
null, 0, $qty, 'returned',
$reason ?: '',
$objCd->label ?: strip_tags($objCd->description)
);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $stz->error ?: 'Fehler';
}
} else {
$response['error'] = 'Auftragsposition nicht gefunden';
}
} elseif ($sourceType === 'sp' && $sourceId > 0) {
// Stundenzettel-Produkt (added ohne commandedet)
$sqlSp = "SELECT sp.* FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp";
$sqlSp .= " WHERE sp.rowid = ".((int)$sourceId);
$resSp = $db->query($sqlSp);
if ($resSp && $db->num_rows($resSp) > 0) {
$objSp = $db->fetch_object($resSp);
$result = $stz->addProduct(
$objSp->fk_product > 0 ? (int)$objSp->fk_product : 0,
$objSp->fk_commandedet > 0 ? (int)$objSp->fk_commandedet : null,
null, 0, $qty, 'returned',
$reason ?: '',
$objSp->product_label
);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $stz->error ?: 'Fehler';
}
} else {
$response['error'] = 'Produkt nicht gefunden';
}
} else {
$response['error'] = 'Ungueltige Quelle';
}
break;
// ---- Ruecknahme loeschen ----
case 'delete_ruecknahme':
$stzId = GETPOST('stz_id', 'int');
$lineId = GETPOST('line_id', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->deleteProduct($lineId);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Loeschen';
}
break;
// ---- Notiz hinzufuegen ----
case 'add_note':
$stzId = GETPOST('stz_id', 'int');
$noteText = GETPOST('note', 'restricthtml');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->addNote($user, $noteText);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Hinzufuegen';
}
break;
// ---- Notiz togglen ----
case 'toggle_note':
$stzId = GETPOST('stz_id', 'int');
$noteId = GETPOST('note_id', 'int');
$checked = GETPOST('checked', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
$result = $stz->updateNoteStatus($noteId, $checked);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Aktualisieren';
}
break;
// ---- Notiz loeschen ----
case 'delete_note':
$stzId = GETPOST('stz_id', 'int');
$noteId = GETPOST('note_id', 'int');
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$result = $stz->deleteNote($noteId);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Loeschen';
}
break;
// ---- Produktsuche ----
case 'search_products':
$term = GETPOST('term', 'alphanohtml');
if (strlen($term) < 2) {
$response['error'] = 'Mindestens 2 Zeichen';
break;
}
$sql = "SELECT p.rowid as id, p.ref, p.label";
$sql .= " FROM ".MAIN_DB_PREFIX."product as p";
$sql .= " WHERE (p.ref LIKE '%".$db->escape($term)."%' OR p.label LIKE '%".$db->escape($term)."%')";
$sql .= " AND p.entity IN (".getEntity('product').")";
$sql .= " AND p.tosell = 1"; // Nur verkaufbare Produkte
$sql .= " ORDER BY p.ref ASC";
$sql .= " LIMIT 20";
$resql = $db->query($sql);
$products = array();
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$products[] = array(
'id' => (int)$obj->id,
'ref' => $obj->ref,
'label' => $obj->label
);
}
}
$response['success'] = true;
$response['products'] = $products;
break;
// ---- Entfaellt-Optionen laden ----
case 'get_entfaellt_options':
$stzId = GETPOST('stz_id', 'int');
$orderId = GETPOST('order_id', 'int');
$options = array();
// Auftragspositionen mit verfuegbarer Menge
$sql = "SELECT cd.rowid, cd.fk_product, cd.qty, cd.description,";
$sql .= " p.ref, p.label,";
$sql .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
$sql .= " WHERE s.fk_commande = ".((int)$orderId);
$sql .= " AND (sp.fk_commandedet = cd.rowid OR (sp.fk_product = cd.fk_product AND cd.fk_product > 0))";
$sql .= " AND sp.origin IN ('order','added')), 0) as qty_used,";
$sql .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
$sql .= " WHERE s2.fk_commande = ".((int)$orderId);
$sql .= " AND (sp2.fk_commandedet = cd.rowid OR (sp2.fk_product = cd.fk_product AND cd.fk_product > 0))";
$sql .= " AND sp2.origin = 'omitted'), 0) as qty_omitted";
$sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
$sql .= " WHERE cd.fk_commande = ".((int)$orderId);
$sql .= " ORDER BY cd.rang";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$available = (float)$obj->qty - (float)$obj->qty_used - (float)$obj->qty_omitted;
if ($available > 0) {
$label = $obj->label ?: strip_tags($obj->description);
$options[] = array(
'value' => 'cd_'.$obj->rowid,
'label' => ($obj->ref ? $obj->ref.' - ' : '').$label,
'max_qty' => $available
);
}
}
}
// Mehraufwand-Zeilen
if ($stzId > 0) {
$sqlMa = "SELECT sp.rowid, sp.fk_product, sp.product_ref, sp.product_label, sp.description, sp.qty_done";
$sqlMa .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp";
$sqlMa .= " WHERE sp.fk_stundenzettel = ".((int)$stzId);
$sqlMa .= " AND sp.origin = 'additional'";
$sqlMa .= " AND sp.qty_done > 0";
$resMa = $db->query($sqlMa);
if ($resMa) {
while ($obj = $db->fetch_object($resMa)) {
$label = $obj->product_label ?: strip_tags($obj->description);
$options[] = array(
'value' => 'ma_'.$obj->rowid,
'label' => '[Mehraufwand] '.($obj->product_ref ? $obj->product_ref.' - ' : '').$label,
'max_qty' => (float)$obj->qty_done
);
}
}
}
$response['success'] = true;
$response['options'] = $options;
break;
// ---- Ruecknahme-Optionen laden ----
case 'get_ruecknahme_options':
$stzId = GETPOST('stz_id', 'int');
$orderId = GETPOST('order_id', 'int');
$options = array();
// Verbaute Auftragspositionen
$sql = "SELECT cd.rowid, cd.fk_product, cd.description,";
$sql .= " p.ref, p.label,";
$sql .= " COALESCE((SELECT SUM(sp.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s ON s.rowid = sp.fk_stundenzettel";
$sql .= " WHERE s.fk_commande = ".((int)$orderId);
$sql .= " AND (sp.fk_commandedet = cd.rowid OR (sp.fk_product = cd.fk_product AND cd.fk_product > 0))";
$sql .= " AND sp.origin IN ('order','added')), 0) as qty_delivered,";
$sql .= " COALESCE((SELECT SUM(sp2.qty_done) FROM ".MAIN_DB_PREFIX."stundenzettel_product sp2";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."stundenzettel s2 ON s2.rowid = sp2.fk_stundenzettel";
$sql .= " WHERE s2.fk_commande = ".((int)$orderId);
$sql .= " AND (sp2.fk_commandedet = cd.rowid OR (sp2.fk_product = cd.fk_product AND cd.fk_product > 0))";
$sql .= " AND sp2.origin = 'returned'), 0) as qty_returned";
$sql .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = cd.fk_product";
$sql .= " WHERE cd.fk_commande = ".((int)$orderId);
$sql .= " ORDER BY cd.rang";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$available = (float)$obj->qty_delivered - (float)$obj->qty_returned;
if ($available > 0) {
$label = $obj->label ?: strip_tags($obj->description);
$options[] = array(
'value' => 'cd_'.$obj->rowid,
'label' => ($obj->ref ? $obj->ref.' - ' : '').$label,
'max_qty' => $available
);
}
}
}
// Verbaute Produkte ohne Auftragszeile (manuell hinzugefuegt)
if ($stzId > 0) {
$sqlSp = "SELECT sp.rowid, sp.fk_product, sp.product_ref, sp.product_label, sp.description, sp.qty_done";
$sqlSp .= " FROM ".MAIN_DB_PREFIX."stundenzettel_product as sp";
$sqlSp .= " WHERE sp.fk_stundenzettel = ".((int)$stzId);
$sqlSp .= " AND sp.origin = 'added'";
$sqlSp .= " AND (sp.fk_commandedet IS NULL OR sp.fk_commandedet = 0)";
$sqlSp .= " AND sp.qty_done > 0";
$resSp = $db->query($sqlSp);
if ($resSp) {
while ($obj = $db->fetch_object($resSp)) {
$label = $obj->product_label ?: strip_tags($obj->description);
$options[] = array(
'value' => 'sp_'.$obj->rowid,
'label' => '[Hinzugefuegt] '.($obj->product_ref ? $obj->product_ref.' - ' : '').$label,
'max_qty' => (float)$obj->qty_done
);
}
}
}
$response['success'] = true;
$response['options'] = $options;
break;
// ---- Dienstleistungen laden ----
case 'get_services':
$sql = "SELECT p.rowid as id, p.ref, p.label";
$sql .= " FROM ".MAIN_DB_PREFIX."product as p";
$sql .= " WHERE p.fk_product_type = 1"; // Typ 1 = Dienstleistung
$sql .= " AND p.tosell = 1";
$sql .= " AND p.entity IN (".getEntity('product').")";
$sql .= " ORDER BY p.ref ASC";
$sql .= " LIMIT 50";
$resql = $db->query($sql);
$services = array();
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$services[] = array(
'id' => (int)$obj->id,
'ref' => $obj->ref,
'label' => $obj->label
);
}
}
$response['success'] = true;
$response['services'] = $services;
break;
// ---- Auftragspositionen in STZ uebernehmen ----
case 'transfer_order_products':
if (!$canWrite) {
$response['error'] = 'Keine Schreibberechtigung';
break;
}
$stzId = GETPOST('stz_id', 'int');
$lineIds = GETPOST('line_ids', 'alpha'); // Kommaseparierte commandedet IDs
$stz = new Stundenzettel($db);
if ($stz->fetch($stzId) <= 0) {
$response['error'] = 'Stundenzettel nicht gefunden';
break;
}
if (!canEditStz($stz, $user, $canWriteAll)) {
$response['error'] = 'Keine Berechtigung oder nicht im Entwurf';
break;
}
$ids = array_filter(array_map('intval', explode(',', $lineIds)));
$added = 0;
foreach ($ids as $lineId) {
if ($lineId <= 0) continue;
// Auftragszeile laden
$sqlLine = "SELECT cd.rowid, cd.fk_product, cd.qty, cd.description";
$sqlLine .= " FROM ".MAIN_DB_PREFIX."commandedet as cd";
$sqlLine .= " WHERE cd.rowid = ".((int)$lineId);
$resLine = $db->query($sqlLine);
if (!$resLine || !($objLine = $db->fetch_object($resLine))) continue;
// Pruefen ob schon auf diesem STZ
$sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel_product";
$sqlCheck .= " WHERE fk_stundenzettel = ".((int)$stzId);
$sqlCheck .= " AND fk_commandedet = ".((int)$lineId);
$sqlCheck .= " AND origin IN ('order', 'added')";
$resCheck = $db->query($sqlCheck);
if ($resCheck && $db->num_rows($resCheck) > 0) continue; // Bereits vorhanden
$stz->addProduct(
$objLine->fk_product,
$objLine->rowid, // fk_commandedet
null,
$objLine->qty, // qty_original
0, // qty_done
'order',
$objLine->description
);
$added++;
}
// Mehraufwand-Produkte uebernehmen (als origin='added' ohne fk_commandedet)
$maProductsJson = GETPOST('ma_products', 'restricthtml');
if (!empty($maProductsJson)) {
$maProducts = json_decode($maProductsJson, true);
if (is_array($maProducts)) {
foreach ($maProducts as $ma) {
$fkProduct = isset($ma['fk_product']) ? (int)$ma['fk_product'] : 0;
$description = isset($ma['description']) ? $ma['description'] : '';
$qty = isset($ma['qty']) ? (float)$ma['qty'] : 0;
// Pruefen ob dieses Mehraufwand-Produkt schon auf dem STZ ist
$sqlCheckMA = "SELECT rowid FROM ".MAIN_DB_PREFIX."stundenzettel_product";
$sqlCheckMA .= " WHERE fk_stundenzettel = ".((int)$stzId);
$sqlCheckMA .= " AND origin = 'added'";
$sqlCheckMA .= " AND (fk_commandedet IS NULL OR fk_commandedet = 0)";
if ($fkProduct > 0) {
$sqlCheckMA .= " AND fk_product = ".((int)$fkProduct);
} else {
$sqlCheckMA .= " AND (fk_product IS NULL OR fk_product = 0)";
$sqlCheckMA .= " AND description = '".$db->escape($description)."'";
}
$resCheckMA = $db->query($sqlCheckMA);
if ($resCheckMA && $db->num_rows($resCheckMA) > 0) continue; // Bereits vorhanden
// Produkt-Label laden
$productLabel = '';
if ($fkProduct > 0) {
$sqlProd = "SELECT ref, label FROM ".MAIN_DB_PREFIX."product WHERE rowid = ".((int)$fkProduct);
$resProd = $db->query($sqlProd);
if ($resProd && ($objProd = $db->fetch_object($resProd))) {
$productLabel = $objProd->label;
}
}
$stz->addProduct(
$fkProduct > 0 ? $fkProduct : null,
null, // fk_commandedet (kein Auftragsbezug)
null,
$qty, // qty_original = Zielmenge
0, // qty_done
'added',
$description ?: $productLabel
);
$added++;
}
}
}
$response['success'] = true;
$response['added'] = $added;
break;
default:
$response['error'] = 'Unbekannte Aktion: '.$action;
}
echo json_encode($response);
$db->close();