File: /homepages/35/d993672390/htdocs/dermiteseborrheique/app/items.php
<?php
define('DS_APP', true);
require_once dirname(__DIR__) . '/includes/init.php';
$user = auth_require();
$search = sanitize_string($_GET['q'] ?? '', 100);
$catFilter = $_GET['cat'] ?? 'all';
$tab = in_array($_GET['tab'] ?? '', ['mine','scanned','all']) ? $_GET['tab'] : 'mine';
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 24;
$offset = ($page - 1) * $perPage;
// ── Requêtes selon onglet ─────────────────────────────────────────────────────
$params = [];
$whereExtra = '';
if ($search) {
$whereExtra .= " AND MATCH(i.name) AGAINST(? IN BOOLEAN MODE)";
$params[] = $search . '*';
}
if ($catFilter !== 'all') {
$whereExtra .= " AND i.category = ?";
$params[] = $catFilter;
}
// Pour 'mine' : jointure avec user_items
$tabConditions = [
'mine' => "EXISTS (SELECT 1 FROM user_items ui WHERE ui.item_id = i.id AND ui.user_id = {$user['id']})",
'scanned' => "i.source IN ('off','obf') AND i.barcode IS NOT NULL",
'all' => "(i.barcode IS NOT NULL OR i.source = 'manual')",
];
$whereSQL = "WHERE ({$tabConditions[$tab]}) {$whereExtra}";
$total = db_count("SELECT COUNT(*) FROM items i {$whereSQL}", $params);
$items = db_fetch_all(
"SELECT i.*,
COUNT(ii.id) as ingredient_count,
SUM(ii.is_suspect) as suspect_count
FROM items i
LEFT JOIN item_ingredients ii ON i.id = ii.item_id
{$whereSQL}
GROUP BY i.id
ORDER BY
CASE WHEN i.user_id = {$user['id']} THEN 0 ELSE 1 END,
i.usage_count DESC,
i.created_at DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$totalPages = (int)ceil($total / $perPage);
// Compteurs onglets
$countMine = db_count("SELECT COUNT(*) FROM user_items WHERE user_id = ?", [$user['id']]);
$countScanned = db_count("SELECT COUNT(*) FROM items WHERE source IN ('off','obf') AND barcode IS NOT NULL");
$countAll = db_count("SELECT COUNT(*) FROM items WHERE barcode IS NOT NULL");
$catMeta = [
'food' => ['icon' => '🍽️', 'label' => 'Aliment'],
'drink' => ['icon' => '🥤', 'label' => 'Boisson'],
'alcohol' => ['icon' => '🍷', 'label' => 'Alcool'],
'cosmetic' => ['icon' => '🧴', 'label' => 'Cosmétique'],
'haircare' => ['icon' => '🧴', 'label' => 'Capillaire'],
'medication' => ['icon' => '💊', 'label' => 'Médicament'],
'supplement' => ['icon' => '💊', 'label' => 'Complément'],
'tobacco' => ['icon' => '🚬', 'label' => 'Tabac'],
'other' => ['icon' => '📦', 'label' => 'Autre'],
];
$pageTitle = 'Produits & Ingrédients';
$activePage = 'items';
$extraHead = '<script src="https://cdnjs.cloudflare.com/ajax/libs/tesseract.js/4.1.1/tesseract.min.js"></script>';
ob_start();
?>
<style>
/* Onglets */
.items-tabs{display:flex;gap:0;background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:20px;width:fit-content;}
.items-tab{padding:9px 18px;font-size:0.82rem;font-weight:600;color:var(--text-mid);text-decoration:none;border-right:1px solid var(--border);transition:all 0.2s;display:flex;align-items:center;gap:6px;}
.items-tab:last-child{border-right:none;}
.items-tab:hover{background:var(--surface2);color:var(--text);}
.items-tab.active{background:var(--cyan-dim);color:var(--cyan);}
.items-tab-count{font-size:0.68rem;background:var(--surface2);color:var(--text-dim);padding:1px 6px;border-radius:100px;}
.items-tab.active .items-tab-count{background:rgba(0,212,200,0.15);color:var(--cyan);}
/* Toolbar */
.items-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px;flex-wrap:wrap;}
.items-search-wrap{display:flex;gap:8px;flex:1;max-width:360px;}
.items-search{flex:1;background:var(--surface2);border:1px solid var(--border-bright);border-radius:9px;padding:8px 12px;color:var(--text);font-size:0.83rem;font-family:inherit;}
.items-search:focus{outline:none;border-color:rgba(0,212,200,0.4);}
.items-search-btn{background:var(--surface2);border:1px solid var(--border);border-radius:9px;padding:8px 12px;cursor:pointer;font-size:0.85rem;transition:all 0.2s;}
.items-search-btn:hover{border-color:var(--cyan);}
.cat-filters{display:flex;gap:5px;flex-wrap:wrap;}
.cat-filter{background:var(--surface);border:1px solid var(--border);color:var(--text-mid);padding:4px 10px;border-radius:100px;font-size:0.75rem;font-weight:600;text-decoration:none;transition:all 0.2s;}
.cat-filter:hover,.cat-filter.active{background:var(--cyan-dim);border-color:rgba(0,212,200,0.3);color:var(--cyan);}
/* Grille */
.items-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;}
.item-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;transition:all 0.2s;}
.item-card:hover{border-color:rgba(0,212,200,0.2);box-shadow:0 4px 16px rgba(0,0,0,0.12);}
.item-card-top{display:flex;gap:10px;padding:12px;align-items:flex-start;}
.item-img{width:48px;height:48px;border-radius:8px;background:var(--surface2);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:1.2rem;overflow:hidden;}
.item-img img{width:100%;height:100%;object-fit:cover;}
.item-info{flex:1;min-width:0;}
.item-name{font-size:0.85rem;font-weight:700;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.item-brand{font-size:0.72rem;color:var(--text-mid);margin-top:1px;}
.item-badges{display:flex;gap:4px;margin-top:6px;flex-wrap:wrap;}
.ibadge{font-size:0.62rem;font-weight:700;padding:1px 6px;border-radius:100px;}
.ibadge-cat{background:var(--cyan-dim);color:var(--cyan);}
.ibadge-mine{background:var(--emerald-dim);color:var(--emerald);}
.ibadge-suspect{background:rgba(255,107,138,0.1);color:var(--rose);}
.ibadge-nodata{background:rgba(255,255,255,0.05);color:var(--text-dim);}
/* Ingrédients */
.item-ing-section{border-top:1px solid var(--border);padding:8px 12px;}
.item-ing-btn{background:none;border:none;color:var(--text-dim);font-size:0.75rem;cursor:pointer;font-family:inherit;padding:0;display:flex;align-items:center;gap:5px;width:100%;transition:color 0.2s;}
.item-ing-btn:hover{color:var(--cyan);}
.item-ing-tags{display:none;flex-wrap:wrap;gap:3px;margin-top:8px;max-height:100px;overflow-y:auto;}
.item-ing-tags.open{display:flex;}
.ing-tag{font-size:0.65rem;padding:2px 6px;border-radius:100px;background:var(--surface2);border:1px solid var(--border);color:var(--text-dim);cursor:default;transition:all 0.2s;}
.ing-tag:hover{border-color:var(--border-bright);color:var(--text-mid);}
.ing-tag.suspect{background:rgba(255,107,138,0.08);border-color:rgba(255,107,138,0.25);color:var(--rose);font-weight:600;}
/* Ajouter ingrédients */
.item-add-ing{display:flex;gap:6px;margin-top:8px;}
.item-add-ing-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:5px 9px;color:var(--text);font-size:0.75rem;font-family:inherit;}
.item-add-ing-input:focus{outline:none;border-color:rgba(0,212,200,0.4);}
.item-add-ing-btn{background:var(--cyan);color:var(--bg);border:none;border-radius:7px;padding:5px 10px;cursor:pointer;font-size:0.75rem;font-weight:700;font-family:inherit;}
/* Empty */
.items-empty{text-align:center;padding:60px 20px;color:var(--text-dim);}
/* Pagination */
.pagination{display:flex;gap:6px;justify-content:center;margin-top:20px;}
.page-btn{background:var(--surface);border:1px solid var(--border);color:var(--text-mid);padding:6px 12px;border-radius:7px;text-decoration:none;font-size:0.8rem;font-weight:600;transition:all 0.2s;}
.page-btn:hover,.page-btn.active{background:var(--cyan-dim);border-color:rgba(0,212,200,0.3);color:var(--cyan);}
</style>
<!-- Onglets -->
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:16px;">
<div class="items-tabs">
<a href="?tab=mine<?= $search?"&q=".urlencode($search):'' ?>" class="items-tab <?= $tab==='mine'?'active':'' ?>">
👤 Mes produits <span class="items-tab-count"><?= $countMine ?></span>
</a>
<a href="?tab=scanned<?= $search?"&q=".urlencode($search):'' ?>" class="items-tab <?= $tab==='scanned'?'active':'' ?>">
📷 Scannés <span class="items-tab-count"><?= $countScanned ?></span>
</a>
<a href="?tab=all<?= $search?"&q=".urlencode($search):'' ?>" class="items-tab <?= $tab==='all'?'active':'' ?>">
📦 Tous <span class="items-tab-count"><?= $countAll ?></span>
</a>
</div>
<form method="GET" class="items-search-wrap">
<input type="hidden" name="tab" value="<?= e($tab) ?>">
<?php if ($catFilter !== 'all'): ?>
<input type="hidden" name="cat" value="<?= e($catFilter) ?>">
<?php endif; ?>
<input type="text" name="q" value="<?= e($search) ?>" placeholder="Rechercher..." class="items-search">
<button type="submit" class="items-search-btn">🔍</button>
</form>
</div>
<!-- Filtres catégories -->
<div class="cat-filters" style="margin-bottom:16px;">
<a href="?tab=<?= $tab ?><?= $search?"&q=".urlencode($search):'' ?>&cat=all" class="cat-filter <?= $catFilter==='all'?'active':'' ?>">Tous</a>
<?php foreach ($catMeta as $cat => $meta): ?>
<a href="?tab=<?= $tab ?><?= $search?"&q=".urlencode($search):'' ?>&cat=<?= $cat ?>"
class="cat-filter <?= $catFilter===$cat?'active':'' ?>">
<?= $meta['icon'] ?> <?= $meta['label'] ?>
</a>
<?php endforeach; ?>
</div>
<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:14px;">
<?= number_format($total) ?> produit<?= $total !== 1 ? 's' : '' ?>
<?php if ($tab === 'all'): ?>
— les produits sans ingrédients peuvent être enrichis via le scanner
<?php endif; ?>
</div>
<?php if (empty($items)): ?>
<div class="items-empty">
<div style="font-size:2.5rem;margin-bottom:12px;">📦</div>
<div style="font-size:0.95rem;font-weight:700;margin-bottom:8px;">Aucun produit trouvé</div>
<?php if ($tab === 'mine'): ?>
<div style="font-size:0.83rem;margin-bottom:16px;">Scannez des produits depuis votre journal pour les voir ici.</div>
<a href="<?= url('app/journal.php') ?>" style="display:inline-flex;align-items:center;gap:6px;background:var(--cyan);color:var(--bg);padding:9px 18px;border-radius:8px;text-decoration:none;font-size:0.83rem;font-weight:700;">📷 Ouvrir le journal</a>
<?php else: ?>
<div style="font-size:0.83rem;">Aucun résultat pour cette recherche.</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="items-grid">
<?php foreach ($items as $item):
$cat = $catMeta[$item['category']] ?? ['icon'=>'📦','label'=>$item['category']];
$ingCount = (int)$item['ingredient_count'];
$suspCount = (int)$item['suspect_count'];
$isMine = $item['user_id'] == $user['id'];
$isScanned = in_array($item['source'], ['off','obf']);
$ingredients = [];
if ($ingCount > 0) {
$ingredients = db_fetch_all(
"SELECT inci_name, common_name, is_suspect FROM item_ingredients
WHERE item_id = ? ORDER BY position ASC LIMIT 100",
[$item['id']]
);
}
?>
<div class="item-card" id="icard-<?= $item['id'] ?>">
<div class="item-card-top">
<div class="item-img">
<?php if ($item['image_url']): ?>
<img src="<?= e($item['image_url']) ?>" alt="" loading="lazy">
<?php else: ?>
<?= $cat['icon'] ?>
<?php endif; ?>
</div>
<div class="item-info">
<div class="item-name" title="<?= e($item['name']) ?>"><?= e($item['name']) ?></div>
<?php if ($item['brand']): ?>
<div class="item-brand"><?= e($item['brand']) ?></div>
<?php endif; ?>
<div class="item-badges">
<span class="ibadge ibadge-cat"><?= $cat['icon'] ?> <?= $cat['label'] ?></span>
<?php if ($isMine): ?>
<span class="ibadge ibadge-mine">✓ Mon produit</span>
<?php endif; ?>
<?php if ($ingCount > 0): ?>
<span class="ibadge" style="background:rgba(0,212,200,0.08);color:var(--cyan);">📋 <?= $ingCount ?> ing.</span>
<?php else: ?>
<span class="ibadge ibadge-nodata">Ingrédients manquants</span>
<?php endif; ?>
<?php if ($suspCount > 0): ?>
<span class="ibadge ibadge-suspect">⚠ <?= $suspCount ?> suspect<?= $suspCount>1?'s':'' ?></span>
<?php endif; ?>
</div>
</div>
</div>
<div class="item-ing-section">
<?php if ($ingCount > 0): ?>
<button class="item-ing-btn" onclick="toggleIng(this, <?= $item['id'] ?>)">
📋 Voir les <?= $ingCount ?> ingrédients
</button>
<div class="item-ing-tags" id="ing-<?= $item['id'] ?>">
<?php foreach ($ingredients as $ing): ?>
<span class="ing-tag <?= $ing['is_suspect']?'suspect':'' ?>"
title="<?= e($ing['inci_name']) ?>">
<?= e($ing['common_name'] ?: ucfirst($ing['inci_name'])) ?>
</span>
<?php endforeach; ?>
</div>
<?php else: ?>
<!-- Boutons ajout ingrédients -->
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="item-ing-btn" onclick="showAddIng(this, <?= $item['id'] ?>)">
✏️ Saisir manuellement
</button>
<button class="item-ing-btn" onclick="startOCR(this, <?= $item['id'] ?>)" style="color:var(--cyan);">
📷 Scanner (OCR)
</button>
</div>
<!-- Saisie manuelle -->
<div id="addIng-<?= $item['id'] ?>" style="display:none;margin-top:8px;">
<div style="font-size:0.72rem;color:var(--text-dim);margin-bottom:6px;">
Liste des ingrédients séparés par des virgules :
</div>
<textarea id="ingText-<?= $item['id'] ?>"
placeholder="Aqua, Sodium Laureth Sulfate, Parfum..."
style="width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:7px 9px;color:var(--text);font-size:0.75rem;font-family:inherit;resize:vertical;min-height:60px;"></textarea>
<div style="display:flex;gap:6px;margin-top:6px;">
<button onclick="saveIngredients(<?= $item['id'] ?>)"
style="background:var(--cyan);color:var(--bg);border:none;border-radius:7px;padding:6px 12px;cursor:pointer;font-size:0.75rem;font-weight:700;font-family:inherit;">
Enregistrer
</button>
<button onclick="document.getElementById('addIng-<?= $item['id'] ?>').style.display='none'"
style="background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:6px 10px;cursor:pointer;font-size:0.75rem;font-family:inherit;color:var(--text-mid);">
Annuler
</button>
</div>
</div>
<!-- Zone OCR -->
<div id="ocrZone-<?= $item['id'] ?>" style="display:none;margin-top:8px;">
<div style="font-size:0.72rem;color:var(--text-dim);margin-bottom:6px;">
📷 Prenez une photo de la liste des ingrédients sur la boîte :
</div>
<input type="file" accept="image/*" capture="environment"
id="ocrInput-<?= $item['id'] ?>"
style="display:none;"
onchange="processOCR(this, <?= $item['id'] ?>)">
<button onclick="document.getElementById('ocrInput-<?= $item['id'] ?>').click()"
style="background:var(--cyan);color:var(--bg);border:none;border-radius:7px;padding:6px 14px;cursor:pointer;font-size:0.75rem;font-weight:700;font-family:inherit;width:100%;margin-bottom:6px;">
📷 Choisir / Prendre une photo
</button>
<div id="ocrStatus-<?= $item['id'] ?>" style="font-size:0.72rem;color:var(--text-dim);min-height:16px;"></div>
<div id="ocrPreview-<?= $item['id'] ?>" style="display:none;margin-top:8px;">
<div style="font-size:0.72rem;color:var(--text-dim);margin-bottom:4px;">Texte détecté — vérifiez et corrigez :</div>
<textarea id="ocrText-<?= $item['id'] ?>"
style="width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:7px 9px;color:var(--text);font-size:0.75rem;font-family:inherit;resize:vertical;min-height:80px;"></textarea>
<div style="display:flex;gap:6px;margin-top:6px;">
<button onclick="saveOCRIngredients(<?= $item['id'] ?>)"
style="background:var(--cyan);color:var(--bg);border:none;border-radius:7px;padding:6px 12px;cursor:pointer;font-size:0.75rem;font-weight:700;font-family:inherit;">
✅ Enregistrer
</button>
<button onclick="document.getElementById('ocrZone-<?= $item['id'] ?>').style.display='none'"
style="background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:6px 10px;cursor:pointer;font-size:0.75rem;font-family:inherit;color:var(--text-mid);">
Annuler
</button>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($totalPages > 1): ?>
<nav class="pagination">
<?php if ($page > 1): ?>
<a href="?tab=<?= $tab ?>&cat=<?= $catFilter ?>&q=<?= urlencode($search) ?>&page=<?= $page-1 ?>" class="page-btn">← Précédent</a>
<?php endif; ?>
<?php for ($p = max(1,$page-2); $p <= min($totalPages,$page+2); $p++): ?>
<a href="?tab=<?= $tab ?>&cat=<?= $catFilter ?>&q=<?= urlencode($search) ?>&page=<?= $p ?>"
class="page-btn <?= $p===$page?'active':'' ?>"><?= $p ?></a>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="?tab=<?= $tab ?>&cat=<?= $catFilter ?>&q=<?= urlencode($search) ?>&page=<?= $page+1 ?>" class="page-btn">Suivant →</a>
<?php endif; ?>
</nav>
<?php endif; ?>
<?php endif; ?>
<?php
$content = ob_get_clean();
$extraScriptFiles = ($extraScriptFiles ?? '') . '<script>' . PHP_EOL;
$extraScriptFiles .= <<<'JSEOF'
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
function toggleIng(btn, id) {
const tags = document.getElementById('ing-' + id);
if (!tags) return;
const open = tags.classList.toggle('open');
const count = tags.querySelectorAll('.ing-tag').length;
btn.innerHTML = open
? '📋 Masquer les ingrédients'
: '📋 Voir les ' + count + ' ingrédients';
}
function showAddIng(btn, id) {
btn.style.display = 'none';
document.getElementById('addIng-' + id).style.display = 'block';
}
function startOCR(btn, id) {
document.getElementById('addIng-' + id).style.display = 'none';
const zone = document.getElementById('ocrZone-' + id);
zone.style.display = zone.style.display === 'none' ? 'block' : 'none';
}
async function processOCR(input, itemId) {
const file = input.files[0];
if (!file) return;
const status = document.getElementById('ocrStatus-' + itemId);
const preview = document.getElementById('ocrPreview-' + itemId);
status.style.color = 'var(--cyan)';
status.textContent = '⏳ Analyse en cours...';
preview.style.display = 'none';
try {
if (typeof Tesseract === 'undefined') { status.textContent = '❌ Tesseract non chargé'; return; }
const result = await Tesseract.recognize(file, 'fra', {
logger: m => { if (m.status === 'recognizing text') status.textContent = '⏳ ' + Math.round(m.progress*100) + '%'; }
});
let text = result.data.text.split('\n').join(', ').replace(/,\s*,/g, ',').trim();
document.getElementById('ocrText-' + itemId).value = text;
preview.style.display = 'block';
status.style.color = 'var(--emerald)';
status.textContent = '✅ Texte reconnu';
} catch(err) { status.style.color='var(--rose)'; status.textContent='❌ Erreur: '+err.message; }
}
async function saveOCRIngredients(itemId) {
document.getElementById('ingText-' + itemId).value = document.getElementById('ocrText-' + itemId).value;
await saveIngredients(itemId);
}
async function saveIngredients(itemId) {
const text = document.getElementById('ingText-' + itemId).value.trim();
if (!text) return;
const fd = new FormData();
fd.append('action', 'save_ingredients');
fd.append('item_id', itemId);
fd.append('ingredients_text', text);
fd.append('_csrf', csrf);
try {
const resp = await fetch('/api/save-ingredients.php', { method:'POST', body:fd, headers:{'X-Requested-With':'XMLHttpRequest'} });
const data = await resp.json();
if (data.success) location.reload();
else alert(data.error || 'Erreur');
} catch { alert('Erreur réseau'); }
}
JSEOF;
$extraScriptFiles .= PHP_EOL . '</script>';
require DS_VIEWS . 'layouts/app.php';