527 lines
15 KiB
PHP
527 lines
15 KiB
PHP
<?php
|
||
/**
|
||
* MSIN-4485 — Calibrage GUI dossard : N boîtes glissables, champ + alignement.
|
||
*/
|
||
$strLangue = 'fr';
|
||
require_once __DIR__ . '/php/inc_start_time.php';
|
||
require_once __DIR__ . '/php/inc_functions.php';
|
||
require_once dirname(__DIR__) . '/php/inc_fx_dossard_print.php';
|
||
|
||
if (empty($_SESSION['usa_id'])) {
|
||
header('Location: login.php');
|
||
exit;
|
||
}
|
||
|
||
$intEveId = isset($_GET['eve_id']) ? intval($_GET['eve_id']) : 0;
|
||
$arrLayout = fxDossardPrintLoadLayout($intEveId);
|
||
$arrCatalog = fxDossardPrintFieldCatalog();
|
||
$strTplWeb = '../assets/dossard_templates/default.jpg';
|
||
if ($intEveId > 0) {
|
||
foreach (array('jpg', 'jpeg', 'png') as $strExt) {
|
||
$strCand = dirname(__DIR__) . '/assets/dossard_templates/eve_' . $intEveId . '.' . $strExt;
|
||
if (is_file($strCand)) {
|
||
$strTplWeb = '../assets/dossard_templates/eve_' . $intEveId . '.' . $strExt;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
$strLayoutJson = json_encode($arrLayout, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||
$strCatalogJson = json_encode($arrCatalog, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>MSIN-4485 — Calibrage dossard</title>
|
||
<style>
|
||
:root {
|
||
--bg: #1a1d23;
|
||
--panel: #252a33;
|
||
--text: #e8eaed;
|
||
--muted: #9aa0a6;
|
||
--ok: #43a047;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0;
|
||
font-family: Segoe UI, system-ui, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
}
|
||
header {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background: var(--panel);
|
||
border-bottom: 1px solid #333;
|
||
}
|
||
header h1 { margin: 0; font-size: 1.05rem; font-weight: 600; }
|
||
header p { margin: 4px 0 0; color: var(--muted); font-size: 0.85rem; }
|
||
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||
button {
|
||
appearance: none;
|
||
border: 0;
|
||
border-radius: 6px;
|
||
padding: 8px 14px;
|
||
font-size: 0.9rem;
|
||
cursor: pointer;
|
||
color: #fff;
|
||
background: #455a64;
|
||
}
|
||
button.primary { background: var(--ok); font-weight: 600; }
|
||
button.danger { background: #c62828; }
|
||
button:disabled { opacity: 0.5; cursor: wait; }
|
||
#status { font-size: 0.85rem; color: var(--muted); min-width: 10rem; }
|
||
#status.ok { color: #81c784; }
|
||
#status.err { color: #ef9a9a; }
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: minmax(240px, 300px) 1fr;
|
||
gap: 16px;
|
||
padding: 16px;
|
||
align-items: start;
|
||
}
|
||
@media (max-width: 900px) {
|
||
.layout { grid-template-columns: 1fr; }
|
||
}
|
||
.sidebar {
|
||
background: var(--panel);
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
}
|
||
.sidebar h2 {
|
||
margin: 0 0 10px;
|
||
font-size: 0.9rem;
|
||
color: var(--muted);
|
||
font-weight: 600;
|
||
}
|
||
.box-row {
|
||
border: 1px solid #3a414d;
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.box-row.active { border-color: #90caf9; }
|
||
.box-row .row-top {
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: center;
|
||
margin-bottom: 6px;
|
||
}
|
||
.swatch {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 2px;
|
||
flex: 0 0 auto;
|
||
border: 1px solid rgba(255,255,255,.4);
|
||
}
|
||
.box-row select, .box-row input {
|
||
width: 100%;
|
||
background: #1a1d23;
|
||
color: var(--text);
|
||
border: 1px solid #444;
|
||
border-radius: 4px;
|
||
padding: 4px 6px;
|
||
font-size: 0.8rem;
|
||
}
|
||
.box-row .grid2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 6px;
|
||
}
|
||
.box-row label {
|
||
display: block;
|
||
font-size: 0.7rem;
|
||
color: var(--muted);
|
||
margin-bottom: 2px;
|
||
}
|
||
.stage-wrap { min-width: 0; }
|
||
#stage {
|
||
position: relative;
|
||
width: 100%;
|
||
aspect-ratio: 8.5 / 5.5;
|
||
background: #000 center / 100% 100% no-repeat;
|
||
border: 1px solid #444;
|
||
overflow: hidden;
|
||
user-select: none;
|
||
touch-action: none;
|
||
}
|
||
.box {
|
||
position: absolute;
|
||
border: 2px solid;
|
||
background: rgba(255,255,255,.12);
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: move;
|
||
min-width: 40px;
|
||
min-height: 24px;
|
||
}
|
||
.box.selected { box-shadow: 0 0 0 2px rgba(255,255,255,.35); }
|
||
.box .label {
|
||
font-weight: 800;
|
||
pointer-events: none;
|
||
text-shadow: 0 0 4px #fff, 0 0 2px #fff;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
max-width: 95%;
|
||
font-size: clamp(12px, 4vh, 48px);
|
||
}
|
||
.box.align-C { justify-content: center; }
|
||
.box.align-L { justify-content: flex-start; padding-left: 2px; }
|
||
.box.align-R { justify-content: flex-end; padding-right: 2px; }
|
||
.handle {
|
||
position: absolute;
|
||
width: 12px;
|
||
height: 12px;
|
||
background: #fff;
|
||
border: 2px solid currentColor;
|
||
right: -6px;
|
||
bottom: -6px;
|
||
cursor: nwse-resize;
|
||
}
|
||
.pct {
|
||
margin-top: 10px;
|
||
font-family: ui-monospace, Consolas, monospace;
|
||
font-size: 0.7rem;
|
||
color: var(--muted);
|
||
white-space: pre-wrap;
|
||
background: var(--panel);
|
||
padding: 10px 12px;
|
||
border-radius: 6px;
|
||
max-height: 180px;
|
||
overflow: auto;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div>
|
||
<h1>Calibrage dossard — boîtes + champs</h1>
|
||
<p>Glisse / resize · champ + alignement · ajoute des boîtes (nom, âge…) pour plus tard</p>
|
||
</div>
|
||
<div class="actions">
|
||
<button type="button" id="btnAdd">+ Boîte</button>
|
||
<button type="button" id="btnReset">Réinitialiser</button>
|
||
<button type="button" class="primary" id="btnSave">Enregistrer</button>
|
||
<span id="status"></span>
|
||
</div>
|
||
</header>
|
||
<div class="layout">
|
||
<aside class="sidebar">
|
||
<h2>Boîtes</h2>
|
||
<div id="boxList"></div>
|
||
</aside>
|
||
<div class="stage-wrap">
|
||
<div id="stage" style="background-image:url('<?php echo htmlspecialchars($strTplWeb, ENT_QUOTES, 'UTF-8'); ?>');"></div>
|
||
<div class="pct" id="pctOut"></div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
var eveId = <?php echo (int)$intEveId; ?>;
|
||
var catalog = <?php echo $strCatalogJson; ?>;
|
||
var colors = ['#e53935', '#1e88e5', '#43a047', '#fb8c00', '#8e24aa', '#00897b', '#f4511e'];
|
||
var initial = <?php echo $strLayoutJson; ?>;
|
||
var layout = JSON.parse(JSON.stringify(initial));
|
||
var stage = document.getElementById('stage');
|
||
var boxList = document.getElementById('boxList');
|
||
var statusEl = document.getElementById('status');
|
||
var pctOut = document.getElementById('pctOut');
|
||
var selectedId = null;
|
||
var drag = null;
|
||
var uid = 0;
|
||
|
||
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
|
||
|
||
function ensureIds() {
|
||
layout.boxes.forEach(function (b, i) {
|
||
if (!b.id) b.id = 'box_' + (++uid);
|
||
});
|
||
}
|
||
|
||
function colorFor(i) { return colors[i % colors.length]; }
|
||
|
||
function sampleFor(field) {
|
||
return (catalog[field] && catalog[field].sample) ? catalog[field].sample : field;
|
||
}
|
||
|
||
function defaultFont(field) {
|
||
return (catalog[field] && catalog[field].font_max) ? catalog[field].font_max : 24;
|
||
}
|
||
|
||
function renderList() {
|
||
boxList.innerHTML = '';
|
||
layout.boxes.forEach(function (b, i) {
|
||
var row = document.createElement('div');
|
||
row.className = 'box-row' + (b.id === selectedId ? ' active' : '');
|
||
row.dataset.id = b.id;
|
||
|
||
var top = document.createElement('div');
|
||
top.className = 'row-top';
|
||
var sw = document.createElement('span');
|
||
sw.className = 'swatch';
|
||
sw.style.background = colorFor(i);
|
||
top.appendChild(sw);
|
||
var title = document.createElement('strong');
|
||
title.style.fontSize = '0.8rem';
|
||
title.style.flex = '1';
|
||
title.textContent = (catalog[b.field] && catalog[b.field].label) ? catalog[b.field].label : b.field;
|
||
top.appendChild(title);
|
||
var btnDel = document.createElement('button');
|
||
btnDel.type = 'button';
|
||
btnDel.className = 'danger';
|
||
btnDel.style.padding = '2px 8px';
|
||
btnDel.textContent = '×';
|
||
btnDel.title = 'Supprimer';
|
||
btnDel.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
if (layout.boxes.length <= 1) {
|
||
statusEl.textContent = 'Garde au moins une boîte.';
|
||
statusEl.className = 'err';
|
||
return;
|
||
}
|
||
layout.boxes = layout.boxes.filter(function (x) { return x.id !== b.id; });
|
||
if (selectedId === b.id) selectedId = layout.boxes[0].id;
|
||
renderAll();
|
||
});
|
||
top.appendChild(btnDel);
|
||
row.appendChild(top);
|
||
|
||
var g = document.createElement('div');
|
||
g.className = 'grid2';
|
||
|
||
var lf = document.createElement('div');
|
||
var labF = document.createElement('label');
|
||
labF.textContent = 'Champ';
|
||
var selF = document.createElement('select');
|
||
Object.keys(catalog).forEach(function (k) {
|
||
var o = document.createElement('option');
|
||
o.value = k;
|
||
o.textContent = catalog[k].label;
|
||
if (k === b.field) o.selected = true;
|
||
selF.appendChild(o);
|
||
});
|
||
selF.addEventListener('change', function () {
|
||
b.field = selF.value;
|
||
b.font_max = defaultFont(b.field);
|
||
renderAll();
|
||
});
|
||
lf.appendChild(labF);
|
||
lf.appendChild(selF);
|
||
g.appendChild(lf);
|
||
|
||
var la = document.createElement('div');
|
||
var labA = document.createElement('label');
|
||
labA.textContent = 'Alignement';
|
||
var selA = document.createElement('select');
|
||
[['C', 'Centre'], ['L', 'Gauche'], ['R', 'Droite']].forEach(function (pair) {
|
||
var o = document.createElement('option');
|
||
o.value = pair[0];
|
||
o.textContent = pair[1];
|
||
if (pair[0] === b.align) o.selected = true;
|
||
selA.appendChild(o);
|
||
});
|
||
selA.addEventListener('change', function () {
|
||
b.align = selA.value;
|
||
renderAll();
|
||
});
|
||
la.appendChild(labA);
|
||
la.appendChild(selA);
|
||
g.appendChild(la);
|
||
row.appendChild(g);
|
||
|
||
row.addEventListener('click', function () {
|
||
selectedId = b.id;
|
||
renderAll();
|
||
});
|
||
boxList.appendChild(row);
|
||
});
|
||
}
|
||
|
||
function renderStage() {
|
||
stage.innerHTML = '';
|
||
layout.boxes.forEach(function (b, i) {
|
||
var el = document.createElement('div');
|
||
el.className = 'box align-' + (b.align || 'C') + (b.id === selectedId ? ' selected' : '');
|
||
el.dataset.id = b.id;
|
||
el.style.borderColor = colorFor(i);
|
||
el.style.color = colorFor(i);
|
||
el.style.left = (b.left * 100) + '%';
|
||
el.style.top = (b.top * 100) + '%';
|
||
el.style.width = ((b.right - b.left) * 100) + '%';
|
||
el.style.height = ((b.bottom - b.top) * 100) + '%';
|
||
if (b.field === 'no_bib') {
|
||
el.querySelector && null;
|
||
el.style.fontSize = '';
|
||
}
|
||
var lab = document.createElement('span');
|
||
lab.className = 'label';
|
||
lab.textContent = sampleFor(b.field);
|
||
if (b.field === 'no_bib') lab.style.fontSize = 'clamp(18px, 8vh, 72px)';
|
||
el.appendChild(lab);
|
||
var h = document.createElement('div');
|
||
h.className = 'handle';
|
||
h.setAttribute('data-resize', '1');
|
||
el.appendChild(h);
|
||
stage.appendChild(el);
|
||
});
|
||
pctOut.textContent = JSON.stringify(layout, null, 2);
|
||
}
|
||
|
||
function renderAll() {
|
||
ensureIds();
|
||
renderList();
|
||
renderStage();
|
||
}
|
||
|
||
function findBox(id) {
|
||
for (var i = 0; i < layout.boxes.length; i++) {
|
||
if (layout.boxes[i].id === id) return layout.boxes[i];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function readFromDom(id) {
|
||
var el = stage.querySelector('.box[data-id="' + id + '"]');
|
||
if (!el) return null;
|
||
var r = stage.getBoundingClientRect();
|
||
var br = el.getBoundingClientRect();
|
||
return {
|
||
left: clamp((br.left - r.left) / r.width, 0, 1),
|
||
top: clamp((br.top - r.top) / r.height, 0, 1),
|
||
right: clamp((br.right - r.left) / r.width, 0, 1),
|
||
bottom: clamp((br.bottom - r.top) / r.height, 0, 1)
|
||
};
|
||
}
|
||
|
||
stage.addEventListener('pointerdown', function (e) {
|
||
var el = e.target.closest('.box');
|
||
if (!el || !stage.contains(el)) return;
|
||
e.preventDefault();
|
||
var id = el.getAttribute('data-id');
|
||
selectedId = id;
|
||
var resize = !!e.target.closest('[data-resize]');
|
||
var r = stage.getBoundingClientRect();
|
||
var br = el.getBoundingClientRect();
|
||
drag = {
|
||
id: id,
|
||
resize: resize,
|
||
startX: e.clientX,
|
||
startY: e.clientY,
|
||
orig: {
|
||
left: (br.left - r.left) / r.width,
|
||
top: (br.top - r.top) / r.height,
|
||
right: (br.right - r.left) / r.width,
|
||
bottom: (br.bottom - r.top) / r.height
|
||
}
|
||
};
|
||
renderAll();
|
||
el.setPointerCapture && el.setPointerCapture(e.pointerId);
|
||
});
|
||
|
||
window.addEventListener('pointermove', function (e) {
|
||
if (!drag) return;
|
||
var b = findBox(drag.id);
|
||
if (!b) return;
|
||
var r = stage.getBoundingClientRect();
|
||
var dx = (e.clientX - drag.startX) / r.width;
|
||
var dy = (e.clientY - drag.startY) / r.height;
|
||
var o = drag.orig;
|
||
if (drag.resize) {
|
||
b.left = o.left;
|
||
b.top = o.top;
|
||
b.right = clamp(o.right + dx, o.left + 0.04, 1);
|
||
b.bottom = clamp(o.bottom + dy, o.top + 0.03, 1);
|
||
} else {
|
||
var w = o.right - o.left;
|
||
var h = o.bottom - o.top;
|
||
b.left = clamp(o.left + dx, 0, 1 - w);
|
||
b.top = clamp(o.top + dy, 0, 1 - h);
|
||
b.right = b.left + w;
|
||
b.bottom = b.top + h;
|
||
}
|
||
renderStage();
|
||
});
|
||
|
||
window.addEventListener('pointerup', function () {
|
||
if (!drag) return;
|
||
var b = findBox(drag.id);
|
||
var edges = readFromDom(drag.id);
|
||
if (b && edges) {
|
||
b.left = edges.left;
|
||
b.top = edges.top;
|
||
b.right = edges.right;
|
||
b.bottom = edges.bottom;
|
||
}
|
||
drag = null;
|
||
renderAll();
|
||
});
|
||
|
||
document.getElementById('btnAdd').addEventListener('click', function () {
|
||
uid++;
|
||
layout.boxes.push({
|
||
id: 'box_' + Date.now(),
|
||
field: 'par_nom',
|
||
align: 'L',
|
||
font_max: defaultFont('par_nom'),
|
||
left: 0.35,
|
||
right: 0.65,
|
||
top: 0.55,
|
||
bottom: 0.65
|
||
});
|
||
selectedId = layout.boxes[layout.boxes.length - 1].id;
|
||
renderAll();
|
||
});
|
||
|
||
document.getElementById('btnReset').addEventListener('click', function () {
|
||
layout = JSON.parse(JSON.stringify(initial));
|
||
selectedId = layout.boxes[0] ? layout.boxes[0].id : null;
|
||
renderAll();
|
||
statusEl.textContent = 'Réinitialisé (pas encore sauvé).';
|
||
statusEl.className = '';
|
||
});
|
||
|
||
document.getElementById('btnSave').addEventListener('click', function () {
|
||
var btn = document.getElementById('btnSave');
|
||
btn.disabled = true;
|
||
statusEl.textContent = 'Enregistrement…';
|
||
statusEl.className = '';
|
||
fetch('ajax_dossard_layout.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
eve_id: eveId,
|
||
version: 2,
|
||
boxes: layout.boxes
|
||
})
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (data && data.ok) {
|
||
initial = JSON.parse(JSON.stringify(layout));
|
||
statusEl.textContent = 'Enregistré — PDF = ces boîtes.';
|
||
statusEl.className = 'ok';
|
||
} else {
|
||
statusEl.textContent = (data && data.error) ? data.error : 'Erreur sauvegarde';
|
||
statusEl.className = 'err';
|
||
}
|
||
})
|
||
.catch(function () {
|
||
statusEl.textContent = 'Erreur réseau';
|
||
statusEl.className = 'err';
|
||
})
|
||
.finally(function () { btn.disabled = false; });
|
||
});
|
||
|
||
selectedId = layout.boxes[0] ? layout.boxes[0].id : null;
|
||
renderAll();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|