405 lines
14 KiB
PHP
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();
|