Merge shape3d2/main into develop (take main versions)
This commit is contained in:
@@ -224,4 +224,3 @@ deploy:production:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
# when: manual
|
# when: manual
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
1
config/stagingdemo.txt
Executable file
1
config/stagingdemo.txt
Executable file
@@ -0,0 +1 @@
|
|||||||
|
Demo männ
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
1
partials/structure/.gitkeep
Normal file
1
partials/structure/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
16
public/api/_db.php
Executable file
16
public/api/_db.php
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/_db.php
|
||||||
|
$cfg = require __DIR__ . '/../../src/config.php';
|
||||||
|
$dsn = "mysql:host={$cfg['db_host']};dbname={$cfg['db_name']};charset={$cfg['db_charset']}";
|
||||||
|
$options = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, $cfg['db_user'], $cfg['db_pass'], $options);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode(['error' => 'DB connection failed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
6
public/api/materials.php
Executable file
6
public/api/materials.php
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/materials.php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/_db.php';
|
||||||
|
$stmt = $pdo->query("SELECT * FROM materials WHERE is_active = 1 ORDER BY code");
|
||||||
|
echo json_encode($stmt->fetchAll(), JSON_UNESCAPED_UNICODE);
|
||||||
35
public/api/printer-materials.php
Executable file
35
public/api/printer-materials.php
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/printer-materials.php?id={printer_id}
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/_db.php';
|
||||||
|
|
||||||
|
$printer_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
||||||
|
if ($printer_id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'printer id missing']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$printerStmt = $pdo->prepare("SELECT * FROM printers WHERE id = ?");
|
||||||
|
$printerStmt->execute([$printer_id]);
|
||||||
|
$printer = $printerStmt->fetch();
|
||||||
|
if (!$printer) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'printer not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "SELECT m.*, pms.support_level, pms.partial_reason, pms.extra_info
|
||||||
|
FROM materials m
|
||||||
|
LEFT JOIN printer_material_support pms
|
||||||
|
ON pms.material_id = m.id AND pms.printer_id = :pid
|
||||||
|
WHERE m.is_active = 1
|
||||||
|
ORDER BY m.code";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([':pid' => $printer_id]);
|
||||||
|
$materials = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'printer' => $printer,
|
||||||
|
'materials' => $materials
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
6
public/api/printers.php
Executable file
6
public/api/printers.php
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/printers.php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
require __DIR__ . '/_db.php';
|
||||||
|
$stmt = $pdo->query("SELECT * FROM printers WHERE is_active = 1 ORDER BY name");
|
||||||
|
echo json_encode($stmt->fetchAll(), JSON_UNESCAPED_UNICODE);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
266
public/index.html
Executable file
266
public/index.html
Executable file
@@ -0,0 +1,266 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>3D-Druck Materialmatrix</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: radial-gradient(circle at top, #e2e8f0 0%, #f8fafc 45%, #e2e8f0 90%);
|
||||||
|
}
|
||||||
|
thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
th[data-printer],
|
||||||
|
td[data-printer] {
|
||||||
|
background: rgba(148, 163, 184, 0.05);
|
||||||
|
}
|
||||||
|
#disclaimer {
|
||||||
|
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.25);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-100 min-h-screen">
|
||||||
|
<div class="max-w-6xl mx-auto py-6 space-y-6">
|
||||||
|
<header class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-slate-900">3D-Druck Materialmatrix</h1>
|
||||||
|
<p class="text-xs text-slate-500">Schnell prüfen, welche Filamente auf welchen Druckern laufen.</p>
|
||||||
|
</div>
|
||||||
|
<span id="status" class="text-xs text-slate-500"></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="bg-white/80 backdrop-blur rounded-lg shadow flex gap-6 p-4 min-h-[420px]">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="w-72 space-y-5 border-r pr-4 border-slate-100">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-semibold text-slate-700 mb-2">Drucker auswählen</h2>
|
||||||
|
<label class="block text-xs font-medium mb-1 text-slate-600">Einzelansicht</label>
|
||||||
|
<select id="printerSelect" class="w-full border rounded px-2 py-1 mb-2 text-sm">
|
||||||
|
<option value="">– wird geladen –</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-slate-500">
|
||||||
|
Zeigt die Kompatibilität nur für diesen Drucker.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium mb-1 text-slate-600">Vergleich (mehrere)</label>
|
||||||
|
<select id="printerCompare" multiple class="w-full border rounded px-2 py-1 h-32 text-sm"></select>
|
||||||
|
<p class="text-xs text-slate-500">
|
||||||
|
Strg/⌘ gedrückt halten, um mehrere zu wählen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-slate-400">
|
||||||
|
Tipp: Im Vergleich werden die ausgewählten Drucker rechts als separate Spalten eingefärbt.
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<main class="flex-1 flex flex-col gap-3">
|
||||||
|
<div class="overflow-auto max-h-[70vh] rounded border bg-white" id="tableWrap">
|
||||||
|
<table class="min-w-full text-sm" id="matTable">
|
||||||
|
<thead class="bg-slate-50">
|
||||||
|
<tr id="tableHead">
|
||||||
|
<th class="px-3 py-2 text-left">Material</th>
|
||||||
|
<th class="px-3 py-2 text-left">Eigenschaften</th>
|
||||||
|
<th class="px-3 py-2 text-left">Tg °C</th>
|
||||||
|
<th class="px-3 py-2 text-left">Düse</th>
|
||||||
|
<th class="px-3 py-2 text-left">Platte</th>
|
||||||
|
<th class="px-3 py-2 text-left">Zusatz</th>
|
||||||
|
<th class="px-3 py-2 text-left">Anwendung</th>
|
||||||
|
<th class="px-3 py-2 text-left">Kinder</th>
|
||||||
|
<th class="px-3 py-2 text-left">Emission</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="matBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorBox" class="hidden rounded bg-rose-50 border border-rose-200 text-rose-700 text-sm px-3 py-2"></div>
|
||||||
|
|
||||||
|
<!-- Hinweisblock unten -->
|
||||||
|
<section id="disclaimer" class="mt-2 rounded-lg border border-slate-200 bg-slate-50 p-4 text-xs text-slate-700 leading-snug">
|
||||||
|
<p>
|
||||||
|
<strong>Hinweis:</strong> Dieses Projekt wird privat betrieben und befindet sich im Aufbau.
|
||||||
|
Es sind noch nicht alle Drucker und Materialien eingetragen.
|
||||||
|
Alle Angaben erfolgen nach bestem Wissen, jedoch <u>ohne Gewähr auf Vollständigkeit oder Richtigkeit</u>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = './api';
|
||||||
|
const printerSelect = document.getElementById('printerSelect');
|
||||||
|
const printerCompare = document.getElementById('printerCompare');
|
||||||
|
const matBody = document.getElementById('matBody');
|
||||||
|
const tableHead = document.getElementById('tableHead');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const errorBox = document.getElementById('errorBox');
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
errorBox.textContent = msg;
|
||||||
|
errorBox.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function clearError() {
|
||||||
|
errorBox.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJSON(url) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + url);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPrinters() {
|
||||||
|
try {
|
||||||
|
statusEl.textContent = 'Lade Drucker …';
|
||||||
|
const data = await fetchJSON(`${API_BASE}/printers.php`);
|
||||||
|
printerSelect.innerHTML = '';
|
||||||
|
printerCompare.innerHTML = '';
|
||||||
|
if (!data.length) {
|
||||||
|
printerSelect.innerHTML = '<option value="">(keine Drucker gefunden)</option>';
|
||||||
|
statusEl.textContent = 'Keine Drucker gefunden';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.forEach(p => {
|
||||||
|
printerSelect.appendChild(new Option(p.name, p.id));
|
||||||
|
printerCompare.appendChild(new Option(p.name, p.id));
|
||||||
|
});
|
||||||
|
// ersten Drucker anzeigen
|
||||||
|
loadSinglePrinter(data[0].id);
|
||||||
|
statusEl.textContent = 'Drucker geladen';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showError('Konnte Drucker nicht laden. Prüfe public/api/printers.php.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSinglePrinter(id) {
|
||||||
|
if (!id) return;
|
||||||
|
clearError();
|
||||||
|
statusEl.textContent = 'Lade Materialien …';
|
||||||
|
try {
|
||||||
|
const data = await fetchJSON(`${API_BASE}/printer-materials.php?id=${id}`);
|
||||||
|
renderTable([data]);
|
||||||
|
statusEl.textContent = 'Fertig';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showError('Konnte Materialien für Drucker nicht laden.');
|
||||||
|
statusEl.textContent = 'Fehler';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMultiplePrinters(ids) {
|
||||||
|
if (!ids.length) return;
|
||||||
|
clearError();
|
||||||
|
statusEl.textContent = 'Lade Vergleich …';
|
||||||
|
try {
|
||||||
|
const datasets = await Promise.all(ids.map(id => fetchJSON(`${API_BASE}/printer-materials.php?id=${id}`)));
|
||||||
|
renderTable(datasets);
|
||||||
|
statusEl.textContent = 'Vergleich geladen';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showError('Konnte einen der gewählten Drucker nicht laden.');
|
||||||
|
statusEl.textContent = 'Fehler';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(datasets) {
|
||||||
|
// Kopf neu aufbauen
|
||||||
|
const baseHead = `
|
||||||
|
<th class="px-3 py-2 text-left">Material</th>
|
||||||
|
<th class="px-3 py-2 text-left">Eigenschaften</th>
|
||||||
|
<th class="px-3 py-2 text-left">Tg °C</th>
|
||||||
|
<th class="px-3 py-2 text-left">Düse</th>
|
||||||
|
<th class="px-3 py-2 text-left">Platte</th>
|
||||||
|
<th class="px-3 py-2 text-left">Zusatz</th>
|
||||||
|
<th class="px-3 py-2 text-left">Anwendung</th>
|
||||||
|
<th class="px-3 py-2 text-left">Kinder</th>
|
||||||
|
<th class="px-3 py-2 text-left">Emission</th>
|
||||||
|
`;
|
||||||
|
let printerCols = '';
|
||||||
|
datasets.forEach(ds => {
|
||||||
|
printerCols += `<th class="px-3 py-2 text-left bg-slate-100" data-printer="${ds.printer.id}">${ds.printer.name}</th>`;
|
||||||
|
});
|
||||||
|
tableHead.innerHTML = baseHead + printerCols;
|
||||||
|
|
||||||
|
const materials = datasets[0]?.materials ?? [];
|
||||||
|
matBody.innerHTML = '';
|
||||||
|
|
||||||
|
materials.forEach((m, idx) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/60';
|
||||||
|
|
||||||
|
const kid = m.kid_safety === 'safe' ? '🌿' : (m.kid_safety === 'limited' ? '🟡' : '🔴');
|
||||||
|
const em = m.emission === 'low' ? '✅' : (m.emission === 'medium' ? '⚠️' : '⛔');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<td class="px-3 py-2 font-medium">${m.code}<div class="text-xs text-slate-500">${m.short_desc ?? ''}</div></td>
|
||||||
|
<td class="px-3 py-2">${m.properties ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.tg_celsius ?? '–'}</td>
|
||||||
|
<td class="px-3 py-2">${m.nozzle_req ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.plate_req ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.extra_req ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.application ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${kid}</td>
|
||||||
|
<td class="px-3 py-2">${em}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Drucker-Spalten
|
||||||
|
datasets.forEach(ds => {
|
||||||
|
const match = ds.materials.find(x => x.id === m.id || x.code === m.code);
|
||||||
|
if (!match || !match.support_level) {
|
||||||
|
html += `<td class="px-3 py-2" data-printer="${ds.printer.id}"><span class="px-2 py-1 rounded bg-slate-200 text-xs">unbekannt</span></td>`;
|
||||||
|
} else {
|
||||||
|
let badge = '';
|
||||||
|
if (match.support_level === 'full') {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-green-100 text-green-800 text-xs">✓ voll</span>';
|
||||||
|
} else if (match.support_level === 'partial') {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-amber-100 text-amber-800 text-xs">⚠ teilw.</span>';
|
||||||
|
} else if (match.support_level === 'with_addon') {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-sky-100 text-sky-800 text-xs">⚙ Zusatz</span>';
|
||||||
|
} else {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs">✗ nein</span>';
|
||||||
|
}
|
||||||
|
const note = match.partial_reason
|
||||||
|
? `<div class="text-xs text-slate-400">${match.partial_reason}</div>`
|
||||||
|
: (match.extra_info ? `<div class="text-xs text-slate-400">${match.extra_info}</div>` : '');
|
||||||
|
html += `<td class="px-3 py-2" data-printer="${ds.printer.id}">${badge}${note}</td>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tr.innerHTML = html;
|
||||||
|
matBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
printerSelect.addEventListener('change', e => {
|
||||||
|
const id = e.target.value;
|
||||||
|
if (id) {
|
||||||
|
loadSinglePrinter(id);
|
||||||
|
// Vergleich leeren
|
||||||
|
printerCompare.selectedIndex = -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
printerCompare.addEventListener('change', e => {
|
||||||
|
const ids = Array.from(e.target.selectedOptions).map(o => o.value);
|
||||||
|
if (ids.length) {
|
||||||
|
loadMultiplePrinters(ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start
|
||||||
|
loadPrinters();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
271
public/index.php
271
public/index.php
@@ -1 +1,270 @@
|
|||||||
<?php // TODO
|
<?php
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo <<<'HTML'
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>3D-Druck Materialmatrix</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: radial-gradient(circle at top, #e2e8f0 0%, #f8fafc 45%, #e2e8f0 90%);
|
||||||
|
}
|
||||||
|
thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
th[data-printer],
|
||||||
|
td[data-printer] {
|
||||||
|
background: rgba(148, 163, 184, 0.05);
|
||||||
|
}
|
||||||
|
#disclaimer {
|
||||||
|
box-shadow: inset 0 1px 0 rgba(148, 163, 184, 0.25);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-100 min-h-screen">
|
||||||
|
<div class="max-w-6xl mx-auto py-6 space-y-6">
|
||||||
|
<header class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight text-slate-900">3D-Druck Materialmatrix</h1>
|
||||||
|
<p class="text-xs text-slate-500">Schnell prüfen, welche Filamente auf welchen Druckern laufen.</p>
|
||||||
|
</div>
|
||||||
|
<span id="status" class="text-xs text-slate-500"></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="bg-white/80 backdrop-blur rounded-lg shadow flex gap-6 p-4 min-h-[420px]">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="w-72 space-y-5 border-r pr-4 border-slate-100">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-sm font-semibold text-slate-700 mb-2">Drucker auswählen</h2>
|
||||||
|
<label class="block text-xs font-medium mb-1 text-slate-600">Einzelansicht</label>
|
||||||
|
<select id="printerSelect" class="w-full border rounded px-2 py-1 mb-2 text-sm">
|
||||||
|
<option value="">– wird geladen –</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-slate-500">
|
||||||
|
Zeigt die Kompatibilität nur für diesen Drucker.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium mb-1 text-slate-600">Vergleich (mehrere)</label>
|
||||||
|
<select id="printerCompare" multiple class="w-full border rounded px-2 py-1 h-32 text-sm"></select>
|
||||||
|
<p class="text-xs text-slate-500">
|
||||||
|
Strg/⌘ gedrückt halten, um mehrere zu wählen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-slate-400">
|
||||||
|
Tipp: Im Vergleich werden die ausgewählten Drucker rechts als separate Spalten eingefärbt.
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<main class="flex-1 flex flex-col gap-3">
|
||||||
|
<div class="overflow-auto max-h-[70vh] rounded border bg-white" id="tableWrap">
|
||||||
|
<table class="min-w-full text-sm" id="matTable">
|
||||||
|
<thead class="bg-slate-50">
|
||||||
|
<tr id="tableHead">
|
||||||
|
<th class="px-3 py-2 text-left">Material</th>
|
||||||
|
<th class="px-3 py-2 text-left">Eigenschaften</th>
|
||||||
|
<th class="px-3 py-2 text-left">Tg °C</th>
|
||||||
|
<th class="px-3 py-2 text-left">Düse</th>
|
||||||
|
<th class="px-3 py-2 text-left">Platte</th>
|
||||||
|
<th class="px-3 py-2 text-left">Zusatz</th>
|
||||||
|
<th class="px-3 py-2 text-left">Anwendung</th>
|
||||||
|
<th class="px-3 py-2 text-left">Kinder</th>
|
||||||
|
<th class="px-3 py-2 text-left">Emission</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="matBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="errorBox" class="hidden rounded bg-rose-50 border border-rose-200 text-rose-700 text-sm px-3 py-2"></div>
|
||||||
|
|
||||||
|
<!-- Hinweisblock unten -->
|
||||||
|
<section id="disclaimer" class="mt-2 rounded-lg border border-slate-200 bg-slate-50 p-4 text-xs text-slate-700 leading-snug">
|
||||||
|
<p>
|
||||||
|
<strong>Hinweis:</strong> Dieses Projekt wird privat betrieben und befindet sich im Aufbau.
|
||||||
|
Es sind noch nicht alle Drucker und Materialien eingetragen.
|
||||||
|
Alle Angaben erfolgen nach bestem Wissen, jedoch <u>ohne Gewähr auf Vollständigkeit oder Richtigkeit</u>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE = './api';
|
||||||
|
const printerSelect = document.getElementById('printerSelect');
|
||||||
|
const printerCompare = document.getElementById('printerCompare');
|
||||||
|
const matBody = document.getElementById('matBody');
|
||||||
|
const tableHead = document.getElementById('tableHead');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const errorBox = document.getElementById('errorBox');
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
errorBox.textContent = msg;
|
||||||
|
errorBox.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function clearError() {
|
||||||
|
errorBox.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJSON(url) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + url);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPrinters() {
|
||||||
|
try {
|
||||||
|
statusEl.textContent = 'Lade Drucker …';
|
||||||
|
const data = await fetchJSON(`${API_BASE}/printers.php`);
|
||||||
|
printerSelect.innerHTML = '';
|
||||||
|
printerCompare.innerHTML = '';
|
||||||
|
if (!data.length) {
|
||||||
|
printerSelect.innerHTML = '<option value="">(keine Drucker gefunden)</option>';
|
||||||
|
statusEl.textContent = 'Keine Drucker gefunden';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.forEach(p => {
|
||||||
|
printerSelect.appendChild(new Option(p.name, p.id));
|
||||||
|
printerCompare.appendChild(new Option(p.name, p.id));
|
||||||
|
});
|
||||||
|
// ersten Drucker anzeigen
|
||||||
|
loadSinglePrinter(data[0].id);
|
||||||
|
statusEl.textContent = 'Drucker geladen';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showError('Konnte Drucker nicht laden. Prüfe public/api/printers.php.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSinglePrinter(id) {
|
||||||
|
if (!id) return;
|
||||||
|
clearError();
|
||||||
|
statusEl.textContent = 'Lade Materialien …';
|
||||||
|
try {
|
||||||
|
const data = await fetchJSON(`${API_BASE}/printer-materials.php?id=${id}`);
|
||||||
|
renderTable([data]);
|
||||||
|
statusEl.textContent = 'Fertig';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showError('Konnte Materialien für Drucker nicht laden.');
|
||||||
|
statusEl.textContent = 'Fehler';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMultiplePrinters(ids) {
|
||||||
|
if (!ids.length) return;
|
||||||
|
clearError();
|
||||||
|
statusEl.textContent = 'Lade Vergleich …';
|
||||||
|
try {
|
||||||
|
const datasets = await Promise.all(ids.map(id => fetchJSON(`${API_BASE}/printer-materials.php?id=${id}`)));
|
||||||
|
renderTable(datasets);
|
||||||
|
statusEl.textContent = 'Vergleich geladen';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
showError('Konnte einen der gewählten Drucker nicht laden.');
|
||||||
|
statusEl.textContent = 'Fehler';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(datasets) {
|
||||||
|
// Kopf neu aufbauen
|
||||||
|
const baseHead = `
|
||||||
|
<th class="px-3 py-2 text-left">Material</th>
|
||||||
|
<th class="px-3 py-2 text-left">Eigenschaften</th>
|
||||||
|
<th class="px-3 py-2 text-left">Tg °C</th>
|
||||||
|
<th class="px-3 py-2 text-left">Düse</th>
|
||||||
|
<th class="px-3 py-2 text-left">Platte</th>
|
||||||
|
<th class="px-3 py-2 text-left">Zusatz</th>
|
||||||
|
<th class="px-3 py-2 text-left">Anwendung</th>
|
||||||
|
<th class="px-3 py-2 text-left">Kinder</th>
|
||||||
|
<th class="px-3 py-2 text-left">Emission</th>
|
||||||
|
`;
|
||||||
|
let printerCols = '';
|
||||||
|
datasets.forEach(ds => {
|
||||||
|
printerCols += `<th class="px-3 py-2 text-left bg-slate-100" data-printer="${ds.printer.id}">${ds.printer.name}</th>`;
|
||||||
|
});
|
||||||
|
tableHead.innerHTML = baseHead + printerCols;
|
||||||
|
|
||||||
|
const materials = datasets[0]?.materials ?? [];
|
||||||
|
matBody.innerHTML = '';
|
||||||
|
|
||||||
|
materials.forEach((m, idx) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/60';
|
||||||
|
|
||||||
|
const kid = m.kid_safety === 'safe' ? '🌿' : (m.kid_safety === 'limited' ? '🟡' : '🔴');
|
||||||
|
const em = m.emission === 'low' ? '✅' : (m.emission === 'medium' ? '⚠️' : '⛔');
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<td class="px-3 py-2 font-medium">${m.code}<div class="text-xs text-slate-500">${m.short_desc ?? ''}</div></td>
|
||||||
|
<td class="px-3 py-2">${m.properties ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.tg_celsius ?? '–'}</td>
|
||||||
|
<td class="px-3 py-2">${m.nozzle_req ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.plate_req ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.extra_req ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${m.application ?? ''}</td>
|
||||||
|
<td class="px-3 py-2">${kid}</td>
|
||||||
|
<td class="px-3 py-2">${em}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Drucker-Spalten
|
||||||
|
datasets.forEach(ds => {
|
||||||
|
const match = ds.materials.find(x => x.id === m.id || x.code === m.code);
|
||||||
|
if (!match || !match.support_level) {
|
||||||
|
html += `<td class="px-3 py-2" data-printer="${ds.printer.id}"><span class="px-2 py-1 rounded bg-slate-200 text-xs">unbekannt</span></td>`;
|
||||||
|
} else {
|
||||||
|
let badge = '';
|
||||||
|
if (match.support_level === 'full') {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-green-100 text-green-800 text-xs">✓ voll</span>';
|
||||||
|
} else if (match.support_level === 'partial') {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-amber-100 text-amber-800 text-xs">⚠ teilw.</span>';
|
||||||
|
} else if (match.support_level === 'with_addon') {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-sky-100 text-sky-800 text-xs">⚙ Zusatz</span>';
|
||||||
|
} else {
|
||||||
|
badge = '<span class="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs">✗ nein</span>';
|
||||||
|
}
|
||||||
|
const note = match.partial_reason
|
||||||
|
? `<div class="text-xs text-slate-400">${match.partial_reason}</div>`
|
||||||
|
: (match.extra_info ? `<div class="text-xs text-slate-400">${match.extra_info}</div>` : '');
|
||||||
|
html += `<td class="px-3 py-2" data-printer="${ds.printer.id}">${badge}${note}</td>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tr.innerHTML = html;
|
||||||
|
matBody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
printerSelect.addEventListener('change', e => {
|
||||||
|
const id = e.target.value;
|
||||||
|
if (id) {
|
||||||
|
loadSinglePrinter(id);
|
||||||
|
// Vergleich leeren
|
||||||
|
printerCompare.selectedIndex = -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
printerCompare.addEventListener('change', e => {
|
||||||
|
const ids = Array.from(e.target.selectedOptions).map(o => o.value);
|
||||||
|
if (ids.length) {
|
||||||
|
loadMultiplePrinters(ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start
|
||||||
|
loadPrinters();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
HTML;
|
||||||
|
|||||||
1
public/test.fff
Normal file
1
public/test.fff
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dfdfassa
|
||||||
8
shape3d.code-workspace
Normal file
8
shape3d.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
1
src/README.md
Normal file
1
src/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# src\n\nTODO: Core backend/business logic.\n
|
||||||
10
src/config.php
Executable file
10
src/config.php
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
// inc/config.php
|
||||||
|
// Diese Datei außerhalb des Webroots ablegen!
|
||||||
|
return [
|
||||||
|
'db_host' => 'localhost',
|
||||||
|
'db_name' => 'd0453540',
|
||||||
|
'db_user' => 'd0453540',
|
||||||
|
'db_pass' => 'P6jGRrSaX8QSiBMEJBL7',
|
||||||
|
'db_charset' => 'utf8mb4',
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user