Die API-Seite zur Baustelle-PWA holt nach, was der Editor mit 1.5.0 bekommen hat.
photo.php kann jetzt size=thumb&w=<px> und rendert ueber bericht_attachment_thumb()
(GD, Cache, EXIF-Rotation) statt die Originaldatei auszuliefern. size=small konnte nur
ein von Dolibarr vorgefertigtes thumbs/<name>_small.<ext> liefern - fuer Uploads ueber
orders.php?action=upload_photo gibt es das nicht, dort laeuft kein vignette(). Die PWA
lud dadurch fuer jede Kachel das komplette Foto: gemessen 9,8 KB statt 235 KB je Kachel.
Antwort mit ETag aus der mtime, Folgeaufrufe enden mit 304.
orders.php?action=photos liefert taken_at (EXIF-Aufnahmezeit ueber bericht_file_taken_at,
sonst Dateidatum) und sortiert danach statt nach filemtime.
reports.php gibt je Seite title und composite_path aus. title konnte ueber pages.php
gesetzt werden, kam aber nie zurueck. composite_path ist das im Editor gebaute
Seitenbild - ohne das zeigen Clients nur das Rohbild ohne Anmerkungen, bei
Raster-Layouts eines von bis zu sechs Bildern und bei title_only gar keins.
Neu: api/note.php - Textnotizen zum Auftrag, aufgebaut wie die Sprachnotiz. Die Notiz
liegt als notiz_<betreff>_<datum>.txt im Auftragsverzeichnis und ist damit auch im
Dolibarr-Auftrag und in der Anhaenge-Spalte des Editors sichtbar. Liste, Lesen, Anlegen,
Aendern, Loeschen. Der file-Parameter wird gegen ^notiz_[A-Za-z0-9_.-]*\.txt$ geprueft;
gegen Pfad-Ausbrueche getestet.
ROADMAP.md wiederhergestellt - sie war mit eb37a4b geloescht worden, weil alle Punkte
abgehakt waren. Sie bleibt ab jetzt liegen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
289 lines
13 KiB
PHP
289 lines
13 KiB
PHP
<?php
|
|
/* GET /api/orders.php
|
|
* Liefert Aufträge des aktuellen Users.
|
|
* GET /api/orders.php?id=<id>
|
|
* Liefert Detail eines einzelnen Auftrags.
|
|
* GET /api/orders.php?id=<id>&action=photos
|
|
* Liefert die Anhang-Bilder/PDFs eines Auftrags.
|
|
* POST /api/orders.php?id=<id>&action=upload_photo
|
|
* multipart: file=<binary> — fügt ein Foto zum Bericht des Auftrags hinzu
|
|
* (legt automatisch einen Bericht an wenn keiner existiert)
|
|
* POST /api/orders.php?action=create
|
|
* JSON-Body: { socid, ref_client, title?, note_private?, date?, date_livraison?, validate? }
|
|
* - ref_client ("Ihr Zeichen") ist PFLICHT (Geschäftsregel; Dolibarr selbst verlangt es nicht).
|
|
* - title (Auftragsbeschreibung) optional -> Extrafield options_auftragsbeschreibung.
|
|
* - date_livraison (Unix-TS) optional -> geplanter Liefertermin (Spalte date_livraison).
|
|
* - validate (bool) optional -> Auftrag direkt freigeben. Der Auftrag bleibt dabei LEER:
|
|
* Commande::valid() verlangt keine Position (verifiziert gegen Dolibarr 22.0.2,
|
|
* KB #355/#989 - nur der Button in commande/card.php ist bei 0 Zeilen gesperrt).
|
|
* Die Leistungen kommen später aus dem Stundenzettel.
|
|
* Legt einen Draft-Auftrag an und übernimmt Kunden-Defaults (Zahlungsbedingungen,
|
|
* Zahlart, Bankkonto, Incoterms, Lieferadresse, Default-Ansprechpartner).
|
|
*/
|
|
require_once __DIR__.'/_inc.php';
|
|
|
|
api_authenticate();
|
|
global $db, $user, $conf;
|
|
|
|
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
|
|
|
|
$id = (int) ($_GET['id'] ?? 0);
|
|
$action = $_GET['action'] ?? '';
|
|
|
|
/* ----- NEUEN AUFTRAG ANLEGEN ----- */
|
|
if ($action === 'create' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$user->hasRight('commande', 'creer')) api_fail('Keine Berechtigung zum Anlegen von Aufträgen', 403);
|
|
|
|
$in = api_input();
|
|
$socid = (int) ($in['socid'] ?? 0);
|
|
$title = trim((string) ($in['title'] ?? '')); // Auftragsbeschreibung (optional)
|
|
$ref_client = trim((string) ($in['ref_client'] ?? '')); // "Ihr Zeichen" (Pflicht)
|
|
$note_private = trim((string) ($in['note_private'] ?? ''));
|
|
$date = (int) ($in['date'] ?? 0);
|
|
if ($date <= 0) $date = dol_now();
|
|
$date_livraison = (int) ($in['date_livraison'] ?? 0); // geplanter Liefertermin (optional)
|
|
$do_validate = !empty($in['validate']); // direkt freigeben?
|
|
|
|
if ($socid <= 0) api_fail('socid fehlt');
|
|
// Geschäftsregel: "Ihr Zeichen" (ref_client) ist Pflicht. Dolibarr-Core verlangt nur den Kunden.
|
|
if ($ref_client === '') api_fail('Ihr Zeichen (Kunden-Referenz) ist Pflicht');
|
|
|
|
// Kundenstamm holen, um Defaults zu ziehen (wie Dolibarr-Web beim Auftrag-Anlegen)
|
|
$soc = new Societe($db);
|
|
if ($soc->fetch($socid) <= 0) api_fail('Kunde nicht gefunden', 404);
|
|
|
|
$cmd = new Commande($db);
|
|
$cmd->socid = $socid;
|
|
$cmd->date_commande = $date;
|
|
$cmd->date = $date;
|
|
$cmd->ref_client = $ref_client;
|
|
$cmd->note_private = $note_private;
|
|
if ($date_livraison > 0) $cmd->delivery_date = $date_livraison; // -> Spalte date_livraison
|
|
|
|
// Kunden-Defaults übernehmen
|
|
if (!empty($soc->cond_reglement_id)) $cmd->cond_reglement_id = (int) $soc->cond_reglement_id;
|
|
if (!empty($soc->mode_reglement_id)) $cmd->mode_reglement_id = (int) $soc->mode_reglement_id;
|
|
if (!empty($soc->fk_account)) $cmd->fk_account = (int) $soc->fk_account;
|
|
if (!empty($soc->fk_incoterms)) $cmd->fk_incoterms = (int) $soc->fk_incoterms;
|
|
if (!empty($soc->location_incoterms)) $cmd->location_incoterms = $soc->location_incoterms;
|
|
if (!empty($soc->fk_delivery_address)) $cmd->fk_delivery_address = (int) $soc->fk_delivery_address;
|
|
|
|
// Titel des Auftrags in Extrafield "auftragsbeschreibung" ablegen (das Bericht-Modul nutzt dieses)
|
|
if ($title !== '') {
|
|
if (!is_array($cmd->array_options)) $cmd->array_options = array();
|
|
$cmd->array_options['options_auftragsbeschreibung'] = $title;
|
|
}
|
|
|
|
$newid = $cmd->create($user);
|
|
if ($newid <= 0) api_fail('Anlegen fehlgeschlagen: '.$cmd->error, 500);
|
|
|
|
// Hauptansprechpartner aus llx_socpeople als externen Kontakt hinzufügen (falls vorhanden)
|
|
$sql_c = "SELECT rowid FROM ".$db->prefix()."socpeople"
|
|
." WHERE fk_soc = ".((int) $socid)." AND statut = 1"
|
|
." ORDER BY rowid ASC LIMIT 1";
|
|
$rc = $db->query($sql_c);
|
|
if ($rc && ($oc = $db->fetch_object($rc))) {
|
|
$cmd->add_contact((int) $oc->rowid, 'CUSTOMER', 'external');
|
|
}
|
|
|
|
/* ----- Optional: Auftrag direkt freigeben -----
|
|
* Ohne Positionen. Früher wurde hier eine Std-Lohn-Zeile mit Menge 1 angehängt, weil
|
|
* angenommen wurde, valid() scheitere am leeren Auftrag. Das stimmt nicht (KB #355/#989,
|
|
* gegen Dolibarr 22.0.2 verifiziert) und die Zeile hatte nur Nachteile: sie verfälscht
|
|
* total_ht, taucht als offene Restmenge in der Lieferauflistung auf, steht im Kunden-PDF
|
|
* und ist nach der Freigabe nicht mehr löschbar. Die Leistungen liefert der Stundenzettel.
|
|
*/
|
|
$validated = false;
|
|
$validate_warning = '';
|
|
if ($do_validate) {
|
|
$cmd->fetch($newid);
|
|
$vr = $cmd->valid($user);
|
|
if ($vr > 0) $validated = true;
|
|
else $validate_warning = 'Freigabe fehlgeschlagen: '.$cmd->error;
|
|
}
|
|
|
|
// Für identische Response-Shape wie GET ?id= neu laden
|
|
$cmd->fetch($newid);
|
|
$cmd->fetch_thirdparty();
|
|
if (method_exists($cmd, 'fetch_optionals')) $cmd->fetch_optionals();
|
|
|
|
api_ok(array(
|
|
'order' => array(
|
|
'id' => (int) $cmd->id,
|
|
'ref' => $cmd->ref,
|
|
'ref_client' => $cmd->ref_client,
|
|
'date' => $cmd->date_commande,
|
|
'date_livraison' => !empty($cmd->delivery_date) ? (int) $cmd->delivery_date : null,
|
|
'status' => (int) $cmd->statut,
|
|
'validated' => $validated,
|
|
'total' => (float) $cmd->total_ttc,
|
|
'note_private' => $cmd->note_private,
|
|
'auftragsbeschreibung' => $cmd->array_options['options_auftragsbeschreibung'] ?? '',
|
|
),
|
|
'customer' => array(
|
|
'id' => (int) ($cmd->thirdparty->id ?? 0),
|
|
'name' => $cmd->thirdparty->name ?? '',
|
|
'address' => $cmd->thirdparty->address ?? '',
|
|
'zip' => $cmd->thirdparty->zip ?? '',
|
|
'town' => $cmd->thirdparty->town ?? '',
|
|
'phone' => $cmd->thirdparty->phone ?? '',
|
|
'email' => $cmd->thirdparty->email ?? '',
|
|
),
|
|
'validate_warning' => $validate_warning,
|
|
));
|
|
}
|
|
|
|
/* ----- LISTE ----- */
|
|
if (!$id) {
|
|
// Filter: nur Aufträge des aktuellen Users (oder Admin sieht alle)
|
|
$where = "c.entity IN (".getEntity('commande').")";
|
|
if (empty($user->admin)) {
|
|
$where .= " AND (c.fk_user_author = ".((int) $user->id)
|
|
." OR c.fk_user_valid = ".((int) $user->id)
|
|
." OR c.fk_user_modif = ".((int) $user->id).")";
|
|
}
|
|
|
|
// Optional: nur offene
|
|
if (!empty($_GET['open'])) {
|
|
$where .= " AND c.fk_statut IN (1, 2)"; // validiert + in Bearbeitung
|
|
}
|
|
// Suchterm
|
|
if (!empty($_GET['q'])) {
|
|
$q = $db->escape($_GET['q']);
|
|
$where .= " AND (c.ref LIKE '%$q%' OR s.nom LIKE '%$q%')";
|
|
}
|
|
|
|
$sql = "SELECT c.rowid, c.ref, c.ref_client, c.date_commande, c.fk_statut, c.total_ttc,"
|
|
." s.rowid AS soc_id, s.nom AS soc_name, s.zip, s.town, s.address,"
|
|
." (SELECT COUNT(*) FROM ".$db->prefix()."bericht b WHERE b.element_type='order' AND b.fk_element=c.rowid) AS bericht_count"
|
|
." FROM ".$db->prefix()."commande c"
|
|
." LEFT JOIN ".$db->prefix()."societe s ON s.rowid = c.fk_soc"
|
|
." WHERE ".$where
|
|
." ORDER BY c.date_commande DESC, c.rowid DESC"
|
|
." LIMIT 200";
|
|
|
|
$r = $db->query($sql);
|
|
if (!$r) api_fail('DB-Fehler: '.$db->lasterror(), 500);
|
|
$orders = array();
|
|
while ($o = $db->fetch_object($r)) {
|
|
$orders[] = array(
|
|
'id' => (int) $o->rowid,
|
|
'ref' => $o->ref,
|
|
'ref_client' => $o->ref_client,
|
|
'date' => $db->jdate($o->date_commande),
|
|
'status'=> (int) $o->fk_statut,
|
|
'total' => (float) $o->total_ttc,
|
|
'customer' => array(
|
|
'id' => (int) $o->soc_id,
|
|
'name' => $o->soc_name,
|
|
'zip' => $o->zip,
|
|
'town' => $o->town,
|
|
'address' => $o->address,
|
|
),
|
|
'bericht_count' => (int) $o->bericht_count,
|
|
);
|
|
}
|
|
api_ok(array('orders' => $orders, 'count' => count($orders)));
|
|
}
|
|
|
|
/* ----- DETAIL eines Auftrags ----- */
|
|
$cmd = new Commande($db);
|
|
if ($cmd->fetch($id) <= 0) api_fail('Auftrag nicht gefunden', 404);
|
|
$cmd->fetch_thirdparty();
|
|
if (method_exists($cmd, 'fetch_optionals')) $cmd->fetch_optionals();
|
|
|
|
if ($action === 'photos') {
|
|
// Anhänge des Auftrags — verwende glob statt dol_dir_list für korrektes filesize
|
|
$upload_dir = $conf->commande->multidir_output[$cmd->entity].'/'.dol_sanitizeFileName($cmd->ref);
|
|
$out = array();
|
|
|
|
if (is_dir($upload_dir)) {
|
|
$allFiles = glob($upload_dir.'/*', GLOB_NOSORT);
|
|
foreach ($allFiles as $filepath) {
|
|
if (!is_file($filepath)) continue;
|
|
$name = basename($filepath);
|
|
// Ignoriere Meta-Dateien, Previews und Hidden Files
|
|
if (strpos($name, '.meta') !== false || strpos($name, '_preview') !== false || $name[0] === '.') continue;
|
|
|
|
$mtime = (int) @filemtime($filepath);
|
|
$out[] = array(
|
|
'filename' => $name,
|
|
'size' => (int) @filesize($filepath),
|
|
'mime' => dol_mimetype($name),
|
|
'date' => $mtime,
|
|
// Aufnahmezeit aus EXIF, sonst das Dateidatum. Bei Uploads aus der Galerie
|
|
// ist die Upload-Zeit die falsche Reihenfolge — auf der Baustelle zählt,
|
|
// wann das Foto entstanden ist. Eigener Parser, weil der Prod-Container
|
|
// kein exif-Modul geladen hat (siehe lib/bericht.lib.php).
|
|
'taken_at' => (int) bericht_file_taken_at($filepath, $mtime),
|
|
'relpath' => str_replace(DOL_DATA_ROOT.'/', '', $filepath),
|
|
);
|
|
}
|
|
}
|
|
// Sortiere nach Aufnahmezeit (neuste zuerst)
|
|
usort($out, function($a, $b) { return $b['taken_at'] <=> $a['taken_at']; });
|
|
api_ok(array('photos' => $out, 'count' => count($out)));
|
|
}
|
|
|
|
if ($action === 'upload_photo' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!$user->hasRight('bericht', 'write')) api_fail('Schreibrechte fehlen', 403);
|
|
if (empty($_FILES['file']['tmp_name'])) api_fail('file fehlt');
|
|
|
|
// Datei validieren
|
|
$orig = dol_sanitizeFileName($_FILES['file']['name']);
|
|
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, array('jpg', 'jpeg', 'png'))) api_fail('Dateityp nicht unterstützt');
|
|
|
|
// Foto direkt in den Dolibarr-Standard-Auftragsordner speichern (kein Bericht nötig)
|
|
$upload_dir = $conf->commande->multidir_output[$cmd->entity].'/'.dol_sanitizeFileName($cmd->ref);
|
|
if (!is_dir($upload_dir)) dol_mkdir($upload_dir);
|
|
|
|
$filename = 'foto_'.dol_print_date(dol_now(), '%Y%m%d_%H%M%S').'_'.uniqid().'.'.$ext;
|
|
$target = $upload_dir.'/'.$filename;
|
|
if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) api_fail('Upload fehlgeschlagen', 500);
|
|
|
|
$relpath = str_replace(DOL_DATA_ROOT.'/', '', $target);
|
|
|
|
api_ok(array(
|
|
'filename' => $filename,
|
|
'relpath' => $relpath,
|
|
'size' => filesize($target),
|
|
));
|
|
}
|
|
|
|
// Default: Auftrags-Detail
|
|
$berichte = Bericht::fetchAllForElement($db, 'order', $cmd->id);
|
|
$berichte_out = array();
|
|
foreach ($berichte as $b) {
|
|
$berichte_out[] = array(
|
|
'id' => (int) $b->id,
|
|
'ref' => $b->ref,
|
|
'titel' => $b->titel,
|
|
'status' => (int) $b->status,
|
|
'datec' => (int) $b->datec,
|
|
);
|
|
}
|
|
|
|
api_ok(array(
|
|
'order' => array(
|
|
'id' => (int) $cmd->id,
|
|
'ref' => $cmd->ref,
|
|
'date' => $cmd->date_commande,
|
|
'status'=> (int) $cmd->statut,
|
|
'total' => (float) $cmd->total_ttc,
|
|
'note_private' => $cmd->note_private,
|
|
'auftragsbeschreibung' => $cmd->array_options['options_auftragsbeschreibung'] ?? '',
|
|
),
|
|
'customer' => array(
|
|
'id' => (int) ($cmd->thirdparty->id ?? 0),
|
|
'name' => $cmd->thirdparty->name ?? '',
|
|
'address' => $cmd->thirdparty->address ?? '',
|
|
'zip' => $cmd->thirdparty->zip ?? '',
|
|
'town' => $cmd->thirdparty->town ?? '',
|
|
'phone' => $cmd->thirdparty->phone ?? '',
|
|
'email' => $cmd->thirdparty->email ?? '',
|
|
),
|
|
'berichte' => $berichte_out,
|
|
));
|