615 lines
25 KiB
PHP
Executable File
615 lines
25 KiB
PHP
Executable File
<?php
|
||
// index.php — Spine/Item Label Generator with CSV + Sheet Template (Demco 1.5"x1.0")
|
||
// Prints to exact physical size using CSS inches and @page rules.
|
||
// Preset defaults can be adjusted from the UI without code changes.
|
||
|
||
declare(strict_types=1);
|
||
|
||
$autoloadOk = file_exists(__DIR__ . '/vendor/autoload.php');
|
||
if ($autoloadOk) {
|
||
require __DIR__ . '/vendor/autoload.php';
|
||
}
|
||
|
||
use Com\Tecnick\Barcode\Barcode;
|
||
|
||
function safe($s) { return htmlspecialchars(trim((string)($s ?? '')), ENT_QUOTES, 'UTF-8'); }
|
||
function strip_bom(string $s): string {
|
||
if (substr($s, 0, 3) === "\xEF\xBB\xBF") return substr($s, 3);
|
||
return $s;
|
||
}
|
||
function detect_delimiter(string $headerLine): string {
|
||
$candidates = [",", "\t", ";", "|"];
|
||
$counts = [];
|
||
foreach ($candidates as $d) $counts[$d] = substr_count($headerLine, $d);
|
||
arsort($counts);
|
||
return array_key_first($counts) ?: ",";
|
||
}
|
||
function read_csv_assoc(string $tmpPath, array &$errors): array {
|
||
$rows = [];
|
||
$required = ['data1','data2','data3','data4','barcode'];
|
||
$raw = file_get_contents($tmpPath);
|
||
if ($raw === false || $raw === '') { $errors[] = "Uploaded CSV is empty or unreadable."; return []; }
|
||
$raw = strip_bom($raw);
|
||
$lines = preg_split('/\r\n|\r|\n/', $raw);
|
||
if (!$lines || count($lines) < 1) { $errors[] = "CSV appears to have no lines."; return []; }
|
||
$headerLine = '';
|
||
foreach ($lines as $ln) { if (trim($ln) !== '') { $headerLine = $ln; break; } }
|
||
if ($headerLine === '') { $errors[] = "CSV header line is missing."; return []; }
|
||
$delimiter = detect_delimiter($headerLine);
|
||
|
||
$fh = fopen($tmpPath, 'r'); if (!$fh) { $errors[] = "Unable to open uploaded CSV."; return []; }
|
||
|
||
// Read header
|
||
$hdr = [];
|
||
while (($line = fgets($fh)) !== false) {
|
||
$line = strip_bom($line);
|
||
if (trim($line) === '') continue;
|
||
$hdr = str_getcsv($line, $delimiter);
|
||
break;
|
||
}
|
||
if (!$hdr) { $errors[] = "Could not read header row."; fclose($fh); return []; }
|
||
$headers = array_map(fn($h) => strtolower(trim((string)$h)), $hdr);
|
||
|
||
foreach (['data1','data2','data3','data4','barcode'] as $req) {
|
||
if (!in_array($req, $headers, true)) $errors[] = "Missing required header: <code>{$req}</code>.";
|
||
}
|
||
if ($errors) { fclose($fh); return []; }
|
||
|
||
while (($row = fgetcsv($fh, 0, $delimiter)) !== false) {
|
||
if ($row === [null] || $row === false) continue;
|
||
if (count(array_filter($row, fn($v) => trim((string)$v) !== '')) === 0) continue; // skip blank line
|
||
$assoc = [];
|
||
foreach ($required as $key) {
|
||
$idx = array_search($key, $headers, true);
|
||
$assoc[$key] = $idx !== false && isset($row[$idx]) ? trim((string)$row[$idx]) : '';
|
||
}
|
||
if (implode('', $assoc) === '') continue;
|
||
$rows[] = $assoc;
|
||
}
|
||
fclose($fh);
|
||
if (empty($rows)) $errors[] = "CSV contained no usable rows after the header.";
|
||
return $rows;
|
||
}
|
||
function render_datamatrix_svg(string $content): string {
|
||
global $autoloadOk;
|
||
if (!$autoloadOk) return '<!-- Barcode library not installed -->';
|
||
$barcode = new Barcode();
|
||
$obj = $barcode->getBarcodeObj(
|
||
'DATAMATRIX',
|
||
$content,
|
||
-1, -1, 'black', [0,0,0,0]
|
||
);
|
||
$obj->setBackgroundColor('white');
|
||
return $obj->getSvgCode();
|
||
}
|
||
|
||
/* ---------------- Sheet Template System ---------------- */
|
||
// Default preset: Demco 1.5" x 1.0" on Letter (guess: 5x8 grid, 0.5" margins, 0.125" gaps).
|
||
// You can adjust these in the UI and re-preview until alignment is perfect.
|
||
$defaults = [
|
||
'page_width_in' => 8.5,
|
||
'page_height_in' => 11.0,
|
||
'margin_top_in' => 0.5,
|
||
'margin_right_in' => 0.5,
|
||
'margin_bottom_in' => 0.5,
|
||
'margin_left_in' => 0.5,
|
||
'cols' => 5,
|
||
'rows' => 8,
|
||
'label_w_in' => 1.5,
|
||
'label_h_in' => 1.0,
|
||
'gap_h_in' => 0.125, // horizontal gap between columns
|
||
'gap_v_in' => 0.125, // vertical gap between rows
|
||
'barcode_h_in' => 0.30, // barcode height inside 1.0" label
|
||
'font_in' => 0.12, // approx 8.6pt
|
||
'show_outlines' => '1'
|
||
];
|
||
|
||
// Pull current template params from POST (template form) or keep defaults
|
||
function valf($name, $def) {
|
||
return isset($_POST[$name]) && $_POST[$name] !== '' ? $_POST[$name] : $def;
|
||
}
|
||
$page_width_in = (float) valf('page_width_in', $defaults['page_width_in']);
|
||
$page_height_in = (float) valf('page_height_in', $defaults['page_height_in']);
|
||
$margin_top_in = (float) valf('margin_top_in', $defaults['margin_top_in']);
|
||
$margin_right_in = (float) valf('margin_right_in', $defaults['margin_right_in']);
|
||
$margin_bottom_in = (float) valf('margin_bottom_in', $defaults['margin_bottom_in']);
|
||
$margin_left_in = (float) valf('margin_left_in', $defaults['margin_left_in']);
|
||
$cols = max(1, (int) valf('cols', $defaults['cols']));
|
||
$rows = max(1, (int) valf('rows', $defaults['rows']));
|
||
$label_w_in = (float) valf('label_w_in', $defaults['label_w_in']);
|
||
$label_h_in = (float) valf('label_h_in', $defaults['label_h_in']);
|
||
$gap_h_in = (float) valf('gap_h_in', $defaults['gap_h_in']);
|
||
$gap_v_in = (float) valf('gap_v_in', $defaults['gap_v_in']);
|
||
$barcode_h_in = (float) valf('barcode_h_in', $defaults['barcode_h_in']);
|
||
$font_in = (float) valf('font_in', $defaults['font_in']);
|
||
$show_outlines = isset($_POST['show_outlines']) ? '1' : $defaults['show_outlines'];
|
||
|
||
// Records pipeline (single or CSV)
|
||
$errors = [];
|
||
$records = [];
|
||
$isPosted = ($_SERVER['REQUEST_METHOD'] === 'POST');
|
||
|
||
$data1 = $_POST['data1'] ?? '';
|
||
$data2 = $_POST['data2'] ?? '';
|
||
$data3 = $_POST['data3'] ?? '';
|
||
$data4 = $_POST['data4'] ?? '';
|
||
$barcodeText = $_POST['barcode'] ?? '';
|
||
|
||
$mode = $_POST['mode'] ?? '';
|
||
|
||
if ($isPosted && $mode === 'csv') {
|
||
if (!isset($_FILES['csvfile']) || !is_uploaded_file($_FILES['csvfile']['tmp_name'])) {
|
||
$errors[] = "Please choose a CSV file to upload.";
|
||
} else {
|
||
$size = $_FILES['csvfile']['size'] ?? 0;
|
||
if ($size > 10 * 1024 * 1024) { $errors[] = "CSV too large (max 10MB)."; }
|
||
else { $records = read_csv_assoc($_FILES['csvfile']['tmp_name'], $errors); }
|
||
}
|
||
} elseif ($isPosted && $mode === 'single') {
|
||
if ($barcodeText === '') $errors[] = "Please provide a Barcode value.";
|
||
else {
|
||
$records[] = ['data1'=>$data1,'data2'=>$data2,'data3'=>$data3,'data4'=>$data4,'barcode'=>$barcodeText];
|
||
}
|
||
}
|
||
|
||
// Single-label physical dimensions (as fed by the label printer: 3.7cm wide × 2.5cm tall)
|
||
// Content is rotated 90° so it reads correctly when applied to a spine.
|
||
$single_feed_w_cm = 3.7;
|
||
$single_feed_h_cm = 2.5;
|
||
// After rotation, the visible label area is 2.5cm wide × 3.7cm tall
|
||
$single_label_w_cm = 2.5;
|
||
$single_label_h_cm = 3.7;
|
||
|
||
$is_single = ($mode === 'single' && !empty($records));
|
||
|
||
// Compute pagination (CSV/sheet mode only)
|
||
$perPage = $cols * $rows;
|
||
$pages = [];
|
||
if ($records && !$is_single) {
|
||
$pages = array_chunk($records, $perPage);
|
||
}
|
||
|
||
?>
|
||
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Label Generator — CSV + Sheet Template</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<style>
|
||
:root {
|
||
--font: "Helvetica Neue", Arial, sans-serif;
|
||
|
||
/* These CSS variables are set from PHP inline below to match your template */
|
||
--page-w: <?= $page_width_in ?>in;
|
||
--page-h: <?= $page_height_in ?>in;
|
||
--m-top: <?= $margin_top_in ?>in;
|
||
--m-right: <?= $margin_right_in ?>in;
|
||
--m-bottom: <?= $margin_bottom_in ?>in;
|
||
--m-left: <?= $margin_left_in ?>in;
|
||
|
||
--cols: <?= $cols ?>;
|
||
--rows: <?= $rows ?>;
|
||
|
||
--label-w: <?= $label_w_in ?>in;
|
||
--label-h: <?= $label_h_in ?>in;
|
||
|
||
--gap-h: <?= $gap_h_in ?>in;
|
||
--gap-v: <?= $gap_v_in ?>in;
|
||
|
||
--barcode-h: <?= $barcode_h_in ?>in;
|
||
--font-in: <?= $font_in ?>in;
|
||
|
||
--show-outlines: <?= $show_outlines === '1' ? 1 : 0 ?>;
|
||
}
|
||
|
||
<?php if ($is_single): ?>
|
||
@page {
|
||
size: <?= $single_feed_w_cm ?>cm <?= $single_feed_h_cm ?>cm landscape;
|
||
margin: 0;
|
||
}
|
||
<?php else: ?>
|
||
@page {
|
||
size: var(--page-w) var(--page-h);
|
||
margin: 0; /* we handle margins inside the .sheet via padding */
|
||
}
|
||
<?php endif; ?>
|
||
|
||
body {
|
||
font-family: var(--font);
|
||
margin: 1.5rem;
|
||
color: #222;
|
||
line-height: 1.35;
|
||
}
|
||
h1 { margin-bottom: .25rem; }
|
||
.subtle { color: #666; margin-top: 0; }
|
||
|
||
.wrap {
|
||
display: grid;
|
||
gap: 1.25rem;
|
||
max-width: 1200px;
|
||
}
|
||
|
||
.pane {
|
||
border: 1px solid #ddd;
|
||
border-radius: 10px;
|
||
padding: 1rem;
|
||
}
|
||
.pane h2 { margin: 0 0 .75rem; font-size: 1.1rem; }
|
||
|
||
form { display: grid; gap: .75rem; }
|
||
.grid2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: .75rem 1rem;
|
||
}
|
||
label { font-weight: 600; }
|
||
input[type="text"], input[type="number"], input[type="file"] {
|
||
padding: .5rem .6rem;
|
||
border: 1px solid #bbb;
|
||
border-radius: 6px;
|
||
font-size: 1rem;
|
||
width: 100%;
|
||
background: #fff;
|
||
}
|
||
.actions { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||
button {
|
||
padding: .6rem .9rem;
|
||
border: 1px solid #444;
|
||
background: #111; color: #fff;
|
||
border-radius: 8px; cursor: pointer; font-weight: 600;
|
||
}
|
||
.ghost { background: #f7f7f7; color: #222; border-color: #ccc; }
|
||
.info { font-size: .9rem; color: #555; }
|
||
.error {
|
||
padding: .75rem 1rem; border: 1px solid #d33; background: #fee; color: #a00;
|
||
border-radius: 8px; margin-bottom: .75rem;
|
||
}
|
||
|
||
/* ====== SHEET LAYOUT ====== */
|
||
.sheets { display: grid; gap: 1rem; }
|
||
.sheet {
|
||
width: var(--page-w);
|
||
height: var(--page-h);
|
||
padding: var(--m-top) var(--m-right) var(--m-bottom) var(--m-left);
|
||
box-sizing: border-box;
|
||
background: white;
|
||
position: relative;
|
||
page-break-after: always;
|
||
}
|
||
.grid {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: grid;
|
||
grid-template-columns: repeat(<?= $cols ?>, var(--label-w));
|
||
grid-template-rows: repeat(<?= $rows ?>, var(--label-h));
|
||
column-gap: var(--gap-h);
|
||
row-gap: var(--gap-v);
|
||
justify-content: start; /* no stretching */
|
||
align-content: start;
|
||
}
|
||
|
||
.cell {
|
||
width: var(--label-w);
|
||
height: var(--label-h);
|
||
box-sizing: border-box;
|
||
padding: 0.06in; /* small inner padding for aesthetics; adjust if needed */
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
background: #fff;
|
||
border: calc(var(--show-outlines) * 1px) dashed #999; /* outline toggle for test prints */
|
||
}
|
||
|
||
.label-text {
|
||
display: grid;
|
||
gap: 0.04in;
|
||
text-align: center;
|
||
margin: 0;
|
||
line-height: 1.2;
|
||
font-size: var(--font-in);
|
||
word-break: break-word;
|
||
}
|
||
.label-text-line { white-space: pre-wrap; }
|
||
|
||
.barcode-block { display: flex; justify-content: center; align-items: flex-end; margin-top: 0.02in; }
|
||
.barcode-block svg { display: block; height: var(--barcode-h); width: auto; }
|
||
|
||
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
|
||
|
||
/* ====== SINGLE LABEL (standalone) ====== */
|
||
.single-label-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 1rem 0;
|
||
}
|
||
.single-label-feed {
|
||
/* Outer box matches the physical feed size (3.7cm × 2.5cm landscape) */
|
||
width: <?= $single_feed_w_cm ?>cm;
|
||
height: <?= $single_feed_h_cm ?>cm;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
border: 1px dashed #999;
|
||
position: relative;
|
||
}
|
||
.single-label {
|
||
/* Inner content sized as portrait (2.5cm × 3.7cm), then rotated to fit the landscape feed */
|
||
width: <?= $single_label_w_cm ?>cm;
|
||
height: <?= $single_label_h_cm ?>cm;
|
||
box-sizing: border-box;
|
||
padding: 0.06cm;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
font-size: 0.24cm;
|
||
|
||
/* Rotate 90° clockwise and re-center within the feed box */
|
||
transform: rotate(90deg);
|
||
transform-origin: center center;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
margin-top: calc(-<?= $single_label_h_cm ?>cm / 2);
|
||
margin-left: calc(-<?= $single_label_w_cm ?>cm / 2);
|
||
}
|
||
.single-label .label-text {
|
||
display: grid;
|
||
gap: 0.04cm;
|
||
text-align: center;
|
||
margin: 0;
|
||
line-height: 1.2;
|
||
font-size: inherit;
|
||
word-break: break-word;
|
||
}
|
||
.single-label .barcode-block {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: flex-end;
|
||
margin-top: 0.02cm;
|
||
}
|
||
.single-label .barcode-block svg {
|
||
display: block;
|
||
height: 0.8cm;
|
||
width: auto;
|
||
}
|
||
|
||
@media print {
|
||
.pane, .controls, .error, .info, .subtle, h1, .wrap { display: none !important; }
|
||
body { margin: 0; padding: 0; background: white; }
|
||
.sheet { border: none; }
|
||
|
||
/* Single label: fill the page exactly */
|
||
.single-label-wrap { display: flex !important; padding: 0; margin: 0; justify-content: start; }
|
||
.single-label-feed {
|
||
width: <?= $single_feed_w_cm ?>cm;
|
||
height: <?= $single_feed_h_cm ?>cm;
|
||
border: none;
|
||
margin: 0;
|
||
}
|
||
|
||
/* Sheet mode: show sheets */
|
||
.sheets { display: grid !important; }
|
||
}
|
||
</style>
|
||
<script>
|
||
function goPrint(){ window.print(); }
|
||
function jump(id){ document.getElementById(id)?.scrollIntoView({behavior:'smooth'}); }
|
||
</script>
|
||
</head>
|
||
<body>
|
||
|
||
<h1>Label Generator</h1>
|
||
<p class="subtle">Import CSV and print to a **1.5" × 1.0"** template (Demco) at <strong>100% / Actual size</strong>.</p>
|
||
|
||
<div class="wrap">
|
||
|
||
<!-- Single label form -->
|
||
<div class="pane">
|
||
<h2>Single Label</h2>
|
||
<form method="post">
|
||
<input type="hidden" name="mode" value="single">
|
||
<!-- Preserve template params across submissions -->
|
||
<?php foreach ([
|
||
'page_width_in','page_height_in','margin_top_in','margin_right_in','margin_bottom_in','margin_left_in',
|
||
'cols','rows','label_w_in','label_h_in','gap_h_in','gap_v_in','barcode_h_in','font_in'
|
||
] as $p): ?>
|
||
<input type="hidden" name="<?= $p ?>" value="<?= safe($_POST[$p] ?? $defaults[$p] ?? '') ?>">
|
||
<?php endforeach; ?>
|
||
<?php if ($show_outlines === '1'): ?><input type="hidden" name="show_outlines" value="1"><?php endif; ?>
|
||
|
||
<div class="grid2">
|
||
<div>
|
||
<label for="data1">Data 1</label>
|
||
<input type="text" id="data1" name="data1" value="<?= safe($data1) ?>" placeholder="e.g., QA76.73.P224">
|
||
</div>
|
||
<div>
|
||
<label for="data2">Data 2</label>
|
||
<input type="text" id="data2" name="data2" value="<?= safe($data2) ?>" placeholder="e.g., 2025">
|
||
</div>
|
||
<div>
|
||
<label for="data3">Data 3</label>
|
||
<input type="text" id="data3" name="data3" value="<?= safe($data3) ?>" placeholder="e.g., v.2">
|
||
</div>
|
||
<div>
|
||
<label for="data4">Data 4</label>
|
||
<input type="text" id="data4" name="data4" value="<?= safe($data4) ?>" placeholder="e.g., Copy 1">
|
||
</div>
|
||
<div style="grid-column: 1 / -1;">
|
||
<label for="barcode">Barcode (alphanumeric)</label>
|
||
<input type="text" id="barcode" name="barcode" value="<?= safe($barcodeText) ?>" placeholder="e.g., 1230004231819">
|
||
</div>
|
||
</div>
|
||
<div class="actions">
|
||
<button type="submit">Preview Single Label</button>
|
||
<button type="button" class="ghost" onclick="jump('csvpane')">Or Import CSV…</button>
|
||
</div>
|
||
<p class="info">Data Matrix prints as SVG at <strong><?= number_format($barcode_h_in, 2) ?> in</strong> height.</p>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- CSV import -->
|
||
<div class="pane" id="csvpane">
|
||
<h2>Import CSV (data1,data2,data3,data4,barcode)</h2>
|
||
<form method="post" enctype="multipart/form-data">
|
||
<input type="hidden" name="mode" value="csv">
|
||
<!-- persist template params -->
|
||
<div class="grid2">
|
||
<div style="grid-column: 1 / -1;">
|
||
<label for="csvfile">Choose CSV file</label>
|
||
<input type="file" id="csvfile" name="csvfile" accept=".csv,text/csv">
|
||
</div>
|
||
</div>
|
||
<?php foreach ([
|
||
'page_width_in','page_height_in','margin_top_in','margin_right_in','margin_bottom_in','margin_left_in',
|
||
'cols','rows','label_w_in','label_h_in','gap_h_in','gap_v_in','barcode_h_in','font_in'
|
||
] as $p): ?>
|
||
<input type="hidden" name="<?= $p ?>" value="<?= safe($_POST[$p] ?? $defaults[$p] ?? '') ?>">
|
||
<?php endforeach; ?>
|
||
<?php if ($show_outlines === '1'): ?><input type="hidden" name="show_outlines" value="1"><?php endif; ?>
|
||
|
||
<div class="actions">
|
||
<button type="submit">Upload & Preview Labels</button>
|
||
<button type="reset" class="ghost">Reset</button>
|
||
</div>
|
||
<p class="info">Delimiters (comma, tab, semicolon) auto-detected. Headers must match exactly.</p>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Template settings -->
|
||
<div class="pane">
|
||
<h2>Template (Demco 1.5" × 1.0")</h2>
|
||
<form method="post">
|
||
<div class="grid2">
|
||
<div><label>Page width (in)</label><input type="number" step="0.01" name="page_width_in" value="<?= safe($page_width_in) ?>"></div>
|
||
<div><label>Page height (in)</label><input type="number" step="0.01" name="page_height_in" value="<?= safe($page_height_in) ?>"></div>
|
||
|
||
<div><label>Margin top (in)</label><input type="number" step="0.01" name="margin_top_in" value="<?= safe($margin_top_in) ?>"></div>
|
||
<div><label>Margin right (in)</label><input type="number" step="0.01" name="margin_right_in" value="<?= safe($margin_right_in) ?>"></div>
|
||
<div><label>Margin bottom (in)</label><input type="number" step="0.01" name="margin_bottom_in" value="<?= safe($margin_bottom_in) ?>"></div>
|
||
<div><label>Margin left (in)</label><input type="number" step="0.01" name="margin_left_in" value="<?= safe($margin_left_in) ?>"></div>
|
||
|
||
<div><label>Columns</label><input type="number" step="1" min="1" name="cols" value="<?= safe($cols) ?>"></div>
|
||
<div><label>Rows</label><input type="number" step="1" min="1" name="rows" value="<?= safe($rows) ?>"></div>
|
||
|
||
<div><label>Label width (in)</label><input type="number" step="0.01" name="label_w_in" value="<?= safe($label_w_in) ?>"></div>
|
||
<div><label>Label height (in)</label><input type="number" step="0.01" name="label_h_in" value="<?= safe($label_h_in) ?>"></div>
|
||
|
||
<div><label>Horizontal gap (in)</label><input type="number" step="0.01" name="gap_h_in" value="<?= safe($gap_h_in) ?>"></div>
|
||
<div><label>Vertical gap (in)</label><input type="number" step="0.01" name="gap_v_in" value="<?= safe($gap_v_in) ?>"></div>
|
||
|
||
<div><label>Barcode height (in)</label><input type="number" step="0.01" name="barcode_h_in" value="<?= safe($barcode_h_in) ?>"></div>
|
||
<div><label>Font size (in)</label><input type="number" step="0.01" name="font_in" value="<?= safe($font_in) ?>"></div>
|
||
|
||
<div style="grid-column: 1 / -1; display:flex; align-items:center; gap:.5rem;">
|
||
<input type="checkbox" id="show_outlines" name="show_outlines" value="1" <?= $show_outlines==='1'?'checked':'' ?>>
|
||
<label for="show_outlines">Show label outlines (for alignment/test prints)</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Keep any uploaded/entered data visible on re-submit by passing the last single entry (optional) -->
|
||
<input type="hidden" name="mode" value="<?= safe($mode ?: 'single') ?>">
|
||
<input type="hidden" name="data1" value="<?= safe($data1) ?>">
|
||
<input type="hidden" name="data2" value="<?= safe($data2) ?>">
|
||
<input type="hidden" name="data3" value="<?= safe($data3) ?>">
|
||
<input type="hidden" name="data4" value="<?= safe($data4) ?>">
|
||
<input type="hidden" name="barcode" value="<?= safe($barcodeText) ?>">
|
||
|
||
<div class="actions">
|
||
<button type="submit">Apply Template Settings</button>
|
||
<button type="button" class="ghost" onclick="goPrint()">Print</button>
|
||
</div>
|
||
<p class="info">
|
||
<strong>Tip:</strong> Do a test print on plain paper with “Show label outlines” checked.
|
||
Hold it behind a real sheet to check alignment, then tweak margins/gaps as needed.
|
||
</p>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Errors -->
|
||
<?php if (!empty($errors)): ?>
|
||
<div class="error">
|
||
<strong>Issues:</strong>
|
||
<ul><?php foreach ($errors as $e): ?><li><?= $e ?></li><?php endforeach; ?></ul>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Preview / Sheets (CSV mode) -->
|
||
<?php if ($records && !$errors && !$is_single): ?>
|
||
<div class="controls">
|
||
<button onclick="goPrint()">Print</button>
|
||
<span class="info"><?= count($records) ?> label(s), <?= count($pages) ?> sheet(s) @ <?= $cols ?>×<?= $rows ?>.</span>
|
||
</div>
|
||
|
||
<div class="sheets">
|
||
<?php foreach ($pages as $pageIndex => $pageRecs): ?>
|
||
<div class="sheet">
|
||
<div class="grid">
|
||
<?php
|
||
// Fill cells row-major: put records first, blank cells if fewer than capacity.
|
||
$count = count($pageRecs);
|
||
$capacity = $cols * $rows;
|
||
for ($i = 0; $i < $capacity; $i++):
|
||
$rec = $pageRecs[$i] ?? null;
|
||
?>
|
||
<div class="cell">
|
||
<?php if ($rec): ?>
|
||
<div class="label-text">
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data1'] ?? '')) ?></div>
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data2'] ?? '')) ?></div>
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data3'] ?? '')) ?></div>
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data4'] ?? '')) ?></div>
|
||
</div>
|
||
<div class="barcode-block">
|
||
<?= render_datamatrix_svg((string)($rec['barcode'] ?? '')) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endfor; ?>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Single label preview -->
|
||
<?php if ($is_single && !$errors):
|
||
$rec = $records[0];
|
||
?>
|
||
<div class="controls">
|
||
<button onclick="goPrint()">Print Label</button>
|
||
<span class="info">Single label — <?= $single_label_w_cm ?>cm × <?= $single_label_h_cm ?>cm</span>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$autoloadOk): ?>
|
||
<div class="error">
|
||
Composer autoload not found. Install the barcode library with:<br>
|
||
<code>composer require tecnickcom/tc-lib-barcode</code><br>
|
||
(Make sure BCMath is enabled.)
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
</div>
|
||
|
||
<?php if ($is_single && !$errors):
|
||
$rec = $records[0];
|
||
?>
|
||
<div class="single-label-wrap">
|
||
<div class="single-label-feed">
|
||
<div class="single-label">
|
||
<div class="label-text">
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data1'] ?? '')) ?></div>
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data2'] ?? '')) ?></div>
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data3'] ?? '')) ?></div>
|
||
<div class="label-text-line"><?= nl2br(safe($rec['data4'] ?? '')) ?></div>
|
||
</div>
|
||
<div class="barcode-block">
|
||
<?= render_datamatrix_svg((string)($rec['barcode'] ?? '')) ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
</body>
|
||
</html>
|