557 lines
27 KiB
PHP
557 lines
27 KiB
PHP
<?php
|
|
// admin_subscription_plans.php
|
|
|
|
// Show errors (remove in production)
|
|
ini_set('display_errors', 1);
|
|
ini_set('display_startup_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
include('include/headscript.php');
|
|
|
|
// DB connection
|
|
$dbHandler = DBHandlerSelect::getInstance();
|
|
$pdo = $dbHandler->getConnection();
|
|
|
|
// Check login
|
|
if (!isset($iduserlogin)) {
|
|
die("Errore: ID utente non definito.");
|
|
}
|
|
|
|
/**
|
|
* Check if the current user is admin.
|
|
* IMPORTANT: set your real admin role_id(s) here.
|
|
*/
|
|
function isAdmin(PDO $pdo, int $userId): bool
|
|
{
|
|
// TODO: adjust these role ids according to your system
|
|
$adminRoleIds = [1]; // e.g. 1 = superadmin
|
|
|
|
$stmt = $pdo->prepare("SELECT role_id FROM auth_users WHERE id = ?");
|
|
$stmt->execute([$userId]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) return false;
|
|
return in_array((int)$row['role_id'], $adminRoleIds, true);
|
|
}
|
|
|
|
if (!isAdmin($pdo, (int)$iduserlogin)) {
|
|
die("Accesso negato: pagina riservata all'amministratore.");
|
|
}
|
|
|
|
function formatMoneyFromCents(int $cents, string $currency): string
|
|
{
|
|
$amount = number_format($cents / 100, 2, ',', '.');
|
|
return $amount . ' ' . strtoupper($currency);
|
|
}
|
|
|
|
// Handle POST actions
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$action = $_POST['action'] ?? '';
|
|
|
|
// Common fields
|
|
$code = trim($_POST['code'] ?? '');
|
|
$name = trim($_POST['name'] ?? '');
|
|
$description = trim($_POST['description'] ?? '');
|
|
|
|
$stripe_product_id = trim($_POST['stripe_product_id'] ?? '');
|
|
$stripe_price_id = trim($_POST['stripe_price_id'] ?? '');
|
|
|
|
$currency = strtoupper(trim($_POST['currency'] ?? 'EUR'));
|
|
$unit_amount = (int)($_POST['unit_amount'] ?? 0); // cents
|
|
$interval = in_array(($_POST['interval'] ?? ''), ['day', 'week', 'month', 'year'], true) ? $_POST['interval'] : 'month';
|
|
$interval_count = max(1, (int)($_POST['interval_count'] ?? 1));
|
|
$trial_days = max(0, (int)($_POST['trial_days'] ?? 0));
|
|
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
|
|
|
// ADD
|
|
if ($action === 'add_plan') {
|
|
if ($code === '' || $name === '') {
|
|
$error = "Code e Nome sono obbligatori.";
|
|
} elseif ($stripe_price_id === '') {
|
|
$error = "Stripe Price ID è obbligatorio (campo NOT NULL in tabella).";
|
|
} elseif (strlen($currency) !== 3) {
|
|
$error = "Currency deve essere nel formato ISO (es. EUR).";
|
|
} elseif ($unit_amount < 0) {
|
|
$error = "Unit amount non può essere negativo.";
|
|
} else {
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO billing_plans
|
|
(code, name, description, stripe_product_id, stripe_price_id, currency, unit_amount, `interval`, interval_count, trial_days, is_active, created_at, updated_at)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
|
");
|
|
$stmt->execute([
|
|
$code,
|
|
$name,
|
|
$description ?: null,
|
|
$stripe_product_id ?: null,
|
|
$stripe_price_id,
|
|
$currency,
|
|
$unit_amount,
|
|
$interval,
|
|
$interval_count,
|
|
$trial_days,
|
|
$is_active
|
|
]);
|
|
$success_message = "Piano creato con successo.";
|
|
} catch (PDOException $e) {
|
|
$error = "Errore durante la creazione del piano: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
// EDIT
|
|
if ($action === 'edit_plan') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
|
|
if ($id <= 0) {
|
|
$error = "ID piano non valido.";
|
|
} elseif ($code === '' || $name === '') {
|
|
$error = "Code e Nome sono obbligatori.";
|
|
} elseif ($stripe_price_id === '') {
|
|
$error = "Stripe Price ID è obbligatorio (campo NOT NULL in tabella).";
|
|
} elseif (strlen($currency) !== 3) {
|
|
$error = "Currency deve essere nel formato ISO (es. EUR).";
|
|
} elseif ($unit_amount < 0) {
|
|
$error = "Unit amount non può essere negativo.";
|
|
} else {
|
|
try {
|
|
$stmt = $pdo->prepare("
|
|
UPDATE billing_plans
|
|
SET
|
|
code = ?,
|
|
name = ?,
|
|
description = ?,
|
|
stripe_product_id = ?,
|
|
stripe_price_id = ?,
|
|
currency = ?,
|
|
unit_amount = ?,
|
|
`interval` = ?,
|
|
interval_count = ?,
|
|
trial_days = ?,
|
|
is_active = ?,
|
|
updated_at = NOW()
|
|
WHERE id = ?
|
|
");
|
|
$stmt->execute([
|
|
$code,
|
|
$name,
|
|
$description ?: null,
|
|
$stripe_product_id ?: null,
|
|
$stripe_price_id,
|
|
$currency,
|
|
$unit_amount,
|
|
$interval,
|
|
$interval_count,
|
|
$trial_days,
|
|
$is_active,
|
|
$id
|
|
]);
|
|
$success_message = "Piano aggiornato con successo.";
|
|
} catch (PDOException $e) {
|
|
$error = "Errore durante l'aggiornamento del piano: " . $e->getMessage();
|
|
}
|
|
}
|
|
}
|
|
|
|
// DISABLE
|
|
if ($action === 'disable_plan') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
if ($id <= 0) {
|
|
$error = "ID piano non valido.";
|
|
} else {
|
|
$stmt = $pdo->prepare("UPDATE billing_plans SET is_active = 0, updated_at = NOW() WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$success_message = "Piano disattivato.";
|
|
}
|
|
}
|
|
|
|
// ENABLE
|
|
if ($action === 'enable_plan') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
if ($id <= 0) {
|
|
$error = "ID piano non valido.";
|
|
} else {
|
|
$stmt = $pdo->prepare("UPDATE billing_plans SET is_active = 1, updated_at = NOW() WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$success_message = "Piano riattivato.";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fetch plans
|
|
$stmt = $pdo->prepare("SELECT * FROM billing_plans ORDER BY is_active DESC, name ASC");
|
|
$stmt->execute();
|
|
$plans = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
?>
|
|
|
|
<!doctype html>
|
|
<html lang="it">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
|
<?php include('cssinclude.php'); ?>
|
|
<?php include('siteinfo.php'); ?>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="wrapper">
|
|
<?php include('include/navbar.php'); ?>
|
|
<?php include('include/topbar.php'); ?>
|
|
|
|
<div class="page-wrapper">
|
|
<div class="page-content">
|
|
|
|
<div class="card radius-10 mb-4">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h5 class="mb-1">Subscription Plans (Admin)</h5>
|
|
<p class="mb-0 text-muted">Gestione piani abbonamento delle scuole</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPlanModal">
|
|
<i class="bx bx-plus"></i> Nuovo Piano
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (isset($error)): ?>
|
|
<div class="alert alert-danger" role="alert"><?php echo htmlspecialchars($error); ?></div>
|
|
<?php endif; ?>
|
|
<?php if (isset($success_message)): ?>
|
|
<div class="alert alert-success" role="alert"><?php echo htmlspecialchars($success_message); ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card radius-10">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">Elenco Piani</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table id="plansTable" class="table table-striped table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Code</th>
|
|
<th>Nome</th>
|
|
<th>Prezzo</th>
|
|
<th>Intervallo</th>
|
|
<th>Trial</th>
|
|
<th>Stripe Product</th>
|
|
<th>Stripe Price *</th>
|
|
<th>Stato</th>
|
|
<th>Azioni</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($plans as $p): ?>
|
|
<tr>
|
|
<td><span class="badge bg-dark"><?php echo htmlspecialchars($p['code']); ?></span></td>
|
|
<td>
|
|
<div class="fw-bold"><?php echo htmlspecialchars($p['name']); ?></div>
|
|
<?php if (!empty($p['description'])): ?>
|
|
<div class="text-muted small"><?php echo nl2br(htmlspecialchars($p['description'])); ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php echo formatMoneyFromCents((int)$p['unit_amount'], $p['currency']); ?></td>
|
|
<td><?php echo (int)$p['interval_count'] . ' / ' . htmlspecialchars($p['interval']); ?></td>
|
|
<td><?php echo (int)$p['trial_days'] > 0 ? ((int)$p['trial_days'] . ' gg') : '—'; ?></td>
|
|
<td>
|
|
<?php if (!empty($p['stripe_product_id'])): ?>
|
|
<span class="badge bg-secondary"><?php echo htmlspecialchars($p['stripe_product_id']); ?></span>
|
|
<?php else: ?>
|
|
<span class="text-muted">—</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info"><?php echo htmlspecialchars($p['stripe_price_id']); ?></span>
|
|
</td>
|
|
<td>
|
|
<span class="badge <?php echo ((int)$p['is_active'] === 1) ? 'bg-success' : 'bg-danger'; ?>">
|
|
<?php echo ((int)$p['is_active'] === 1) ? 'Attivo' : 'Disattivo'; ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-nowrap">
|
|
<button class="btn btn-sm btn-warning"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#editPlanModal"
|
|
onclick='fillEditPlanModal(<?php echo json_encode([
|
|
"id" => (int)$p["id"],
|
|
"code" => $p["code"],
|
|
"name" => $p["name"],
|
|
"description" => $p["description"],
|
|
"stripe_product_id" => $p["stripe_product_id"],
|
|
"stripe_price_id" => $p["stripe_price_id"],
|
|
"currency" => $p["currency"],
|
|
"unit_amount" => (int)$p["unit_amount"],
|
|
"interval" => $p["interval"],
|
|
"interval_count" => (int)$p["interval_count"],
|
|
"trial_days" => (int)$p["trial_days"],
|
|
"is_active" => (int)$p["is_active"],
|
|
]); ?>)'>
|
|
<i class="bx bx-edit"></i>
|
|
</button>
|
|
|
|
<?php if ((int)$p['is_active'] === 1): ?>
|
|
<form method="POST" style="display:inline;" onsubmit="return confirm('Disattivare questo piano?');">
|
|
<input type="hidden" name="action" value="disable_plan">
|
|
<input type="hidden" name="id" value="<?php echo (int)$p['id']; ?>">
|
|
<button class="btn btn-sm btn-danger" type="submit" title="Disattiva">
|
|
<i class="bx bx-power-off"></i>
|
|
</button>
|
|
</form>
|
|
<?php else: ?>
|
|
<form method="POST" style="display:inline;" onsubmit="return confirm('Riattivare questo piano?');">
|
|
<input type="hidden" name="action" value="enable_plan">
|
|
<input type="hidden" name="id" value="<?php echo (int)$p['id']; ?>">
|
|
<button class="btn btn-sm btn-success" type="submit" title="Riattiva">
|
|
<i class="bx bx-check-circle"></i>
|
|
</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-3 text-muted small">
|
|
<strong>Nota:</strong> `unit_amount` è in centesimi (es. 2990 = 29,90 EUR)
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overlay toggle-icon"></div>
|
|
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
|
|
<?php include('include/footer.php'); ?>
|
|
</div>
|
|
|
|
<?php include('jsinclude.php'); ?>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
$('#plansTable').DataTable({
|
|
"language": {
|
|
"url": "//cdn.datatables.net/plug-ins/1.10.25/i18n/Italian.json"
|
|
},
|
|
"pageLength": 10,
|
|
"lengthMenu": [10, 25, 50, 100],
|
|
"order": [
|
|
[1, "asc"]
|
|
]
|
|
});
|
|
});
|
|
|
|
function fillEditPlanModal(p) {
|
|
document.getElementById('edit_id').value = p.id;
|
|
document.getElementById('edit_code').value = p.code || '';
|
|
document.getElementById('edit_name').value = p.name || '';
|
|
document.getElementById('edit_description').value = p.description || '';
|
|
document.getElementById('edit_stripe_product_id').value = p.stripe_product_id || '';
|
|
document.getElementById('edit_stripe_price_id').value = p.stripe_price_id || '';
|
|
document.getElementById('edit_currency').value = p.currency || 'EUR';
|
|
document.getElementById('edit_unit_amount').value = (p.unit_amount ?? 0);
|
|
document.getElementById('edit_interval').value = p.interval || 'month';
|
|
document.getElementById('edit_interval_count').value = p.interval_count || 1;
|
|
document.getElementById('edit_trial_days').value = p.trial_days || 0;
|
|
document.getElementById('edit_is_active').checked = (parseInt(p.is_active, 10) === 1);
|
|
}
|
|
</script>
|
|
|
|
<!-- ADD MODAL -->
|
|
<div class="modal fade" id="addPlanModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Nuovo Piano</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
|
</div>
|
|
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="action" value="add_plan">
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Code *</label>
|
|
<input type="text" name="code" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-8 mb-3">
|
|
<label class="form-label">Nome *</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Descrizione</label>
|
|
<textarea name="description" class="form-control" rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Valuta</label>
|
|
<input type="text" name="currency" class="form-control" value="EUR" maxlength="3">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Unit amount (centesimi) *</label>
|
|
<input type="number" name="unit_amount" class="form-control" min="0" value="0" required>
|
|
<small class="text-muted">es: 2990 = 29,90</small>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Trial days</label>
|
|
<input type="number" name="trial_days" class="form-control" min="0" value="0">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Interval</label>
|
|
<select name="interval" class="form-control">
|
|
<option value="day">day</option>
|
|
<option value="week">week</option>
|
|
<option value="month" selected>month</option>
|
|
<option value="year">year</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Interval count</label>
|
|
<input type="number" name="interval_count" class="form-control" min="1" value="1">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Stato</label>
|
|
<div class="form-check form-switch mt-2">
|
|
<input class="form-check-input" type="checkbox" name="is_active" value="1" checked>
|
|
<label class="form-check-label">Attivo</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Stripe product id</label>
|
|
<input type="text" name="stripe_product_id" class="form-control" placeholder="prod_... (optional)">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Stripe price id *</label>
|
|
<input type="text" name="stripe_price_id" class="form-control" placeholder="price_..." required>
|
|
<small class="text-muted">Obbligatorio (NOT NULL)</small>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Chiudi</button>
|
|
<button type="submit" class="btn btn-primary">Crea Piano</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- EDIT MODAL -->
|
|
<div class="modal fade" id="editPlanModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Modifica Piano</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Chiudi"></button>
|
|
</div>
|
|
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="action" value="edit_plan">
|
|
<input type="hidden" name="id" id="edit_id">
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Code *</label>
|
|
<input type="text" name="code" id="edit_code" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-8 mb-3">
|
|
<label class="form-label">Nome *</label>
|
|
<input type="text" name="name" id="edit_name" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Descrizione</label>
|
|
<textarea name="description" id="edit_description" class="form-control" rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Valuta</label>
|
|
<input type="text" name="currency" id="edit_currency" class="form-control" maxlength="3">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Unit amount (centesimi) *</label>
|
|
<input type="number" name="unit_amount" id="edit_unit_amount" class="form-control" min="0" required>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Trial days</label>
|
|
<input type="number" name="trial_days" id="edit_trial_days" class="form-control" min="0">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Interval</label>
|
|
<select name="interval" id="edit_interval" class="form-control">
|
|
<option value="day">day</option>
|
|
<option value="week">week</option>
|
|
<option value="month">month</option>
|
|
<option value="year">year</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Interval count</label>
|
|
<input type="number" name="interval_count" id="edit_interval_count" class="form-control" min="1">
|
|
</div>
|
|
<div class="col-md-4 mb-3">
|
|
<label class="form-label">Stato</label>
|
|
<div class="form-check form-switch mt-2">
|
|
<input class="form-check-input" type="checkbox" name="is_active" id="edit_is_active" value="1">
|
|
<label class="form-check-label">Attivo</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Stripe product id</label>
|
|
<input type="text" name="stripe_product_id" id="edit_stripe_product_id" class="form-control">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Stripe price id *</label>
|
|
<input type="text" name="stripe_price_id" id="edit_stripe_price_id" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Chiudi</button>
|
|
<button type="submit" class="btn btn-primary">Salva</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
|
|
</html>
|