HEX
Server: Apache
System: Linux info 3.0 #1337 SMP Tue Jan 01 00:00:00 CEST 2000 all GNU/Linux
User: u115237990 (7145895)
PHP: 8.3.32
Disabled: NONE
Upload Files
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';