Files
yogibook_aury_new/public/product_save.php
T

405 lines
14 KiB
PHP

<?php
require_once('include/headscript.php');
require_once('include/csrf.php');
SchoolContext::requireSchoolAdmin();
/**
* Catalogo prodotti - salvataggio (insert/update) di prodotto, classi valide, opzioni e galleria.
* Le opzioni rimosse vengono eliminate solo se mai usate; altrimenti vengono disattivate.
*/
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: products.php');
exit();
}
yb_csrf_check();
$pdo = DBHandlerSelect::getInstance()->getConnection();
$id = (int) ($_POST['id'] ?? 0);
function back(string $msg, int $id = 0): void
{
header('Location: ' . ($id > 0 ? 'product_form.php?id=' . $id . '&' : ($msg === 'saved' || $msg === 'saved_kept' ? 'products.php?' : 'product_form.php?')) . 'message=' . $msg);
exit();
}
function yn($v): string
{
return $v === 'Y' ? 'Y' : 'N';
}
function intOrNull($v): ?int
{
$v = trim((string) $v);
if ($v === '' || !is_numeric($v)) {
return null;
}
return max(0, (int) $v);
}
function money($v): ?float
{
$v = str_replace(',', '.', trim((string) $v));
if ($v === '' || !is_numeric($v)) {
return null;
}
return round(max(0, (float) $v), 2);
}
function textOrNull($v, int $max = 65000): ?string
{
$v = trim((string) $v);
return $v === '' ? null : mb_substr($v, 0, $max);
}
function slugify(string $s): string
{
$t = function_exists('iconv') ? @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s) : $s;
if ($t === false) {
$t = $s;
}
$t = strtolower($t);
$t = preg_replace('/[^a-z0-9]+/', '-', $t);
return trim((string) $t, '-');
}
/** HTML dell'editor: tag ammessi, niente attributi on*, niente javascript: */
function cleanHtml(string $html): ?string
{
$html = trim($html);
if ($html === '') {
return null;
}
$html = strip_tags($html, '<p><br><strong><b><em><i><u><s><h2><h3><ul><ol><li><a><span><blockquote>');
$html = preg_replace('/\s+on[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $html);
$html = preg_replace('/(href|src)\s*=\s*("|\')\s*(javascript|data|vbscript):[^"\']*\2/i', '$1="#"', $html);
return $html;
}
$imgPrefix = 'uploads/schools/' . (int) $idschool . '/products/';
function validImage(string $path, string $prefix): bool
{
return strpos($path, $prefix) === 0
&& strpos($path, '..') === false
&& preg_match('#^[A-Za-z0-9/_\-]+\.(jpe?g|png|webp|gif)$#', $path) === 1;
}
// ---------------------------------------------------------------------
// Prodotto esistente: appartenenza, tipo bloccato se già venduto
// ---------------------------------------------------------------------
$oldKind = null;
$nsold = 0;
if ($id > 0) {
$st = $pdo->prepare("SELECT kind FROM products WHERE idproduct = ? AND idschool = ?");
$st->execute([$id, $idschool]);
$oldKind = $st->fetchColumn();
if ($oldKind === false) {
header('Location: products.php?message=error');
exit();
}
$st = $pdo->prepare("SELECT COUNT(*) FROM orderbook WHERE idproduct = ? AND idschool = ?");
$st->execute([$id, $idschool]);
$nsold = (int) $st->fetchColumn();
}
$kind = in_array($_POST['kind'] ?? '', ['lessons', 'membership', 'voucher'], true) ? $_POST['kind'] : 'lessons';
if ($oldKind !== null && $nsold > 0) {
$kind = $oldKind;
}
// ---------------------------------------------------------------------
// Presentazione
// ---------------------------------------------------------------------
$title = trim((string) ($_POST['title'] ?? ''));
if ($title === '' || mb_strlen($title) > 191) {
back('invalid', $id);
}
$data = [
'kind' => $kind,
'title' => $title,
'subtitle' => textOrNull($_POST['subtitle'] ?? '', 255),
'badge' => textOrNull($_POST['badge'] ?? '', 50),
'description' => cleanHtml((string) ($_POST['description'] ?? '')),
'suitable_for' => textOrNull($_POST['suitable_for'] ?? ''),
'pros' => textOrNull($_POST['pros'] ?? ''),
'cons' => textOrNull($_POST['cons'] ?? ''),
'requirements' => textOrNull($_POST['requirements'] ?? ''),
'visible_online' => yn($_POST['visible_online'] ?? 'N'),
'active' => yn($_POST['active'] ?? 'N'),
'sort_order' => (int) ($_POST['sort_order'] ?? 0),
];
$cover = trim((string) ($_POST['cover_image'] ?? ''));
$data['cover_image'] = ($cover !== '' && validImage($cover, $imgPrefix)) ? $cover : null;
$gallery = json_decode((string) ($_POST['gallery'] ?? '[]'), true);
$gallery = is_array($gallery) ? $gallery : [];
$gallery = array_values(array_unique(array_filter($gallery, function ($p) use ($imgPrefix) {
return is_string($p) && validImage($p, $imgPrefix);
})));
$gallery = array_slice($gallery, 0, 10);
// Slug unico per scuola
$base = slugify((string) ($_POST['slug'] ?? '')) ?: slugify($title);
$base = substr($base !== '' ? $base : 'prodotto', 0, 180);
$slug = $base;
$n = 2;
$chk = $pdo->prepare("SELECT 1 FROM products WHERE idschool = ? AND slug = ? AND idproduct <> ? LIMIT 1");
while (true) {
$chk->execute([$idschool, $slug, $id]);
if (!$chk->fetchColumn()) {
break;
}
$slug = $base . '-' . $n++;
}
$data['slug'] = $slug;
// ---------------------------------------------------------------------
// Regole (solo lezioni)
// ---------------------------------------------------------------------
if ($kind === 'lessons') {
$mode = in_array($_POST['booking_mode'] ?? '', ['fixed', 'free', 'single'], true) ? $_POST['booking_mode'] : 'free';
$data['booking_mode'] = $mode;
$data['fixed_slots'] = ($mode === 'fixed' && (int) ($_POST['fixed_slots'] ?? 1) === 2) ? 2 : 1;
$weekly = $mode === 'free' ? intOrNull($_POST['weekly_limit'] ?? '') : null;
$data['weekly_limit'] = $weekly ?: null;
$data['max_reschedule'] = $mode === 'single' ? null : intOrNull($_POST['max_reschedule'] ?? '');
$data['once_per_student'] = yn($_POST['once_per_student'] ?? 'N');
} else {
$data['booking_mode'] = 'free';
$data['fixed_slots'] = 1;
$data['weekly_limit'] = null;
$data['max_reschedule'] = null;
$data['once_per_student'] = 'N';
}
// ---------------------------------------------------------------------
// Classi valide (solo lezioni): solo ID della scuola
// ---------------------------------------------------------------------
$services = [];
$categories = [];
if ($kind === 'lessons') {
$inSvc = array_values(array_unique(array_filter(array_map('intval', (array) ($_POST['services'] ?? [])))));
$inCat = array_values(array_unique(array_filter(array_map('intval', (array) ($_POST['categories'] ?? [])))));
if ($inSvc) {
$ph = implode(',', array_fill(0, count($inSvc), '?'));
$st = $pdo->prepare("SELECT idservice FROM service WHERE idschool = ? AND idservice IN ($ph)");
$st->execute(array_merge([$idschool], $inSvc));
$services = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
}
if ($inCat) {
$ph = implode(',', array_fill(0, count($inCat), '?'));
$st = $pdo->prepare("SELECT idservicecategory FROM servicecategory WHERE idschool = ? AND idservicecategory IN ($ph)");
$st->execute(array_merge([$idschool], $inCat));
$categories = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
}
if (!$services && !$categories) {
back('noclasses', $id);
}
}
// ---------------------------------------------------------------------
// Opzioni
// ---------------------------------------------------------------------
$existingIds = [];
if ($id > 0) {
$st = $pdo->prepare("SELECT idvariant FROM product_variants WHERE idproduct = ? AND idschool = ?");
$st->execute([$id, $idschool]);
$existingIds = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
}
$validTargets = [];
if ($kind === 'voucher') {
$st = $pdo->prepare(
"SELECT v.idvariant FROM product_variants v
JOIN products p ON p.idproduct = v.idproduct
WHERE v.idschool = ? AND p.kind = 'lessons' AND p.idproduct <> ?"
);
$st->execute([$idschool, $id]);
$validTargets = array_map('intval', $st->fetchAll(PDO::FETCH_COLUMN));
}
$variants = [];
foreach ((array) ($_POST['variants'] ?? []) as $r) {
if (!is_array($r)) {
continue;
}
$label = trim((string) ($r['label'] ?? ''));
if ($label === '') {
continue;
}
$vid = (int) ($r['idvariant'] ?? 0);
if (!in_array($vid, $existingIds, true)) {
$vid = 0;
}
$lessons = $kind === 'lessons' ? intOrNull($r['lessons'] ?? '') : null;
$vmode = ($kind === 'membership' && in_array($r['validity_mode'] ?? '', ['duration', 'calendar_year', 'season'], true))
? $r['validity_mode'] : 'duration';
$vval = null;
$vunit = null;
if ($vmode === 'duration') {
$vval = intOrNull($r['validity_value'] ?? '');
$vunit = in_array($r['validity_unit'] ?? '', ['days', 'weeks', 'months'], true) ? $r['validity_unit'] : 'weeks';
if (!$vval) {
$vmode = 'none';
$vval = null;
$vunit = null;
}
}
$price = money($r['price'] ?? '') ?? 0.0;
$compare = money($r['compare_price'] ?? '');
if ($compare !== null && $compare <= $price) {
$compare = null;
}
$target = null;
if ($kind === 'voucher') {
$target = (int) ($r['target_idvariant'] ?? 0);
if (!in_array($target, $validTargets, true)) {
back('notarget', $id);
}
}
$variants[] = [
'idvariant' => $vid,
'label' => mb_substr($label, 0, 100),
'lessons' => $lessons ?: null,
'validity_mode' => $vmode,
'validity_value' => $vval,
'validity_unit' => $vunit,
'validity_start' => ($kind === 'lessons' && ($r['validity_start'] ?? '') !== 'purchase') ? 'first_lesson' : 'purchase',
'price' => $price,
'compare_price' => $compare,
'price_note' => textOrNull($r['price_note'] ?? '', 100),
'target_idvariant' => $target,
'wc_product_id' => intOrNull($r['wc_product_id'] ?? '') ?: null,
'wc_variation_id' => intOrNull($r['wc_variation_id'] ?? '') ?: null,
'active' => yn($r['active'] ?? 'N'),
];
}
if (!$variants) {
back('novariants', $id);
}
// ---------------------------------------------------------------------
// Salvataggio
// ---------------------------------------------------------------------
$kept = false;
try {
$pdo->beginTransaction();
$cols = array_keys($data);
if ($id > 0) {
$set = implode(', ', array_map(function ($c) {
return "`$c` = ?";
}, $cols));
$pdo->prepare("UPDATE products SET $set WHERE idproduct = ? AND idschool = ?")
->execute(array_merge(array_values($data), [$id, $idschool]));
} else {
$colList = '`idschool`, ' . implode(', ', array_map(function ($c) {
return "`$c`";
}, $cols));
$ph = implode(', ', array_fill(0, count($cols) + 1, '?'));
$pdo->prepare("INSERT INTO products ($colList) VALUES ($ph)")
->execute(array_merge([$idschool], array_values($data)));
$id = (int) $pdo->lastInsertId();
}
// Classi valide
$pdo->prepare("DELETE FROM product_services WHERE idproduct = ? AND idschool = ?")->execute([$id, $idschool]);
$ins = $pdo->prepare("INSERT INTO product_services (idproduct, idschool, idservice, idservicecategory) VALUES (?, ?, ?, ?)");
foreach ($categories as $cid) {
$ins->execute([$id, $idschool, null, $cid]);
}
foreach ($services as $sid) {
$ins->execute([$id, $idschool, $sid, null]);
}
// Opzioni
$vCols = [
'label',
'lessons',
'validity_mode',
'validity_value',
'validity_unit',
'validity_start',
'price',
'compare_price',
'price_note',
'target_idvariant',
'wc_product_id',
'wc_variation_id',
'active',
'sort_order'
];
$upd = $pdo->prepare(
"UPDATE product_variants SET " . implode(', ', array_map(function ($c) {
return "`$c` = ?";
}, $vCols)) .
" WHERE idvariant = ? AND idproduct = ? AND idschool = ?"
);
$insV = $pdo->prepare(
"INSERT INTO product_variants (idproduct, idschool, " . implode(', ', array_map(function ($c) {
return "`$c`";
}, $vCols)) . ")
VALUES (?, ?, " . implode(', ', array_fill(0, count($vCols), '?')) . ")"
);
$keepIds = [];
foreach ($variants as $pos => $v) {
$v['sort_order'] = $pos * 10;
$values = [];
foreach ($vCols as $c) {
$values[] = $v[$c];
}
if ($v['idvariant'] > 0) {
$upd->execute(array_merge($values, [$v['idvariant'], $id, $idschool]));
$keepIds[] = $v['idvariant'];
} else {
$insV->execute(array_merge([$id, $idschool], $values));
}
}
// Opzioni rimosse: elimina se mai usate, altrimenti disattiva
$used = $pdo->prepare(
"SELECT (SELECT COUNT(*) FROM orderbook WHERE idvariant = ? AND idschool = ?)
+ (SELECT COUNT(*) FROM gift_vouchers WHERE idvariant = ? AND idschool = ?)
+ (SELECT COUNT(*) FROM product_variants WHERE target_idvariant = ? AND idschool = ?)"
);
$del = $pdo->prepare("DELETE FROM product_variants WHERE idvariant = ? AND idproduct = ? AND idschool = ?");
$off = $pdo->prepare("UPDATE product_variants SET active = 'N' WHERE idvariant = ? AND idproduct = ? AND idschool = ?");
foreach (array_diff($existingIds, $keepIds) as $vid) {
$used->execute([$vid, $idschool, $vid, $idschool, $vid, $idschool]);
if ((int) $used->fetchColumn() > 0) {
$off->execute([$vid, $id, $idschool]);
$kept = true;
} else {
$del->execute([$vid, $id, $idschool]);
}
}
// Galleria
$pdo->prepare("DELETE FROM product_images WHERE idproduct = ? AND idschool = ?")->execute([$id, $idschool]);
$insI = $pdo->prepare("INSERT INTO product_images (idproduct, idschool, filename, sort_order) VALUES (?, ?, ?, ?)");
foreach ($gallery as $pos => $path) {
$insI->execute([$id, $idschool, $path, $pos]);
}
$pdo->commit();
} catch (PDOException $ex) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('[product_save] ' . $ex->getMessage());
back('error', $id);
}
header('Location: products.php?message=' . ($kept ? 'saved_kept' : 'saved'));
exit();