Online PHP and Javascript Decoder decode hidden script to uncover its real functionality


if (!defined('NUESSLE_AUTHENTICATED') || NUESSLE_AUTHENTICATED !== true) { http_response_code(401); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); echo json_encode(['error'=>'Bitte die geschützte App unter https://hausmeisterei-nuessle.de/auftraege/ öffnen.']); exit; }

header("Cache-Control: no-store");
/**
 * ============================================================
 *  Hausmeisterei Nüssle – Kleinaufträge-API
 *  Brücke zwischen der Web-App, der Strato MySQL-Datenbank
 *  und der Anthropic KI (sicherer Proxy)
 * ============================================================
 *  Diese Datei bleibt auf dem Server. Datenbank-Passwort und
 *  API-Key sind hier sicher verborgen und niemals im Browser
 *  sichtbar.
 * ============================================================
 */

error_reporting(E_ALL);
ini_set('display_errors', 0);

header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json; charset=utf-8");

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    
}

// ── Datenbank-Zugangsdaten ────────────────────────────────────
// WICHTIG: Bitte mit den Zugangsdaten DEINER neuen Kleinaufträge-Datenbank ersetzen!
$DB_HOST = "database-5020741602.webspace-host.com";
$DB_NAME = "dbs15808981";
$DB_USER = "dbu2752249";
$DB_PASS = "5pt4de+Kleinauftraege";

// ── Anthropic API-Key (sicher auf dem Server, NICHT im Browser) ──
$ANTHROPIC_API_KEY = "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";

// ── Verbindung herstellen ─────────────────────────────────────
function productionConnection() {
    global $DB_HOST, $DB_NAME, $DB_USER, $DB_PASS;
    try {
        $pdo = new PDO(
            "mysql:host=$DB_HOST;dbname=$DB_NAME;charset=utf8mb4",
            $DB_USER,
            $DB_PASS,
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            ]
        );
        return $pdo;
    } catch (PDOException $e) {
        http_response_code(500);
        echo json_encode(["error" => "Datenbankverbindung fehlgeschlagen: " . $e->getMessage()]);
        
    }
}

// ── Tabelle automatisch anlegen, falls noch nicht vorhanden ───
function ensureTables($pdo) {
    $pdo->exec("
        CREATE TABLE IF NOT EXISTS kleinauftraege (
            id BIGINT PRIMARY KEY,
            datum VARCHAR(20),
            waGesendet VARCHAR(50),
            objekt VARCHAR(255),
            kategorie VARCHAR(255),
            prio VARCHAR(50),
            beschreibung TEXT,
            gemeldet VARCHAR(255),
            zustaendig VARCHAR(100),
            status VARCHAR(50),
            bemerkung TEXT,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    ");
    // Spalte waGesendet nachträglich hinzufügen falls nicht vorhanden
    try {
        $pdo->exec("ALTER TABLE kleinauftraege ADD COLUMN waGesendet VARCHAR(50) DEFAULT ''");
    } catch(Exception $e) {
        // Spalte existiert bereits – kein Fehler
    }
}

$action = $_GET['action'] ?? '';
$input = json_decode(file_get_contents('php://input'), true);

// ── KI-ANALYSE (sicherer Proxy, Key bleibt auf dem Server) ────
if ($action === 'kiAnalyse') {
    $prompt = $input['prompt'] ?? '';
    if (!$prompt) {
        http_response_code(400);
        echo json_encode(["error" => "Kein Prompt übergeben"]);
        
    }

    $ch = curl_init("https://api.anthropic.com/v1/messages");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 55);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        "Content-Type: application/json",
        "x-api-key: $ANTHROPIC_API_KEY",
        "anthropic-version: 2023-06-01"
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
        "model" => "claude-sonnet-4-5",
        "max_tokens" => 500,
        "messages" => [["role" => "user", "content" => $prompt]]
    ]));
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr  = curl_error($ch);
    curl_close($ch);

    if ($curlErr) {
        http_response_code(500);
        echo json_encode(["error" => "Verbindung zur KI fehlgeschlagen: " . $curlErr]);
        
    }

    http_response_code($httpCode);
    echo $response;
    
}

// Nur lesende Anbindungen. Schlüssel bleiben im PHP-Serverteil.
function integrationConfig() {
    static $config;
    if ($config === null) {
        $path=__DIR__.'/kleinauftraege_integrationen.php';
        $config=is_file($path) ? require $path : [];
        if (!is_array($config)) $config=[];
        if(!empty($_SERVER['NUESSLE_TIMESHEET_KEY']))$config['timesheetKey']=$_SERVER['NUESSLE_TIMESHEET_KEY'];
    }
    return $config;
}
function integrationSecure() {
    if(PHP_SAPI==='cli' && defined('NUESSLE_CRON') && NUESSLE_CRON===true)return true;
    return !empty($_SERVER['REMOTE_USER']) && (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS']!=='off');
}
function integrationGuard() {
    if (!integrationSecure()) throw new RuntimeException('Für Timesheet und Easybill sind HTTPS und ein geschützter App-Zugang erforderlich.');
}
function integrationStatus() {
    $c=integrationConfig(); $secure=integrationSecure();
    return ['secure'=>$secure,'timesheet'=>$secure&&!empty($c['timesheetKey']),
        'easybill'=>array_keys(array_filter($c['easybill']??[],function($a){return !empty($a['key']);})),
        'message'=>$secure?'':'Automatischer Abruf noch gesperrt: geschützter Zugang und Anbindungen einrichten.'];
}
function integrationHttp($service,$path,$body=null,$account='') {
    integrationGuard(); $c=integrationConfig();
    if ($service==='timesheet') {
        $key=$c['timesheetKey']??''; $base='https://api.timesheet.io'; $auth='ApiKey ';
        if (!in_array($path,['/v1/projects','/v1/projects/search','/v1/teams'],true) && !preg_match('~^/v1/(?:tasks(?:/search|/[a-zA-Z0-9_-]+)?|projects/[a-zA-Z0-9_-]+/members(?:/list)?)$~',$path)) throw new RuntimeException('Unzulässiger Timesheet-Aufruf.');
    } else {
        $key=$c['easybill'][$account]['key']??''; $base='https://api.easybill.de/rest/v1'; $auth='Bearer ';
        if (!preg_match('~^/documents(?:\?|/[0-9]+)~',$path) || $body!==null) throw new RuntimeException('Unzulässiger Easybill-Aufruf.');
    }
    if (!$key) throw new RuntimeException('Diese Anbindung ist noch nicht eingerichtet.');
    $h=curl_init($base.$path);
    $headers=['Authorization: '.$auth.$key,'Accept: application/json'];
    if ($body!==null) $headers[]='Content-Type: application/json';
    curl_setopt_array($h,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>false,CURLOPT_CONNECTTIMEOUT=>8,CURLOPT_TIMEOUT=>25,CURLOPT_HTTPHEADER=>$headers,CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2]);
    if($body!==null) curl_setopt_array($h,[CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>json_encode($body)]);
    $raw=curl_exec($h); $code=curl_getinfo($h,CURLINFO_HTTP_CODE); curl_close($h);
    if($raw===false || $code<200 || $code>=300) throw new RuntimeException(ucfirst($service).' ist nicht abrufbar (HTTP '.$code.'). Gespeicherte Zuordnungen bleiben erhalten.');
    $data=json_decode($raw,true); if(!is_array($data)) throw new RuntimeException('Die Schnittstelle liefert keine lesbaren Daten.');
    return $data;
}
function integrationDate($value) {
    if (!is_string($value) || !preg_match('/^\d{4}-\d{2}-\d{2}$/D',$value)) throw new RuntimeException('Ungültiges Datum.');
    $d=DateTimeImmutable::createFromFormat('!Y-m-d',$value,new DateTimeZone('Europe/Berlin'));
    if(!$d || $d->format('Y-m-d')!==$value) throw new RuntimeException('Ungültiges Datum.');
    return $d;
}
function normalizeTimeEntry($t) {
    if(!empty($t['deleted']) || !empty($t['running'])) return null;
    if(empty($t['id']) || empty($t['startDateTime']) || empty($t['endDateTime'])) return null;
    $tz=new DateTimeZone('Europe/Berlin');
    $start=new DateTimeImmutable($t['startDateTime'],$tz); $end=new DateTimeImmutable($t['endDateTime'],$tz);
    $a=$start->getTimestamp(); $b=$end->getTimestamp();
    if($b<=$a || $b-$a>7*86400) throw new RuntimeException('Unplausible Timesheet-Zeit. Bitte den Originaleintrag prüfen.');
    // Pausenintervalle vereinen, damit überlappende Pausen nicht doppelt abgezogen werden.
    $intervals=[];
    foreach($t['pauses']??[] as $p) {
        if(!empty($p['deleted'])) continue;
        if(empty($p['startDateTime']) || empty($p['endDateTime'])) throw new RuntimeException('Unvollständige Pause im Timesheet.');
        $pa=max($a,(new DateTimeImmutable($p['startDateTime'],$tz))->getTimestamp());
        $pb=min($b,(new DateTimeImmutable($p['endDateTime'],$tz))->getTimestamp());
        if($pb>$pa) $intervals[]=[$pa,$pb];
    }
    if(!empty($t['durationBreak']) && !$intervals) throw new RuntimeException('Pausendetails fehlen. Dieser Abruf kann keine verlässliche Nettozeit berechnen.');
    usort($intervals,function($a,$b){return $a[0]<=>$b[0];});
    $pause=0; $last=$a;
    foreach($intervals as $p){$pause+=max(0,$p[1]-max($last,$p[0]));$last=max($last,$p[1]);}
    $m=$t['member']??[];
    return ['id'=>(string)$t['id'],'userId'=>(string)($m['uid']??$t['user']??''),
        'employee'=>shortText($m['displayName']??trim(($m['firstname']??'').' '.($m['lastname']??'')),200),
        'projectId'=>(string)($t['project']['id']??''),'project'=>shortText($t['project']['title']??'',300),
        'description'=>shortText($t['description']??'',4000),'date'=>$start->setTimezone($tz)->format('Y-m-d'),
        'start'=>$start->format(DateTimeInterface::ATOM),'end'=>$end->format(DateTimeInterface::ATOM),
        'seconds'=>$b-$a-$pause,'pauseSeconds'=>$pause,'sourceUpdated'=>$t['lastUpdate']??null,'checkedAt'=>date('c')];
}
function invoiceSummary($d,$account) {
    // Ein Rechnungsstatus sagt nichts über Zahlungseingang oder vollständige Auftragsabdeckung aus.
    $known=array_key_exists('is_draft',$d) && array_key_exists('cancel_id',$d);
    $valid=$known && ($d['type']??'')==='INVOICE' && !$d['is_draft'] && empty($d['cancel_id']) && !empty($d['number']) && !empty($d['document_date']);
    return ['id'=>(string)($d['id']??''),'account'=>$account,'number'=>shortText($d['number']??'',100),
        'date'=>shortText($d['document_date']??'',10),'title'=>shortText(strip_tags($d['title']??''),500),
        'customer'=>shortText($d['customer_id']??'',100),'valid'=>$valid,
        'status'=>!$known?'Prüfung unvollständig':(!empty($d['cancel_id'])?'Storniert':(!empty($d['is_draft'])?'Entwurf':($valid?'Erstellte Rechnung':'Keine Rechnung'))),
        'checkedAt'=>date('c')];
}
function integrationCache($pdo,$key,$type,$data) {
    $s=$pdo->prepare('INSERT INTO kleinauftraege_quellen (source_key,source_type,data,checked_at) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE data=VALUES(data),checked_at=VALUES(checked_at)');
    $s->execute([$key,$type,json_encode($data,JSON_UNESCAPED_UNICODE),time()]);
}
function cachedSource($pdo,$key,$maxAge=900) {
    $s=$pdo->prepare('SELECT data,checked_at FROM kleinauftraege_quellen WHERE source_key=?');$s->execute([$key]);$r=$s->fetch();
    if(!$r || $r['checked_at']<time()-$maxAge) return null;
    return json_decode($r['data'],true);
}
function timeKey($id) { return 'ts:'.hash('sha256',(string)$id); }
function invoiceKey($account,$id) { return 'eb:'.hash('sha256',$account.':'.$id); }
require_once __DIR__.'/kleinauftraege_vorgaenge.php';
require_once __DIR__.'/kleinauftraege_mail.php';
require_once __DIR__.'/kleinauftraege_timesheet.php';
require_once __DIR__.'/kleinauftraege_objekte.php';
function integrationActions($pdo,$action,$input) {
    tsActions($pdo,$action,$input);
    timesheetTestActions($pdo,$action);
    if($action==='integrationStatus') reply(integrationStatus());
    if(!in_array($action,['timesheetSearch','easybillLookup'],true)) return;
    integrationGuard();
    if($_SERVER['REQUEST_METHOD']!=='POST') throw new RuntimeException('Bitte die Suchfunktion in der App verwenden.');
    if($action==='timesheetSearch') {
        $start=integrationDate($input['from']??'');$end=integrationDate($input['to']??'');
        if($end<$start || $end->getTimestamp()-$start->getTimestamp()>93*86400) throw new RuntimeException('Bitte einen Zeitraum von höchstens drei Monaten wählen.');
        $params=['startDate'=>$start->format('Y-m-d'),'endDate'=>$end->format('Y-m-d'),'sort'=>'dateTime','order'=>'desc','limit'=>100,'populatePauses'=>true];
        $c=integrationConfig();if(!empty($c['timesheetOrganizationId']))$params['organizationId']=$c['timesheetOrganizationId'];
        $items=[];$complete=false;$skipped=0;
        for($page=1;$page<=10;$page++) {
            $params['page']=$page;$data=integrationHttp('timesheet','/v1/tasks/search',$params);
            if(!isset($data['items']) || !is_array($data['items'])) throw new RuntimeException('Timesheet-Antwort ist unvollständig.');
            foreach($data['items'] as $t) { $entry=normalizeTimeEntry($t);if(!$entry){$skipped++;continue;}
                if($entry['date']<$params['startDate'] || $entry['date']>$params['endDate']) continue;
                $items[$entry['id']]=$entry;
            }
            if(count($data['items'])<100){$complete=true;break;}
        }
        if(!$complete) throw new RuntimeException('Zu viele Einträge. Bitte den Zeitraum verkleinern; es wird keine unvollständige Summe übernommen.');
        $assigned=[];foreach($pdo->query('SELECT source_key,auftrag_id FROM kleinauftraege_zeitzuordnung')->fetchAll() as $r)$assigned[$r['source_key']]=(int)$r['auftrag_id'];
        foreach($items as &$entry){$key=timeKey($entry['id']);integrationCache($pdo,$key,'timesheet',$entry);$entry['assignedOrderId']=$assigned[$key]??null;}unset($entry);
        reply(['items'=>array_values($items),'skipped'=>$skipped,'checkedAt'=>date('c')]);
    }
    $account=shortText($input['account']??'',80);$number=shortText($input['number']??'',100);
    if(!$number) throw new RuntimeException('Bitte die genaue Rechnungsnummer eintragen.');
    $data=integrationHttp('easybill','/documents?'.http_build_query(['number'=>$number,'type'=>'INVOICE','limit'=>100]),null,$account);
    if(!isset($data['items']) || !is_array($data['items'])) throw new RuntimeException('Easybill-Antwort ist unvollständig.');
    $matches=array_values(array_filter($data['items'],function($d)use($number){return (string)($d['number']??'')===$number;}));
    if(count($matches)!==1) throw new RuntimeException(count($matches)?'Mehrere Rechnungen gefunden. Zuordnung bitte in Easybill prüfen.':'Diese Rechnungsnummer wurde in diesem Easybill-Konto nicht gefunden.');
    $d=invoiceSummary($matches[0],$account);if(!$d['id'])throw new RuntimeException('Rechnungs-ID fehlt.');
    integrationCache($pdo,invoiceKey($account,$d['id']),'easybill',$d);reply(['document'=>$d]);
}
function saveIntegrationData($pdo,$id,$incoming,$old,&$w) {
    $w['lastWorkedDate']=shortText($incoming['lastWorkedDate']??$w['lastWorkedDate']??'',10);
    if($w['lastWorkedDate'])integrationDate($w['lastWorkedDate']);
    $w['workNote']=shortText($incoming['workNote']??$w['workNote']??'',2000);
    $w['easybillAccount']=shortText($incoming['easybillAccount']??$w['easybillAccount']??'',80);
    $w['invoiceCoverage']=in_array($incoming['invoiceCoverage']??'',['vollständig','teilweise'],true)?$incoming['invoiceCoverage']:'';
    $existing=$old['workflow']['timeEntries']??[];
    if(array_key_exists('timeSelection',$incoming)) {
        integrationGuard();$ids=$incoming['timeSelection'];
        if(!is_array($ids) || count($ids)>200)throw new RuntimeException('Maximal 200 Zeiteinträge je Auftrag.');
        $ids=array_values(array_unique(array_map('strval',$ids)));
        foreach($existing as $entry)if(($entry['origin']??'')==='timesheet-auto' && !in_array((string)$entry['id'],$ids,true))throw new RuntimeException('Automatisch zugeordnete Buchungen bitte in Timesheet berichtigen und erneut abrufen.');
        $entries=[];$oldMap=[];
        foreach($existing as $e)$oldMap[$e['id']]=$e;
        foreach($ids as $entryId){$entry=cachedSource($pdo,timeKey($entryId))??($oldMap[$entryId]??null);if(!$entry)throw new RuntimeException('Zeiteintrag ist nicht mehr aktuell. Bitte Timesheet erneut durchsuchen.');$entries[]=$entry;}
        $s=$pdo->prepare('DELETE FROM kleinauftraege_zeitzuordnung WHERE auftrag_id=?');$s->execute([$id]);
        foreach($entries as $entry){
            $s=$pdo->prepare('SELECT auftrag_id FROM kleinauftraege_zeitzuordnung WHERE source_key=? FOR UPDATE');$s->execute([timeKey($entry['id'])]);$other=$s->fetch();
            if($other)throw new RuntimeException('Dieser Zeiteintrag gehört bereits zu Auftrag '.$other['auftrag_id'].'. Bitte zuerst dort lösen.');
            $s=$pdo->prepare('INSERT INTO kleinauftraege_zeitzuordnung (source_key,auftrag_id) VALUES (?,?)');$s->execute([timeKey($entry['id']),$id]);
        }
        $before=array_map(function($e){return [$e['id'],$e['seconds'],$e['sourceUpdated']??null];},$existing);
        $after=array_map(function($e){return [$e['id'],$e['seconds'],$e['sourceUpdated']??null];},$entries);
        if($before!==$after)$w['stundenGeprueft']=false;
        $w['timeEntries']=$entries;
    }
    if(!empty($w['timeEntries'])){
        integrationGuard(); $w['stunden']=(string)round(array_sum(array_column($w['timeEntries'],'seconds'))/3600,2);
        $dates=array_column($w['timeEntries'],'date');sort($dates);$w['lastWorkedDate']=end($dates);
    } elseif($existing && array_key_exists('timeSelection',$incoming)){$w['stunden']='';$w['lastWorkedDate']='';}
    if(isset($w['invoiceLink']) && (($w['invoiceLink']['number']??'')!==$w['rechnungNr'] || ($w['invoiceLink']['account']??'')!==$w['easybillAccount'])) unset($w['invoiceLink']);
    if(!empty($incoming['invoiceSelection'])) {
        integrationGuard();$selection=$incoming['invoiceSelection'];
        $d=cachedSource($pdo,invoiceKey($selection['account']??'',$selection['id']??''));
        if(!$d)throw new RuntimeException('Rechnungsprüfung abgelaufen. Bitte erneut in Easybill prüfen.');
        if($d['number']!==$w['rechnungNr'] || $d['account']!==$w['easybillAccount'])throw new RuntimeException('Rechnungsnummer oder Konto wurde nach der Prüfung geändert.');
        $w['invoiceLink']=$d;
        if($d['valid'])$w['rechnungDatum']=$d['date'];
    }
    if(($w['phase']??'')==='Abgeschlossen' && ($w['abrechnungsart']??'')!=='Vertrag') {
        if(isset($w['invoiceLink']) && !$w['invoiceLink']['valid'])throw new RuntimeException('Diese Easybill-Rechnung ist ein Entwurf, storniert oder nicht vollständig geprüft.');
        if(isset($w['invoiceLink']) && $w['invoiceCoverage']!=='vollständig')throw new RuntimeException('Bitte bestätigen, dass die Rechnung diesen Auftrag vollständig abdeckt.');
        if($w['invoiceCoverage']==='teilweise')throw new RuntimeException('Eine Teilabrechnung schließt den Auftrag noch nicht ab.');
    }
}

// Workflow-Erweiterung 2026-10-21. Bestehende Datensätze werden nicht umklassifiziert.
function reply($data, $code=200) {
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
    exit;
}
function stage($a) {
    if (!empty($a['workflow']['phase'])) return $a['workflow']['phase'];
    if (($a['status'] ?? '') === 'Erledigt') return 'Abrechnung prüfen';
    if (($a['status'] ?? '') === 'In Bearbeitung') return 'In Bearbeitung';
    return 'Bestand prüfen';
}
function assemble($base, $meta) {
    $base['id'] = (int)$base['id'];
    $base['workflow'] = $meta ? (json_decode($meta['data'], true) ?: []) : [];
    $base['revision'] = hash('sha256', json_encode([$base, $meta ? $meta['revision'] : 0]));
    return $base;
}
function readOne($pdo, $id, $lock=false) {
    $s=$pdo->prepare('SELECT * FROM kleinauftraege WHERE id=?'.($lock?' FOR UPDATE':''));
    $s->execute([$id]); $base=$s->fetch();
    if (!$base) return null;
    $s=$pdo->prepare('SELECT data,revision FROM kleinauftraege_workflow WHERE auftrag_id=?');
    $s->execute([$id]); return tsAttach($pdo,assemble($base, $s->fetch()));
}
function shortText($value, $max=20000) {
    if (!is_string($value) && !is_numeric($value)) return '';
    return substr(trim((string)$value), 0, $max);
}
function cleanMessage($m) {
    $r=['from'=>shortText($m['from']??'',500),'subject'=>shortText($m['subject']??'',1000),
        'text'=>shortText($m['text']??'',300000),'messageId'=>shortText($m['messageId']??'',1000),
        'replyIds'=>array_slice(array_map(function($s){return shortText($s,1000);},$m['replyIds']??[]),-30),'date'=>shortText($m['date']??'',80),'receivedAt'=>date('c'),'attachments'=>[]];
    $total=0;
    foreach (($m['attachments']??[]) as $f) {
        $bytes=base64_decode($f['base64']??'',true);
        if ($bytes===false) throw new RuntimeException('Ungültiger Anhang.');
        $total+=strlen($bytes);
        if ($total>5*1024*1024 || count($r['attachments'])>=12) throw new RuntimeException('Maximal 12 Anhänge und zusammen 5 MB je Nachricht.');
        $r['attachments'][]=['name'=>shortText(basename($f['name']??'Anhang'),250),
          'type'=>'application/octet-stream','base64'=>base64_encode($bytes),'size'=>strlen($bytes)];
    }
    if (!$r['text'] && !$r['subject'] && !$r['attachments']) throw new RuntimeException('Die Nachricht ist leer.');
    $fingerprint=$r['messageId'] ? 'mid:'.strtolower(trim($r['messageId'],'<> ')) :
      'body:'.preg_replace('/\s+/u',' ',trim($r['from'].' '.$r['subject'].' '.$r['text']));
    if (!$r['text'] && !$r['subject']) $fingerprint.=json_encode(array_column($r['attachments'],'base64'));
    $r['key']=hash('sha256',$fingerprint);
    return $r;
}
if(defined('NUESSLE_LIBRARY_ONLY') && NUESSLE_LIBRARY_ONLY===true)return;
try {
    $pdo=getConnection();
    ensureTables($pdo);
    $pdo->exec('CREATE TABLE IF NOT EXISTS kleinauftraege_workflow (auftrag_id BIGINT PRIMARY KEY, data LONGTEXT NOT NULL, revision INT NOT NULL DEFAULT 1) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
    $pdo->exec('CREATE TABLE IF NOT EXISTS kleinauftraege_nachrichten (fingerprint CHAR(64) PRIMARY KEY, auftrag_id BIGINT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
    $pdo->exec('CREATE TABLE IF NOT EXISTS kleinauftraege_quellen (source_key VARCHAR(80) PRIMARY KEY, source_type VARCHAR(20) NOT NULL, data LONGTEXT NOT NULL, checked_at BIGINT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
    $pdo->exec('CREATE TABLE IF NOT EXISTS kleinauftraege_zeitzuordnung (source_key VARCHAR(80) PRIMARY KEY, auftrag_id BIGINT NOT NULL, KEY auftrag_idx (auftrag_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
    tsEnsure($pdo);
    mailActions($pdo,$action,$input);
    objActions($pdo,$action,$input);
    integrationActions($pdo,$action,$input);
    if ($action==='getAll') {
        $all=$pdo->query('SELECT * FROM kleinauftraege ORDER BY id DESC')->fetchAll();
        $meta=[]; foreach($pdo->query('SELECT * FROM kleinauftraege_workflow')->fetchAll() as $m) $meta[$m['auftrag_id']]=$m;
        foreach($meta as $m){$w=json_decode($m['data'],true);if(!empty($w['timeEntries']) || !empty($w['invoiceLink']))integrationGuard();}
        
        $tsMeta=[];foreach($pdo->query('SELECT * FROM kleinauftraege_timesheet')->fetchAll() as $r){$v=json_decode($r['state'],true);$v['projectId']=$r['project_id'];$v['lastSync']=(int)$r['last_sync'];$v['lastAttempt']=(int)$r['last_attempt'];$tsMeta[$r['auftrag_id']]=tsPublicState($v);}
        reply(['apiVersion'=>5,'integrations'=>array_merge(integrationStatus(),['automation'=>tsAutomationStatus($pdo),'mail'=>mailPublic($pdo)]),'auftraege'=>array_map(function($a)use($meta,$tsMeta){$v=assemble($a,$meta[$a['id']]??null);if(isset($tsMeta[$a['id']]))$v['workflow']['timesheet']=$tsMeta[$a['id']];return $v;},$all)]);
    }
    if ($action==='saveAuftrag') {
        if ($_SERVER['REQUEST_METHOD']!=='POST' || !is_array($input)) reply(['error'=>'Ungültige Anfrage.'],400);
        $id=$input['id']??null;
        if (!is_numeric($id) || $id<1 || $id>9007199254740991 || floor((float)$id)!=(float)$id) reply(['error'=>'Ungültige Auftragsnummer.'],400);
        $pdo->beginTransaction();
        $old=readOne($pdo,$id,true);
        if ($old && isset($input['revision']) && !hash_equals($old['revision'],(string)$input['revision'])) {
            $pdo->rollBack(); reply(['error'=>'Dieser Auftrag wurde inzwischen geändert. Bitte neu laden und Ihre Änderung erneut eintragen.'],409);
        }
        $w=$old['workflow']??[];unset($w['timesheet']);
        if(!empty($w['timeEntries']) || !empty($w['invoiceLink'])) integrationGuard();
        $incoming=$input['workflow']??null;
        if($old && (vgKind($old['workflow'])==='vorgang'||!empty($old['workflow']['archivedAt'])) && !is_array($incoming))throw new RuntimeException('Bitte diesen Vorgang mit seinem aktuellen Versionsstand speichern.');
        if (!$old && !is_array($incoming)) $incoming=['phase'=>'Anfrage','mitarbeiter'=>array_filter(array_map('trim',explode(',',(string)($input['zustaendig']??''))))];
        if (is_array($incoming)) {
            if ($old && !isset($input['revision'])) { $pdo->rollBack(); reply(['error'=>'Versionsstand fehlt. Bitte neu laden.'],409); }
            vgPrepare($old,$input,$incoming);
            $allowed=['Neu','Zugeordnet','Erledigt','Aussortiert','Anfrage','Angebot / Zusage','Freigegeben – noch nicht eingeplant','Eingeplant','In Bearbeitung','Wartet','Zur Abrechnung','Abgeschlossen','Kein Auftrag','Abrechnung prüfen','Bestand prüfen'];
            $phase=shortText($incoming['phase']??stage($old??[]),80);
            if (!in_array($phase,$allowed,true)) throw new RuntimeException('Unbekannter Status.');
            foreach(['einsatzDatum','einsatzZeit','freigabeAm','angebotNr','angebotVersandt','kundenzusage','warteGrund','wiedervorlage','stunden','timesheetBezug','material','abrechnungsgrundlage','abrechnungsart','rechnungNr','rechnungDatum','vertragsbezug','keinAuftragGrund'] as $k) $w[$k]=shortText($incoming[$k]??'');
            foreach(['stundenGeprueft','materialGeprueft','grundlageGeprueft'] as $k) $w[$k]=!empty($incoming[$k]);
            $w['mitarbeiter']=array_values(array_unique(array_filter(array_map(function($s){return shortText($s,100);},$incoming['mitarbeiter']??[]))));
            $w['phase']=$phase;
            $w['timesheetMode']=in_array($incoming['timesheetMode']??'',['auto','manual'],true)?$incoming['timesheetMode']:tsMode($w);
            vgApply($old,$input,$incoming,$w);$phase=$w['phase'];
            if ($phase==='Freigegeben – noch nicht eingeplant' && !$w['freigabeAm']) $w['freigabeAm']=date('Y-m-d');
            if ($phase==='Eingeplant' && (!$w['mitarbeiter'] || !$w['einsatzDatum'] || !$w['freigabeAm'])) throw new RuntimeException('Für Eingeplant sind Freigabe, Mitarbeiter und Einsatzdatum erforderlich.');
            if ($phase==='Wartet' && (!$w['warteGrund'] || !$w['wiedervorlage'])) throw new RuntimeException('Bitte Wartegrund und Wiedervorlage eintragen.');
            if(vgKind($w)==='auftrag')saveIntegrationData($pdo,$id,$incoming,$old,$w);
            if ($phase==='Abgeschlossen') {
                if (!$w['stundenGeprueft'] || !$w['materialGeprueft'] || !$w['grundlageGeprueft']) throw new RuntimeException('Bitte die drei Abrechnungsprüfungen bestätigen.');
                if ($w['abrechnungsart']==='Vertrag') { if (!$w['vertragsbezug']) throw new RuntimeException('Vertragsbezug fehlt.'); }
                else { if (!$w['rechnungNr'] || !$w['rechnungDatum']) throw new RuntimeException('Rechnungsnummer und Rechnungsdatum fehlen.'); }
            }
        }
        if (!empty($input['newMessage'])) {
            $msg=cleanMessage($input['newMessage']);
            if (!$old) foreach($msg['replyIds'] as $replyId){$rs=$pdo->prepare('SELECT auftrag_id FROM kleinauftraege_nachrichten WHERE fingerprint=?');$rs->execute([hash('sha256','mid:'.strtolower(trim($replyId,'<> ')))]);$related=$rs->fetch();if($related){$pdo->rollBack();reply(['error'=>'Diese Folge-E-Mail gehört zu einem bestehenden Vorgang.','relatedId'=>(int)$related['auftrag_id']],409);}}
            $s=$pdo->prepare('SELECT auftrag_id FROM kleinauftraege_nachrichten WHERE fingerprint=?');$s->execute([$msg['key']]);$duplicate=$s->fetch();
            if($duplicate){$pdo->rollBack();reply(['error'=>'Diese Nachricht ist bereits einem Auftrag zugeordnet.','duplicateId'=>(int)$duplicate['auftrag_id']],409);}
            $s=$pdo->prepare('INSERT INTO kleinauftraege_nachrichten (fingerprint,auftrag_id) VALUES (?,?)');$s->execute([$msg['key'],$id]);
            $w['messages'][]=$msg;
        }
        if (strlen(json_encode($w))>20*1024*1024) throw new RuntimeException('Dieser Vorgang erreicht die Größenbegrenzung von 20 MB.');
        $base=$old??[];
        foreach(['datum','objekt','kategorie','prio','beschreibung','gemeldet','zustaendig','status','bemerkung','waGesendet'] as $key) $base[$key]=shortText($input[$key]??$base[$key]??'',in_array($key,['beschreibung','bemerkung'])?20000:250);
        if (!$base['objekt'] || !$base['beschreibung']) throw new RuntimeException('Objekt und Beschreibung sind erforderlich.');
        if (is_array($incoming)) {
            $base['zustaendig']=implode(', ',$w['mitarbeiter']);
            if(strlen($base['zustaendig'])>100) throw new RuntimeException('Die Mitarbeiternamen sind zusammen zu lang (max. 100 Zeichen).');
            $base['status']=$w['phase'];
        }
        $s=$pdo->prepare('INSERT INTO kleinauftraege (id,datum,objekt,kategorie,prio,beschreibung,gemeldet,zustaendig,status,bemerkung,waGesendet) VALUES (?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE datum=VALUES(datum),objekt=VALUES(objekt),kategorie=VALUES(kategorie),prio=VALUES(prio),beschreibung=VALUES(beschreibung),gemeldet=VALUES(gemeldet),zustaendig=VALUES(zustaendig),status=VALUES(status),bemerkung=VALUES(bemerkung),waGesendet=VALUES(waGesendet)');
        $s->execute([$id,$base['datum'],$base['objekt'],$base['kategorie'],$base['prio'],$base['beschreibung'],$base['gemeldet'],$base['zustaendig'],$base['status'],$base['bemerkung'],$base['waGesendet']]);
        if (is_array($incoming) || !empty($input['newMessage'])) {
            $w['history'][]=['at'=>date('c'),'from'=>$old?stage($old):'Neu','to'=>$w['phase']??stage($old??[]),'message'=>!empty($input['newMessage']),'by'=>shortText($_SERVER['REMOTE_USER']??'Büro',100),'action'=>$input['workflowAction']??'','kind'=>$w['kind']??'auftrag','assignees'=>$w['mitarbeiter']??[],'note'=>shortText($input['changeNote']??'',500)];
            $s=$pdo->prepare('INSERT INTO kleinauftraege_workflow (auftrag_id,data,revision) VALUES (?,?,1) ON DUPLICATE KEY UPDATE data=VALUES(data),revision=revision+1');$s->execute([$id,json_encode($w,JSON_UNESCAPED_UNICODE|JSON_INVALID_UTF8_SUBSTITUTE)]);
        }
        if(is_array($incoming))tsQueue($pdo,$id,$w);
        $saved=readOne($pdo,$id);if(!empty($input['dryRun'])){$pdo->rollBack();reply(['success'=>true,'validated'=>true,'auftrag'=>$saved]);}
        $pdo->commit();
        if(is_array($incoming) && tsEligible($w))tsProcess($pdo,$id);
        reply(['success'=>true,'id'=>(int)$id,'auftrag'=>readOne($pdo,$id)]);
    }
    // Keine endgültige Löschung über die neue App. Aussortieren bleibt rückgängig machbar.
    if ($action==='deleteAuftrag') reply(['error'=>'Bitte den Vorgang als Kein Auftrag kennzeichnen.'],400);
    reply(['error'=>'Unbekannte Aktion.'],400);
} catch(Throwable $e) {
    if(isset($pdo) && $pdo->inTransaction())$pdo->rollBack();
    if($e instanceof PDOException) { error_log('Kleinauftraege: '.$e->getMessage()); reply(['error'=>'Datenbankfehler. Es wurde nichts gespeichert. Bitte erneut laden.'],500); }
    reply(['error'=>$e->getMessage()],400);
}

function timesheetTestActions($pdo,$action) {
    if(!in_array($action,['timesheetTestStatus','timesheetTestCreate','timesheetTestRead','timesheetTestInspect'],true)) return;
    integrationGuard();
    if($action==='timesheetTestInspect'){ $teams=integrationHttp('timesheet','/v1/teams');$projects=integrationHttp('timesheet','/v1/projects/search',['search'=>'TEST AU-20261021','limit'=>100,'page'=>1]); reply(['teams'=>array_map(function($t){return ['id'=>$t['id'],'name'=>$t['name']];},$teams['items']??[]),'projects'=>array_map(function($p){return ['id'=>$p['id'],'title'=>$p['title']];},$projects['items']??[])]); }
    $key='ts-project-test-v1';
    $s=$pdo->prepare('SELECT data FROM kleinauftraege_quellen WHERE source_key=?');$s->execute([$key]);$row=$s->fetch();$state=$row?json_decode($row['data'],true):null;
    if($action==='timesheetTestStatus')reply(['state'=>$state,'connected'=>integrationStatus()['timesheet']]);
    if($_SERVER['REQUEST_METHOD']!=='POST')throw new RuntimeException('Bitte die Testoberfläche verwenden.');
    if($action==='timesheetTestCreate') {
        $retry=false;
        if($state && ($state['status']??'')==='uncertain') {
            $found=integrationHttp('timesheet','/v1/projects/search',['search'=>'TEST AU-20261021','limit'=>100,'page'=>1]);
            if(!isset($found['items']) || count($found['items'])!==0)throw new RuntimeException('Bitte vorhandene Timesheet-Projekte prüfen; keine erneute Erstellung.');
            $retry=true;$previousData=$row['data'];
        } elseif($state)reply(['state'=>$state,'alreadyStarted'=>true]);
        // Einmalige Reservierung verhindert Doppelprojekte bei Doppelklick/Timeout.
        $state=['status'=>'creating','title'=>'TEST AU-20261021 · Auftrags-App Nüssle','teamName'=>'Sonderaufträge UG','startedAt'=>date('c')];
        if($retry){$s=$pdo->prepare('UPDATE kleinauftraege_quellen SET data=?,checked_at=? WHERE source_key=? AND data=?');$s->execute([json_encode($state),time(),$key,$previousData]);}
        else {$s=$pdo->prepare('INSERT IGNORE INTO kleinauftraege_quellen (source_key,source_type,data,checked_at) VALUES (?,?,?,?)');
        $s->execute([$key,'timesheet-project-test',json_encode($state),time()]);}
        if($s->rowCount()!==1)throw new RuntimeException('Der Test wurde bereits gestartet. Bitte aktualisieren.');
        try {
            $project=integrationHttp('timesheet','/v1/projects',['teamId'=>'74f16317b0d44a4e8b21222531f1c831','title'=>$state['title'],'description'=>'Technischer Verbindungstest der Auftrags-App. Keine Kundenleistung. Keine automatische Mitarbeiterfreigabe.','archived'=>false,'taskDefaultBillable'=>false]);
            if(empty($project['id']) || !is_string($project['id']))throw new RuntimeException('Projektantwort enthält keine eindeutige ID.');
            $state['status']='ready';$state['projectId']=$project['id'];$state['title']=$project['title']??$state['title'];
            integrationCache($pdo,$key,'timesheet-project-test',$state);reply(['state'=>$state]);
        }catch(Throwable $e) {
            $state['status']='uncertain';integrationCache($pdo,$key,'timesheet-project-test',$state);
            throw new RuntimeException('Projekterstellung nicht bestätigt. Bitte Timesheet prüfen; es wird kein zweites Projekt automatisch angelegt. '.$e->getMessage());
        }
    }
    if(($state['status']??'')!=='ready' || empty($state['projectId']))throw new RuntimeException('Zuerst das Testprojekt anlegen.');
    $items=[];$complete=false;
    for($page=1;$page<=10;$page++){
        $data=integrationHttp('timesheet','/v1/tasks/search',['projectId'=>$state['projectId'],'limit'=>100,'page'=>$page,'populatePauses'=>true,'sort'=>'dateTime','order'=>'desc']);
        if(!isset($data['items']) || !is_array($data['items']))throw new RuntimeException('Timesheet-Antwort ist unvollständig.');
        foreach($data['items'] as $t){if(($t['project']['id']??'')!==$state['projectId'])throw new RuntimeException('Projektfilter wurde nicht bestätigt.');$entry=normalizeTimeEntry($t);if($entry)$items[$entry['id']]=$entry;}
        if(count($data['items'])<100){$complete=true;break;}
    }
    if(!$complete)throw new RuntimeException('Zu viele Einträge für den Test. Es wird keine unvollständige Summe angezeigt.');
    $seconds=0;$last=null;foreach($items as $e){$seconds+=$e['seconds'];if($last===null || strcmp($e['end'],$last)>0)$last=$e['end'];}
    reply(['projectId'=>$state['projectId'],'items'=>array_values($items),'seconds'=>$seconds,'lastEntry'=>$last,'checkedAt'=>date('c'),'note'=>'Nur abgeschlossene Buchungen; laufende Timer sind nicht enthalten.']);
}
 



© 2023 Quttera Ltd. All rights reserved.