owner and admin section
This commit is contained in:
@@ -123,10 +123,10 @@ class LoginController extends Controller
|
|||||||
// Redirezione basata sul ruolo
|
// Redirezione basata sul ruolo
|
||||||
if ($user->hasRole('Admin')) {
|
if ($user->hasRole('Admin')) {
|
||||||
return redirect()->to('userarea/admin.php');
|
return redirect()->to('userarea/admin.php');
|
||||||
|
} elseif ($user->hasRole('Owner')) {
|
||||||
|
return redirect()->to('userarea/appointments.php');
|
||||||
} elseif ($user->hasRole('User')) {
|
} elseif ($user->hasRole('User')) {
|
||||||
return redirect()->to('userarea/select_school.php');
|
return redirect()->to('userarea/my_appointments.php');
|
||||||
} elseif ($user->hasRole('teacher')) {
|
|
||||||
return redirect()->to('userarea/teacher.php');
|
|
||||||
} elseif ($user->hasRole('school_owner')) {
|
} elseif ($user->hasRole('school_owner')) {
|
||||||
return redirect()->to('userarea/school_dashboard.php');
|
return redirect()->to('userarea/school_dashboard.php');
|
||||||
}
|
}
|
||||||
@@ -134,6 +134,10 @@ class LoginController extends Controller
|
|||||||
return redirect()->intended('userarea/default.php');
|
return redirect()->intended('userarea/default.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected function logoutAndRedirectToTokenPage(Request $request, $user, ?string $redirectPage): RedirectResponse
|
protected function logoutAndRedirectToTokenPage(Request $request, $user, ?string $redirectPage): RedirectResponse
|
||||||
{
|
{
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea le tabelle per gli abbonamenti dei saloni:
|
||||||
|
* - subscription_plans: i piani disponibili (gestiti dall'admin)
|
||||||
|
* - shop_subscriptions: l'abbonamento corrente di ogni salone
|
||||||
|
*
|
||||||
|
* Modello: l'abbonamento è il "motore" che accende/spegne shops.active.
|
||||||
|
* expires_at è la data chiave: superata, il cron mette status=expired e spegne il salone.
|
||||||
|
*/
|
||||||
|
class CreateSubscriptions extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
// ---- subscription_plans ----
|
||||||
|
$plans = $this->table('subscription_plans', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'engine' => 'InnoDB',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$plans
|
||||||
|
->addColumn('id', 'integer', ['identity' => true, 'signed' => false])
|
||||||
|
->addColumn('name', 'string', ['limit' => 80])
|
||||||
|
->addColumn('description', 'string', ['limit' => 255, 'null' => true])
|
||||||
|
->addColumn('price', 'decimal', ['precision' => 8, 'scale' => 2, 'default' => '0.00'])
|
||||||
|
->addColumn('billing_period', 'enum', ['values' => ['monthly', 'yearly'], 'default' => 'monthly'])
|
||||||
|
->addColumn('stripe_price_id', 'string', ['limit' => 120, 'null' => true])
|
||||||
|
->addColumn('is_active', 'boolean', ['default' => true])
|
||||||
|
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
|
||||||
|
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
|
||||||
|
->create();
|
||||||
|
|
||||||
|
// ---- shop_subscriptions ----
|
||||||
|
$subs = $this->table('shop_subscriptions', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'engine' => 'InnoDB',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subs
|
||||||
|
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||||
|
->addColumn('shop_id', 'biginteger', ['signed' => false])
|
||||||
|
->addColumn('plan_id', 'integer', ['signed' => false, 'null' => true])
|
||||||
|
->addColumn('status', 'enum', [
|
||||||
|
'values' => ['active', 'expired', 'blocked', 'trialing', 'pending'],
|
||||||
|
'default' => 'pending',
|
||||||
|
])
|
||||||
|
->addColumn('started_at', 'date', ['null' => true])
|
||||||
|
->addColumn('expires_at', 'date', ['null' => true])
|
||||||
|
->addColumn('block_reason', 'string', ['limit' => 255, 'null' => true])
|
||||||
|
->addColumn('stripe_customer_id', 'string', ['limit' => 120, 'null' => true])
|
||||||
|
->addColumn('stripe_subscription_id', 'string', ['limit' => 120, 'null' => true])
|
||||||
|
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
|
||||||
|
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
|
||||||
|
->addIndex(['shop_id'], ['unique' => true, 'name' => 'uniq_shop'])
|
||||||
|
->addIndex(['status'], ['name' => 'idx_status'])
|
||||||
|
->addIndex(['expires_at'], ['name' => 'idx_expires'])
|
||||||
|
->addForeignKey('shop_id', 'shops', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION', 'constraint' => 'fk_sub_shop'])
|
||||||
|
->addForeignKey('plan_id', 'subscription_plans', 'id', ['delete' => 'SET_NULL', 'update' => 'NO_ACTION', 'constraint' => 'fk_sub_plan'])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tabella billing_profiles: dati fiscali per la fatturazione degli abbonamenti.
|
||||||
|
* Legata sia all'utente (chi paga) sia al salone (per cosa), così regge
|
||||||
|
* anche il caso futuro di un owner con più saloni e P.IVA diverse.
|
||||||
|
*/
|
||||||
|
class CreateBillingProfiles extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$t = $this->table('billing_profiles', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'engine' => 'InnoDB',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$t
|
||||||
|
->addColumn('id', 'biginteger', ['identity' => true, 'signed' => false])
|
||||||
|
->addColumn('user_id', 'integer', ['signed' => false])
|
||||||
|
->addColumn('shop_id', 'biginteger', ['signed' => false, 'null' => true])
|
||||||
|
->addColumn('business_name', 'string', ['limit' => 191])
|
||||||
|
->addColumn('vat_number', 'string', ['limit' => 20])
|
||||||
|
->addColumn('tax_code', 'string', ['limit' => 20, 'null' => true])
|
||||||
|
->addColumn('address', 'string', ['limit' => 255])
|
||||||
|
->addColumn('zip_code', 'string', ['limit' => 10])
|
||||||
|
->addColumn('city', 'string', ['limit' => 100])
|
||||||
|
->addColumn('province', 'string', ['limit' => 50, 'null' => true])
|
||||||
|
->addColumn('pec', 'string', ['limit' => 191, 'null' => true])
|
||||||
|
->addColumn('sdi_code', 'string', ['limit' => 7, 'null' => true])
|
||||||
|
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true])
|
||||||
|
->addColumn('updated_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'update' => 'CURRENT_TIMESTAMP', 'null' => true])
|
||||||
|
->addIndex(['user_id'], ['name' => 'idx_bp_user'])
|
||||||
|
->addIndex(['shop_id'], ['name' => 'idx_bp_shop'])
|
||||||
|
->addForeignKey('user_id', 'auth_users', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION', 'constraint' => 'fk_bp_user'])
|
||||||
|
->addForeignKey('shop_id', 'shops', 'id', ['delete' => 'SET_NULL', 'update' => 'NO_ACTION', 'constraint' => 'fk_bp_shop'])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggiunge il campo "target" (pubblico servito) a shops, staff e services.
|
||||||
|
* Tipo SET MySQL: si possono selezionare più valori insieme (es. 'uomo,donna').
|
||||||
|
* Valori: uomo, donna, bambino, unisex.
|
||||||
|
*
|
||||||
|
* Scelta SET vs tabelle di collegamento: le categorie sono 4, fisse e senza
|
||||||
|
* attributi propri, quindi il SET è più semplice e filtrabile con FIND_IN_SET()
|
||||||
|
* senza JOIN aggiuntive. Nota logica (da applicare in ricerca): "unisex" copre
|
||||||
|
* sia uomo che donna.
|
||||||
|
*/
|
||||||
|
class AddTargetAudience extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$values = "'uomo','donna','bambino','unisex'";
|
||||||
|
|
||||||
|
// shops: default unisex (un salone di norma serve tutti finché non specifica)
|
||||||
|
$this->execute("
|
||||||
|
ALTER TABLE `shops`
|
||||||
|
ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
|
||||||
|
AFTER `description`
|
||||||
|
");
|
||||||
|
|
||||||
|
// staff: chi tratta cosa. Default unisex.
|
||||||
|
$this->execute("
|
||||||
|
ALTER TABLE `staff`
|
||||||
|
ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
|
||||||
|
AFTER `role`
|
||||||
|
");
|
||||||
|
|
||||||
|
// services: a chi è rivolto il servizio. Default unisex.
|
||||||
|
$this->execute("
|
||||||
|
ALTER TABLE `services`
|
||||||
|
ADD COLUMN `target` SET($values) NOT NULL DEFAULT 'unisex'
|
||||||
|
AFTER `category`
|
||||||
|
");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Phinx\Seed\AbstractSeed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserisce due piani base d'esempio.
|
||||||
|
* I prezzi/nome sono modificabili poi dal pannello admin o ri-eseguendo il seed.
|
||||||
|
* stripe_price_id resta NULL: lo popoleremo quando colleghiamo Stripe.
|
||||||
|
*/
|
||||||
|
class SubscriptionPlansSeeder extends AbstractSeed
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Evita duplicati se il seed viene rilanciato: inserisce solo se la tabella è vuota
|
||||||
|
$count = (int)$this->fetchRow("SELECT COUNT(*) AS c FROM subscription_plans")['c'];
|
||||||
|
if ($count > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
[
|
||||||
|
'name' => 'Base',
|
||||||
|
'description' => 'Prenotazioni online, agenda e gestione clienti.',
|
||||||
|
'price' => 19.90,
|
||||||
|
'billing_period' => 'monthly',
|
||||||
|
'is_active' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Pro',
|
||||||
|
'description' => 'Tutto del piano Base + più staff, statistiche e priorità in ricerca.',
|
||||||
|
'price' => 39.90,
|
||||||
|
'billing_period' => 'monthly',
|
||||||
|
'is_active' => 1,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->table('subscription_plans')->insert($data)->saveData();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 828 KiB |
@@ -0,0 +1,540 @@
|
|||||||
|
<?php
|
||||||
|
// business.php — Landing "Sei un salone?" — spiega il servizio, i vantaggi, i costi bassi
|
||||||
|
// e spinge a pricing.php. Pagina di marketing, contenuti statici.
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html class="no-js" lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<?php include('tophead.php'); ?>
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<style>
|
||||||
|
.bz-accent {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-btn {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px 38px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-btn-primary {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 12px 28px rgba(230, 0, 126, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-btn-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
opacity: .95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-btn-ghost {
|
||||||
|
background: #fff;
|
||||||
|
color: #e6007e;
|
||||||
|
border: 1.5px solid #ffd0e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-btn-ghost:hover {
|
||||||
|
background: #e6007e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HERO */
|
||||||
|
.bz-hero {
|
||||||
|
position: relative;
|
||||||
|
padding: 100px 0 90px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(160deg, #fff5f9 0%, #fff 55%, #fff7f3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-hero-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.1fr .9fr;
|
||||||
|
gap: 50px;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 1150px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:900px) {
|
||||||
|
.bz-hero-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-hero h1 {
|
||||||
|
font-size: 52px;
|
||||||
|
line-height: 1.08;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
letter-spacing: -.03em;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-hero p {
|
||||||
|
font-size: 19px;
|
||||||
|
color: #6b6480;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-hero-cta {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:900px) {
|
||||||
|
.bz-hero-cta {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-hero-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #fff0e8;
|
||||||
|
color: #ff6a3d;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 7px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-hero-visual {
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 30px 70px rgba(43, 37, 64, .18);
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
background: linear-gradient(135deg, #e6007e, #ff6a3d);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-hero-visual i {
|
||||||
|
font-size: 120px;
|
||||||
|
color: rgba(255, 255, 255, .9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:575px) {
|
||||||
|
.bz-hero h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STRISCIA numeri */
|
||||||
|
.bz-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 30px;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 56px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:700px) {
|
||||||
|
.bz-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-stat .num {
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #e6007e;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-stat .lbl {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SEZIONE come funziona */
|
||||||
|
.bz-section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-section h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-section .bz-lead {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 17px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 30px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:800px) {
|
||||||
|
.bz-steps {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-step {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-step-num {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin: 0 auto 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(135deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 900;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 12px 26px rgba(230, 0, 126, .24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-step h4 {
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-step p {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VANTAGGI a due colonne */
|
||||||
|
.bz-benefits {
|
||||||
|
background: #fff5f9;
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-ben-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 26px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:800px) {
|
||||||
|
.bz-ben-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-ben {
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ffe0ee;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-ben-ico {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(135deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-ben h4 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-ben p {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 14.5px;
|
||||||
|
line-height: 1.55;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COSTI */
|
||||||
|
.bz-cost {
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-cost-box {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: linear-gradient(160deg, #fff, #fff5f9);
|
||||||
|
border: 1.5px solid #ffd9e8;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 50px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-cost h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-cost p {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-cost-points {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-cost-point {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #2b2540;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-cost-point i {
|
||||||
|
color: #1db954;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA finale */
|
||||||
|
.bz-final {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
padding: 80px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-final h2 {
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-final p {
|
||||||
|
color: rgba(255, 255, 255, .92);
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-final .bz-btn-white {
|
||||||
|
background: #fff;
|
||||||
|
color: #e6007e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bz-final .bz-btn-white:hover {
|
||||||
|
background: #2b2540;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<?php include('preloader.php'); ?>
|
||||||
|
<?php include('offcanvas.php'); ?>
|
||||||
|
<header class="tp-header-height"><?php include('topbar.php'); ?></header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<section class="bz-hero">
|
||||||
|
<div class="container">
|
||||||
|
<div class="bz-hero-grid">
|
||||||
|
<div>
|
||||||
|
<span class="bz-hero-badge">★ La piattaforma per il tuo salone</span>
|
||||||
|
<h1>Il tuo salone, sempre <span class="bz-accent">prenotabile online</span></h1>
|
||||||
|
<p>Basta telefonate e agende cartacee. Con Come Li Facciamo i tuoi clienti prenotano da soli, a qualsiasi ora, e tu riempi la giornata senza pensieri.</p>
|
||||||
|
<div class="bz-hero-cta">
|
||||||
|
<a href="pricing.php" class="bz-btn bz-btn-primary">Scopri i piani</a>
|
||||||
|
<a href="#come-funziona" class="bz-btn bz-btn-ghost">Come funziona</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-hero-visual">
|
||||||
|
<i class="fa-solid fa-scissors"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- STATS -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="bz-stats">
|
||||||
|
<div class="bz-stat">
|
||||||
|
<div class="num">24/7</div>
|
||||||
|
<div class="lbl">Prenotazioni sempre aperte, anche a salone chiuso</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-stat">
|
||||||
|
<div class="num">0€</div>
|
||||||
|
<div class="lbl">Nessun costo di installazione o hardware</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-stat">
|
||||||
|
<div class="num">5 min</div>
|
||||||
|
<div class="lbl">Per attivare il salone e iniziare</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COME FUNZIONA -->
|
||||||
|
<section class="bz-section" id="come-funziona">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Come funziona</h2>
|
||||||
|
<p class="bz-lead">Tre passi e sei operativo. Nessuna competenza tecnica richiesta.</p>
|
||||||
|
<div class="bz-steps">
|
||||||
|
<div class="bz-step">
|
||||||
|
<div class="bz-step-num">1</div>
|
||||||
|
<h4>Crea il tuo salone</h4>
|
||||||
|
<p>Ti registri, inserisci i servizi, gli orari e il tuo staff. In pochi minuti la tua pagina è online.</p>
|
||||||
|
</div>
|
||||||
|
<div class="bz-step">
|
||||||
|
<div class="bz-step-num">2</div>
|
||||||
|
<h4>Ricevi prenotazioni</h4>
|
||||||
|
<p>I clienti ti trovano nella ricerca, scelgono servizio, operatore e orario. Tu li vedi subito in agenda.</p>
|
||||||
|
</div>
|
||||||
|
<div class="bz-step">
|
||||||
|
<div class="bz-step-num">3</div>
|
||||||
|
<h4>Gestisci tutto</h4>
|
||||||
|
<p>Confermi, sposti o annulli con un tocco. Agenda per operatore, storico clienti e tutto sotto controllo.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- VANTAGGI -->
|
||||||
|
<section class="bz-benefits">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="text-align:center; font-size:34px; font-weight:900; color:#2b2540; margin-bottom:12px;">Perché sceglierci</h2>
|
||||||
|
<p style="text-align:center; color:#6b6480; font-size:17px; max-width:600px; margin:0 auto 50px;">Tutto quello che serve per gestire e far crescere il salone, in un unico posto.</p>
|
||||||
|
<div class="bz-ben-grid">
|
||||||
|
<div class="bz-ben">
|
||||||
|
<div class="bz-ben-ico"><i class="fa-solid fa-calendar-check"></i></div>
|
||||||
|
<div>
|
||||||
|
<h4>Agenda intelligente</h4>
|
||||||
|
<p>Vista per operatore, slot automatici in base a orari e durata dei servizi. Niente più doppie prenotazioni.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-ben">
|
||||||
|
<div class="bz-ben-ico"><i class="fa-solid fa-magnifying-glass"></i></div>
|
||||||
|
<div>
|
||||||
|
<h4>Nuovi clienti</h4>
|
||||||
|
<p>Comparì nella ricerca dei clienti della tua zona e riempi gli spazi vuoti in agenda.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-ben">
|
||||||
|
<div class="bz-ben-ico"><i class="fa-solid fa-mobile-screen"></i></div>
|
||||||
|
<div>
|
||||||
|
<h4>Prenotazioni da mobile</h4>
|
||||||
|
<p>I tuoi clienti prenotano dallo smartphone in pochi tap, senza chiamare né aspettare.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-ben">
|
||||||
|
<div class="bz-ben-ico"><i class="fa-solid fa-qrcode"></i></div>
|
||||||
|
<div>
|
||||||
|
<h4>Pagina salone + QR</h4>
|
||||||
|
<p>Una pagina professionale con i tuoi servizi e un QR code da mettere in vetrina o sui social.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-ben">
|
||||||
|
<div class="bz-ben-ico"><i class="fa-solid fa-users-gear"></i></div>
|
||||||
|
<div>
|
||||||
|
<h4>Gestione staff</h4>
|
||||||
|
<p>Ogni operatore ha la sua agenda e i suoi orari. Il cliente sceglie da chi farsi servire.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bz-ben">
|
||||||
|
<div class="bz-ben-ico"><i class="fa-solid fa-heart"></i></div>
|
||||||
|
<div>
|
||||||
|
<h4>Clienti fidelizzati</h4>
|
||||||
|
<p>Storico appuntamenti e preferiti: i clienti ti ritrovano e tornano con un tap.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- COSTI -->
|
||||||
|
<section class="bz-cost">
|
||||||
|
<div class="container">
|
||||||
|
<div class="bz-cost-box">
|
||||||
|
<h2>Prezzi trasparenti, senza sorprese</h2>
|
||||||
|
<p>Un canone chiaro, tutto incluso. Nessun costo nascosto, nessuna commissione sulle prenotazioni, nessun vincolo a lungo termine.</p>
|
||||||
|
<div class="bz-cost-points">
|
||||||
|
<div class="bz-cost-point"><i class="fa-solid fa-circle-check"></i> Nessuna commissione</div>
|
||||||
|
<div class="bz-cost-point"><i class="fa-solid fa-circle-check"></i> Nessun vincolo</div>
|
||||||
|
<div class="bz-cost-point"><i class="fa-solid fa-circle-check"></i> Disdici quando vuoi</div>
|
||||||
|
</div>
|
||||||
|
<a href="pricing.php" class="bz-btn bz-btn-primary">Vedi i piani e i prezzi</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA FINALE -->
|
||||||
|
<section class="bz-final">
|
||||||
|
<h2>Pronto a far risplendere il tuo salone?</h2>
|
||||||
|
<p>Attivalo oggi e inizia a ricevere prenotazioni da subito.</p>
|
||||||
|
<a href="pricing.php" class="bz-btn bz-btn-white">Inizia ora</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer><?php include('footer.php'); ?></footer>
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
<?php
|
||||||
|
// checkout.php — Riepilogo piano + form registrazione/dati fiscali + pagamento (SIMULATO)
|
||||||
|
// Arriva da pricing.php?plan=ID&cycle=monthly|yearly
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
function getFlash(): ?array
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['flash'])) return null;
|
||||||
|
$f = $_SESSION['flash'];
|
||||||
|
unset($_SESSION['flash']);
|
||||||
|
return $f;
|
||||||
|
}
|
||||||
|
function euro($v): string
|
||||||
|
{
|
||||||
|
return number_format((float)$v, 2, ',', '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$plan_id = (int)($_GET['plan'] ?? 0);
|
||||||
|
$cycle = ($_GET['cycle'] ?? 'monthly') === 'yearly' ? 'yearly' : 'monthly';
|
||||||
|
|
||||||
|
if ($plan_id <= 0) {
|
||||||
|
header('Location: pricing.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piano scelto
|
||||||
|
$st = $pdo->prepare("SELECT id, name, description, price FROM subscription_plans WHERE id = ? AND is_active = 1");
|
||||||
|
$st->execute([$plan_id]);
|
||||||
|
$plan = $st->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$plan) {
|
||||||
|
header('Location: pricing.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$monthly = (float)$plan['price'];
|
||||||
|
$total = $cycle === 'yearly' ? $monthly * 11 : $monthly; // annuale = 11 mesi
|
||||||
|
$cycleLabel = $cycle === 'yearly' ? 'Annuale (1 mese gratis)' : 'Mensile';
|
||||||
|
|
||||||
|
// Ripopola i campi in caso di errore (tornano da checkout_process)
|
||||||
|
$old = $_SESSION['checkout_old'] ?? [];
|
||||||
|
unset($_SESSION['checkout_old']);
|
||||||
|
function old(string $k, array $old): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($old[$k] ?? '', ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
$flash = getFlash();
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html class="no-js" lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<?php include('tophead.php'); ?>
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<style>
|
||||||
|
.co-wrap {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 40px auto 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-title {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-sub {
|
||||||
|
text-align: center;
|
||||||
|
color: #6b6480;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 340px;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.co-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid #f0e6ee;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-card h3 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-card h3 .n {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-card .hint {
|
||||||
|
color: #9a93ac;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 0 18px 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-field {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-field label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4a4458;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-field input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px 13px;
|
||||||
|
border: 1.5px solid #eee3ec;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-field input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #e6007e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-row-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
|
||||||
|
.co-row,
|
||||||
|
.co-row-3 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.req {
|
||||||
|
color: #e6007e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Riepilogo sticky */
|
||||||
|
.co-summary {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
background: linear-gradient(160deg, #fff 60%, #fff5f9);
|
||||||
|
border: 1.5px solid #ffd9e8;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-summary h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
color: #9a93ac;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-plan-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-plan-cycle {
|
||||||
|
color: #ff6a3d;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4a4458;
|
||||||
|
border-bottom: 1px dashed #f0e6ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-total {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-total .lbl {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-total .amt {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-pay-note {
|
||||||
|
background: #fff8e6;
|
||||||
|
border: 1px solid #ffe4a3;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: #9a7a1a;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-btn {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 12px 26px rgba(230, 0, 126, .28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-btn:hover {
|
||||||
|
opacity: .94;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-secure {
|
||||||
|
text-align: center;
|
||||||
|
color: #9a93ac;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.co-alert {
|
||||||
|
background: #fdecef;
|
||||||
|
border: 1px solid #f5c2cd;
|
||||||
|
color: #b3143a;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<?php include('preloader.php'); ?>
|
||||||
|
<?php include('offcanvas.php'); ?>
|
||||||
|
<header class="tp-header-height"><?php include('topbar.php'); ?></header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
<div class="co-wrap">
|
||||||
|
<h1 class="co-title">Completa il tuo abbonamento</h1>
|
||||||
|
<p class="co-sub">Crea il tuo account e attiva il salone in un solo passaggio.</p>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="co-alert"><?= htmlspecialchars($flash['text']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="checkout_process.php">
|
||||||
|
<input type="hidden" name="plan_id" value="<?= (int)$plan['id'] ?>">
|
||||||
|
<input type="hidden" name="cycle" value="<?= $cycle ?>">
|
||||||
|
|
||||||
|
<div class="co-grid">
|
||||||
|
<!-- Colonna form -->
|
||||||
|
<div>
|
||||||
|
<!-- 1. Account -->
|
||||||
|
<div class="co-card mb-3">
|
||||||
|
<h3><span class="n">1</span> Il tuo account</h3>
|
||||||
|
<p class="hint">Con questi dati accederai alla tua area titolare.</p>
|
||||||
|
<div class="co-row">
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Nome <span class="req">*</span></label>
|
||||||
|
<input type="text" name="first_name" value="<?= old('first_name', $old) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Cognome <span class="req">*</span></label>
|
||||||
|
<input type="text" name="last_name" value="<?= old('last_name', $old) ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Email <span class="req">*</span></label>
|
||||||
|
<input type="email" name="email" value="<?= old('email', $old) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="co-row">
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Password <span class="req">*</span></label>
|
||||||
|
<input type="password" name="password" minlength="8" required>
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Conferma password <span class="req">*</span></label>
|
||||||
|
<input type="password" name="password_confirm" minlength="8" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Salone -->
|
||||||
|
<div class="co-card mb-3">
|
||||||
|
<h3><span class="n">2</span> Il tuo salone</h3>
|
||||||
|
<p class="hint">Il nome con cui i clienti ti troveranno.</p>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Nome del salone <span class="req">*</span></label>
|
||||||
|
<input type="text" name="shop_name" value="<?= old('shop_name', $old) ?>" placeholder="Es. Salone Bellezza Anna" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Dati fiscali -->
|
||||||
|
<div class="co-card">
|
||||||
|
<h3><span class="n">3</span> Dati di fatturazione</h3>
|
||||||
|
<p class="hint">Necessari per emettere la fattura dell'abbonamento.</p>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Ragione sociale <span class="req">*</span></label>
|
||||||
|
<input type="text" name="business_name" value="<?= old('business_name', $old) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="co-row">
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Partita IVA <span class="req">*</span></label>
|
||||||
|
<input type="text" name="vat_number" value="<?= old('vat_number', $old) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Codice Fiscale</label>
|
||||||
|
<input type="text" name="tax_code" value="<?= old('tax_code', $old) ?>">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Indirizzo <span class="req">*</span></label>
|
||||||
|
<input type="text" name="address" value="<?= old('address', $old) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="co-row-3">
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Città <span class="req">*</span></label>
|
||||||
|
<input type="text" name="city" value="<?= old('city', $old) ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Prov.</label>
|
||||||
|
<input type="text" name="province" value="<?= old('province', $old) ?>" maxlength="2">
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>CAP <span class="req">*</span></label>
|
||||||
|
<input type="text" name="zip_code" value="<?= old('zip_code', $old) ?>" required maxlength="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="co-row">
|
||||||
|
<div class="co-field">
|
||||||
|
<label>PEC <span class="req">*</span></label>
|
||||||
|
<input type="email" name="pec" value="<?= old('pec', $old) ?>" placeholder="oppure compila SDI">
|
||||||
|
</div>
|
||||||
|
<div class="co-field">
|
||||||
|
<label>Codice SDI <span class="req">*</span></label>
|
||||||
|
<input type="text" name="sdi_code" value="<?= old('sdi_code', $old) ?>" maxlength="7" placeholder="oppure compila PEC">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="hint" style="margin-left:0;">Per la fattura elettronica è obbligatorio almeno uno tra PEC e Codice SDI.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna riepilogo -->
|
||||||
|
<div>
|
||||||
|
<div class="co-summary">
|
||||||
|
<h4>Riepilogo ordine</h4>
|
||||||
|
<div class="co-plan-name"><?= htmlspecialchars($plan['name']) ?></div>
|
||||||
|
<div class="co-plan-cycle"><?= $cycleLabel ?></div>
|
||||||
|
|
||||||
|
<div class="co-line"><span>Prezzo mensile</span><span>€<?= euro($monthly) ?></span></div>
|
||||||
|
<?php if ($cycle === 'yearly'): ?>
|
||||||
|
<div class="co-line"><span>Durata</span><span>12 mesi</span></div>
|
||||||
|
<div class="co-line"><span>Sconto</span><span style="color:#ff6a3d;">−€<?= euro($monthly) ?></span></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="co-line"><span>Durata</span><span>1 mese</span></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="co-total">
|
||||||
|
<span class="lbl">Totale</span>
|
||||||
|
<span class="amt">€<?= euro($total) ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="co-pay-note">
|
||||||
|
<i class="fa-solid fa-flask"></i> Pagamento <strong>simulato</strong>: nessun addebito reale. L'integrazione con Stripe arriverà a breve.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="co-btn">Attiva abbonamento</button>
|
||||||
|
<p class="co-secure"><i class="fa-solid fa-lock"></i> Dati protetti · disdici quando vuoi</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer><?php include('footer.php'); ?></footer>
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
// checkout_process.php — Elabora il checkout: crea utente Owner + salone + profilo fiscale
|
||||||
|
// + abbonamento attivo, "paga" (SIMULATO), fa login e porta in area titolare.
|
||||||
|
// Tutto in transazione: o va a buon fine tutto, o non viene creato nulla.
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
// Ruolo Owner (da headscript: ROLE_OWNER = 2)
|
||||||
|
const ROLE_OWNER = 2;
|
||||||
|
|
||||||
|
function setFlash(string $type, string $text): void
|
||||||
|
{
|
||||||
|
$_SESSION['flash'] = ['type' => $type, 'text' => $text];
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToCheckout(array $post, string $msg): void
|
||||||
|
{
|
||||||
|
// Ripopola il form e torna al checkout con l'errore
|
||||||
|
$_SESSION['checkout_old'] = $post;
|
||||||
|
setFlash('danger', $msg);
|
||||||
|
$plan = (int)($post['plan_id'] ?? 0);
|
||||||
|
$cycle = ($post['cycle'] ?? 'monthly') === 'yearly' ? 'yearly' : 'monthly';
|
||||||
|
header("Location: checkout.php?plan={$plan}&cycle={$cycle}");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
header('Location: pricing.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = $_POST;
|
||||||
|
|
||||||
|
// --- Raccolta dati ---
|
||||||
|
$plan_id = (int)($post['plan_id'] ?? 0);
|
||||||
|
$cycle = ($post['cycle'] ?? 'monthly') === 'yearly' ? 'yearly' : 'monthly';
|
||||||
|
|
||||||
|
$first_name = trim($post['first_name'] ?? '');
|
||||||
|
$last_name = trim($post['last_name'] ?? '');
|
||||||
|
$email = trim($post['email'] ?? '');
|
||||||
|
$password = (string)($post['password'] ?? '');
|
||||||
|
$password2 = (string)($post['password_confirm'] ?? '');
|
||||||
|
|
||||||
|
$shop_name = trim($post['shop_name'] ?? '');
|
||||||
|
|
||||||
|
$business_name = trim($post['business_name'] ?? '');
|
||||||
|
$vat_number = trim($post['vat_number'] ?? '');
|
||||||
|
$tax_code = trim($post['tax_code'] ?? '');
|
||||||
|
$address = trim($post['address'] ?? '');
|
||||||
|
$city = trim($post['city'] ?? '');
|
||||||
|
$province = trim($post['province'] ?? '');
|
||||||
|
$zip_code = trim($post['zip_code'] ?? '');
|
||||||
|
$pec = trim($post['pec'] ?? '');
|
||||||
|
$sdi_code = trim($post['sdi_code'] ?? '');
|
||||||
|
|
||||||
|
// --- Validazioni ---
|
||||||
|
if ($plan_id <= 0) {
|
||||||
|
header('Location: pricing.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($first_name === '' || $last_name === '' || $email === '' || $password === '') {
|
||||||
|
backToCheckout($post, 'Compila tutti i campi obbligatori dell\'account.');
|
||||||
|
}
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
backToCheckout($post, 'Inserisci un indirizzo email valido.');
|
||||||
|
}
|
||||||
|
if (strlen($password) < 8) {
|
||||||
|
backToCheckout($post, 'La password deve avere almeno 8 caratteri.');
|
||||||
|
}
|
||||||
|
if ($password !== $password2) {
|
||||||
|
backToCheckout($post, 'Le due password non coincidono.');
|
||||||
|
}
|
||||||
|
if ($shop_name === '') {
|
||||||
|
backToCheckout($post, 'Inserisci il nome del salone.');
|
||||||
|
}
|
||||||
|
if ($business_name === '' || $vat_number === '' || $address === '' || $city === '' || $zip_code === '') {
|
||||||
|
backToCheckout($post, 'Compila tutti i dati di fatturazione obbligatori.');
|
||||||
|
}
|
||||||
|
// Normativa IT: almeno uno tra PEC e SDI
|
||||||
|
if ($pec === '' && $sdi_code === '') {
|
||||||
|
backToCheckout($post, 'Per la fattura elettronica inserisci almeno la PEC o il Codice SDI.');
|
||||||
|
}
|
||||||
|
if ($pec !== '' && !filter_var($pec, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
backToCheckout($post, 'La PEC non è un indirizzo valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Email già registrata?
|
||||||
|
$chk = $pdo->prepare("SELECT id FROM auth_users WHERE email = ?");
|
||||||
|
$chk->execute([$email]);
|
||||||
|
if ($chk->fetch()) {
|
||||||
|
backToCheckout($post, 'Esiste già un account con questa email. Prova ad accedere.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piano valido + prezzo
|
||||||
|
$ps = $pdo->prepare("SELECT id, price FROM subscription_plans WHERE id = ? AND is_active = 1");
|
||||||
|
$ps->execute([$plan_id]);
|
||||||
|
$plan = $ps->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$plan) {
|
||||||
|
backToCheckout($post, 'Il piano selezionato non è più disponibile.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slug salone univoco (generazione al volo, finché non c'è il sistema slug dedicato)
|
||||||
|
$baseSlug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', $shop_name), '-'));
|
||||||
|
if ($baseSlug === '') $baseSlug = 'salone';
|
||||||
|
$slug = $baseSlug;
|
||||||
|
$i = 1;
|
||||||
|
$slugChk = $pdo->prepare("SELECT id FROM shops WHERE slug = ?");
|
||||||
|
while (true) {
|
||||||
|
$slugChk->execute([$slug]);
|
||||||
|
if (!$slugChk->fetch()) break;
|
||||||
|
$slug = $baseSlug . '-' . (++$i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durata abbonamento
|
||||||
|
$expires = $cycle === 'yearly'
|
||||||
|
? date('Y-m-d', strtotime('+1 year'))
|
||||||
|
: date('Y-m-d', strtotime('+1 month'));
|
||||||
|
|
||||||
|
// ---- TRANSAZIONE: tutto o niente ----
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Utente Owner
|
||||||
|
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
$insU = $pdo->prepare("
|
||||||
|
INSERT INTO auth_users (email, password, role_id, status, first_name, last_name, created_at)
|
||||||
|
VALUES (?, ?, ?, 'Active', ?, ?, NOW())
|
||||||
|
");
|
||||||
|
$insU->execute([$email, $hash, ROLE_OWNER, $first_name, $last_name]);
|
||||||
|
$user_id = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
// 2. Salone (indirizzo iniziale = indirizzo fiscale; modificabile poi dal profilo)
|
||||||
|
$insS = $pdo->prepare("
|
||||||
|
INSERT INTO shops (owner_id, name, slug, address, city, province, zip_code, email, active, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, NOW())
|
||||||
|
");
|
||||||
|
$insS->execute([$user_id, $shop_name, $slug, $address, $city, ($province ?: null), $zip_code, $email]);
|
||||||
|
$shop_id = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
// 3. Profilo fiscale
|
||||||
|
$insB = $pdo->prepare("
|
||||||
|
INSERT INTO billing_profiles
|
||||||
|
(user_id, shop_id, business_name, vat_number, tax_code, address, zip_code, city, province, pec, sdi_code)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$insB->execute([
|
||||||
|
$user_id,
|
||||||
|
$shop_id,
|
||||||
|
$business_name,
|
||||||
|
$vat_number,
|
||||||
|
($tax_code ?: null),
|
||||||
|
$address,
|
||||||
|
$zip_code,
|
||||||
|
$city,
|
||||||
|
($province ?: null),
|
||||||
|
($pec ?: null),
|
||||||
|
($sdi_code ?: null)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. Abbonamento attivo (pagamento SIMULATO riuscito)
|
||||||
|
$insSub = $pdo->prepare("
|
||||||
|
INSERT INTO shop_subscriptions (shop_id, plan_id, status, started_at, expires_at)
|
||||||
|
VALUES (?, ?, 'active', CURDATE(), ?)
|
||||||
|
");
|
||||||
|
$insSub->execute([$shop_id, $plan_id, $expires]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
// --- Login: impostiamo la sessione userarea come fa il resto del sito ---
|
||||||
|
// NB: headscript usa Auth::user() (Laravel). Qui creiamo comunque le chiavi
|
||||||
|
// di sessione PHP native usate nelle pagine userarea, così l'utente è "dentro".
|
||||||
|
$_SESSION['iduserlogin'] = $user_id;
|
||||||
|
$_SESSION['nameuser'] = $first_name;
|
||||||
|
$_SESSION['surnameuser'] = $last_name;
|
||||||
|
$_SESSION['emailuser'] = $email;
|
||||||
|
|
||||||
|
setFlash('success', 'Benvenuto! Il tuo salone è attivo. Completa il profilo e inizia a ricevere prenotazioni.');
|
||||||
|
// Va in area titolare (l'agenda). Se la sessione Laravel non è attiva,
|
||||||
|
// la pagina rimanderà al login: in tal caso l'utente accede con le sue credenziali.
|
||||||
|
header('Location: userarea/appointments.php');
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
error_log('checkout_process: ' . $e->getMessage());
|
||||||
|
backToCheckout($post, 'Si è verificato un errore durante l\'attivazione. Riprova.');
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cron_subscriptions.php — Da eseguire UNA VOLTA AL GIORNO.
|
||||||
|
*
|
||||||
|
* Cosa fa:
|
||||||
|
* - Trova gli abbonamenti "active" la cui expires_at è già passata → li mette "expired"
|
||||||
|
* e spegne il salone (shops.active = 0).
|
||||||
|
* - Riaccende i saloni il cui abbonamento è tornato valido (active e non scaduto)
|
||||||
|
* ma che risultano ancora spenti (es. dopo un rinnovo registrato).
|
||||||
|
*
|
||||||
|
* NON tocca i saloni "blocked" dall'admin: quelli restano spenti finché l'admin non li sblocca.
|
||||||
|
*
|
||||||
|
* Esecuzione (XAMPP / Windows Task Scheduler):
|
||||||
|
* C:\xampp\php\php.exe C:\xampp\htdocs\comelifacciamo\public\cron_subscriptions.php
|
||||||
|
* Esecuzione (cron Linux, ogni giorno alle 3:00):
|
||||||
|
* 0 3 * * * /usr/bin/php /percorso/public/cron_subscriptions.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Percorso corretto: questo file sta in public/, la classe DB è in public/userarea/class/
|
||||||
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$log = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// 1. Scaduti: erano active, la data è passata → expired + salone spento
|
||||||
|
$exp = $pdo->prepare("
|
||||||
|
SELECT shop_id FROM shop_subscriptions
|
||||||
|
WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at < ?
|
||||||
|
");
|
||||||
|
$exp->execute([$today]);
|
||||||
|
$expiredShops = $exp->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
if ($expiredShops) {
|
||||||
|
$updSub = $pdo->prepare("
|
||||||
|
UPDATE shop_subscriptions SET status = 'expired'
|
||||||
|
WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at < ?
|
||||||
|
");
|
||||||
|
$updSub->execute([$today]);
|
||||||
|
|
||||||
|
// Spegni i saloni relativi
|
||||||
|
$in = implode(',', array_fill(0, count($expiredShops), '?'));
|
||||||
|
$updShop = $pdo->prepare("UPDATE shops SET active = 0 WHERE id IN ($in)");
|
||||||
|
$updShop->execute($expiredShops);
|
||||||
|
|
||||||
|
$log[] = count($expiredShops) . " salone/i scaduto/i e disattivato/i.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Riattivazione: abbonamento tornato valido ma salone ancora spento.
|
||||||
|
// (utile se un rinnovo ha rimesso status=active ma active era 0)
|
||||||
|
$rea = $pdo->prepare("
|
||||||
|
SELECT s.id
|
||||||
|
FROM shops s
|
||||||
|
JOIN shop_subscriptions sub ON sub.shop_id = s.id
|
||||||
|
WHERE s.active = 0
|
||||||
|
AND sub.status = 'active'
|
||||||
|
AND (sub.expires_at IS NULL OR sub.expires_at >= ?)
|
||||||
|
");
|
||||||
|
$rea->execute([$today]);
|
||||||
|
$reactivate = $rea->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
if ($reactivate) {
|
||||||
|
$in = implode(',', array_fill(0, count($reactivate), '?'));
|
||||||
|
$updOn = $pdo->prepare("UPDATE shops SET active = 1 WHERE id IN ($in)");
|
||||||
|
$updOn->execute($reactivate);
|
||||||
|
$log[] = count($reactivate) . " salone/i riattivato/i.";
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
if (empty($log)) $log[] = "Nessuna modifica.";
|
||||||
|
$line = '[' . date('Y-m-d H:i:s') . '] ' . implode(' ', $log) . PHP_EOL;
|
||||||
|
echo $line;
|
||||||
|
// Log su file accanto allo script
|
||||||
|
@file_put_contents(__DIR__ . '/cron_subscriptions.log', $line, FILE_APPEND);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
$err = '[' . date('Y-m-d H:i:s') . '] ERRORE: ' . $e->getMessage() . PHP_EOL;
|
||||||
|
echo $err;
|
||||||
|
@file_put_contents(__DIR__ . '/cron_subscriptions.log', $err, FILE_APPEND);
|
||||||
|
}
|
||||||
+87
-4
@@ -8,18 +8,101 @@
|
|||||||
<link rel="stylesheet" href="assets/css/spacing.css">
|
<link rel="stylesheet" href="assets/css/spacing.css">
|
||||||
<link rel="stylesheet" href="assets/css/main.css">
|
<link rel="stylesheet" href="assets/css/main.css">
|
||||||
<style>
|
<style>
|
||||||
/* Cuore preferito attivo → rosso pieno */
|
/* ===== Palette HairBook (dal logo) ===== */
|
||||||
|
:root {
|
||||||
|
--hb-magenta: #e6007e;
|
||||||
|
/* fucsia del logo */
|
||||||
|
--hb-pink: #e11d6b;
|
||||||
|
/* accento cuori / hover */
|
||||||
|
--hb-purple: #7b2d8e;
|
||||||
|
/* viola */
|
||||||
|
--hb-dark: #2d1b4e;
|
||||||
|
/* viola scuro (testi/topbar) */
|
||||||
|
--hb-grad: linear-gradient(90deg, #e6007e 0%, #7b2d8e 60%, #2d1b4e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Cuore preferito ===== */
|
||||||
.js-fav.is-active i,
|
.js-fav.is-active i,
|
||||||
.js-fav.is-active .fa-heart {
|
.js-fav.is-active .fa-heart {
|
||||||
color: #e11d6b;
|
color: var(--hb-pink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (opzionale) cuore non attivo un po' più tenue + rosso al passaggio del mouse */
|
|
||||||
.js-fav i {
|
.js-fav i {
|
||||||
transition: color .15s ease;
|
transition: color .15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.js-fav:hover i {
|
.js-fav:hover i {
|
||||||
color: #e11d6b;
|
color: var(--hb-pink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Topbar ===== */
|
||||||
|
.tp-header-area,
|
||||||
|
.tp-header-height .tp-header-area {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* voci di menu */
|
||||||
|
.tp-main-menu ul li a {
|
||||||
|
color: var(--hb-dark);
|
||||||
|
transition: color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-main-menu ul li a:hover,
|
||||||
|
.tp-main-menu ul li.active>a {
|
||||||
|
color: var(--hb-magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* icone nella topbar (ricerca, utente, carrello…) */
|
||||||
|
.tp-header-action a,
|
||||||
|
.tp-header-action button,
|
||||||
|
.tp-header-action i {
|
||||||
|
color: var(--hb-dark);
|
||||||
|
transition: color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-header-action a:hover,
|
||||||
|
.tp-header-action button:hover,
|
||||||
|
.tp-header-action a:hover i {
|
||||||
|
color: var(--hb-magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Bottoni principali ===== */
|
||||||
|
.tp-btn,
|
||||||
|
.tp-btn-sm,
|
||||||
|
.tp-btn-border:hover {
|
||||||
|
background: var(--hb-magenta);
|
||||||
|
border-color: var(--hb-magenta);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-btn:hover,
|
||||||
|
.tp-btn-sm:hover {
|
||||||
|
background: var(--hb-purple);
|
||||||
|
border-color: var(--hb-purple);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Link e accenti generici ===== */
|
||||||
|
a {
|
||||||
|
color: var(--hb-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--hb-magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Icone accento sparse (location, telefono nelle card) ===== */
|
||||||
|
.tp-tour-info i,
|
||||||
|
.tp-tour-media-meta i {
|
||||||
|
color: var(--hb-magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Offcanvas / menu mobile ===== */
|
||||||
|
.tp-offcanvas-area .tp-main-menu-mobile ul li a {
|
||||||
|
color: var(--hb-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-offcanvas-area .tp-main-menu-mobile ul li a:hover {
|
||||||
|
color: var(--hb-magenta);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+181
-186
@@ -1,194 +1,189 @@
|
|||||||
<div class="tp-footer-area z-index-2 pt-70 p-relative" data-bg-color="#EBF8EB">
|
<style>
|
||||||
<img class="tp-footer-shape" src="assets/img/subscribe/shape.png" alt="">
|
/* Footer HairBook su fondo chiaro */
|
||||||
<img class="tp-footer-shape-2" src="assets/img/subscribe/shape-2.png" alt="">
|
.tp-footer-area {
|
||||||
|
color: #4a4458;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-widget-title {
|
||||||
|
color: #2b2540 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-dec,
|
||||||
|
.tp-footer-widget-menu ul li a,
|
||||||
|
.tp-footer-contact a,
|
||||||
|
.tp-footer-contact .support,
|
||||||
|
.tp-copyright-text p,
|
||||||
|
.tp-copyright-text a {
|
||||||
|
color: #4a4458;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-widget-menu ul li a {
|
||||||
|
transition: color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-widget-menu ul li a:hover,
|
||||||
|
.tp-footer-contact a:hover,
|
||||||
|
.tp-copyright-payment a:hover {
|
||||||
|
color: #e6007e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-contact a.phone {
|
||||||
|
color: #e6007e;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-copyright-payment a {
|
||||||
|
color: #7a7392;
|
||||||
|
transition: color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-social a {
|
||||||
|
color: #2b2540;
|
||||||
|
transition: color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-social a:hover {
|
||||||
|
color: #ff6a3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-subscribe-title {
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-subscribe-btn {
|
||||||
|
background: #e6007e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tp-footer-subscribe-btn:hover {
|
||||||
|
background: #ff6a3d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="tp-footer-area z-index-2 pt-70 p-relative" data-bg-color="#FFF4F8">
|
||||||
|
<img class="tp-footer-shape" src="assets/img/subscribe/shape.png" alt="">
|
||||||
|
<img class="tp-footer-shape-2" src="assets/img/subscribe/shape-2.png" alt="">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- ===== Newsletter ===== -->
|
||||||
|
<div class="tp-footer-subscribe-wrap pb-60">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-xxl-6 col-xl-7 col-lg-8 col-md-9">
|
||||||
|
<div class="tp-footer-subscribe-inner text-center">
|
||||||
|
<h2 class="tp-footer-subscribe-title fw-500 mb-30">Iscriviti e ricevi le offerte dei saloni vicino a te!</h2>
|
||||||
|
<div class="tp-footer-subscribe-form p-relative">
|
||||||
|
<form action="#" class="mb-20 p-relative">
|
||||||
|
<span class="tp-footer-subscribe-icon">
|
||||||
|
<svg width="17" height="14" viewBox="0 0 17 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.1499 0.649902H14.1499C14.9749 0.649902 15.6499 1.3249 15.6499 2.1499V11.1499C15.6499 11.9749 14.9749 12.6499 14.1499 12.6499H2.1499C1.3249 12.6499 0.649902 11.9749 0.649902 11.1499V2.1499C0.649902 1.3249 1.3249 0.649902 2.1499 0.649902Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M15.6499 2.1499L8.1499 7.3999L0.649902 2.1499" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input class="tp-input" type="email" placeholder="La tua email ...">
|
||||||
|
<button class="tp-footer-subscribe-btn" type="submit">Iscriviti</button>
|
||||||
|
</form>
|
||||||
|
<p>Rispettiamo la tua <a href="privacy.php">privacy</a>. Niente spam, solo novità utili.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== Widget / colonne ===== -->
|
||||||
|
<div class="tp-footer-widget-wrap pt-75 pb-65">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Colonna brand -->
|
||||||
|
<div class="col-xxl-4 col-xl-4 col-lg-4 col-md-6 col-sm-6">
|
||||||
|
<div class="tp-footer-widget tp-footer-col-1 mb-30">
|
||||||
|
<div class="tp-footer-logo mb-20">
|
||||||
|
<a href="search.php">
|
||||||
|
<img data-width="150" src="assets/img/logo/logo.png" alt="Come Li Facciamo">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="tp-footer-dec">Prenota il tuo parrucchiere di fiducia in pochi tap.<br>
|
||||||
|
Scegli il salone, l'operatore e l'orario:<br>
|
||||||
|
al resto pensiamo noi.</p>
|
||||||
|
<div class="tp-footer-social">
|
||||||
|
<a href="#" aria-label="Facebook">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="18" viewBox="0 0 12 18" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.62839 7.77713C0.911363 7.77713 0.761719 7.91782 0.761719 8.59194V9.81416C0.761719 10.4883 0.911363 10.629 1.62839 10.629H3.36172V15.5179C3.36172 16.192 3.51136 16.3327 4.22839 16.3327H5.96172C6.67874 16.3327 6.82839 16.192 6.82839 15.5179V10.629H8.77466C9.31846 10.629 9.45859 10.5296 9.60798 10.038L9.97941 8.81579C10.2353 7.97368 10.0776 7.77713 9.14609 7.77713H6.82839V5.74009C6.82839 5.29008 7.21641 4.92527 7.69505 4.92527H10.1617C10.8787 4.92527 11.0284 4.78458 11.0284 4.11046V2.48083C11.0284 1.80671 10.8787 1.66602 10.1617 1.66602H7.69505C5.30182 1.66602 3.36172 3.49004 3.36172 5.74009V7.77713H1.62839Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="#" aria-label="Instagram">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.66602 8.99935C1.66602 5.54238 1.66602 3.8139 2.73996 2.73996C3.8139 1.66602 5.54238 1.66602 8.99935 1.66602C12.4563 1.66602 14.1848 1.66602 15.2587 2.73996C16.3327 3.8139 16.3327 5.54238 16.3327 8.99935C16.3327 12.4563 16.3327 14.1848 15.2587 15.2587C14.1848 16.3327 12.4563 16.3327 8.99935 16.3327C5.54238 16.3327 3.8139 16.3327 2.73996 15.2587C1.66602 14.1848 1.66602 12.4563 1.66602 8.99935Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
||||||
|
<path d="M12.4747 9.00103C12.4747 10.9195 10.9195 12.4747 9.00103 12.4747C7.08256 12.4747 5.52734 10.9195 5.52734 9.00103C5.52734 7.08256 7.08256 5.52734 9.00103 5.52734C10.9195 5.52734 12.4747 7.08256 12.4747 9.00103Z" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M13.251 4.75391L13.242 4.75391" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna: Scopri -->
|
||||||
|
<div class="col-xxl-2 col-xl-2 col-lg-4 col-md-6 col-sm-6">
|
||||||
|
<div class="tp-footer-widget tp-footer-col-2 mb-30">
|
||||||
|
<h3 class="tp-footer-widget-title fw-600 mb-15">Scopri</h3>
|
||||||
|
<div class="tp-footer-widget-menu">
|
||||||
|
<ul>
|
||||||
|
<li><a href="search.php">Cerca saloni</a></li>
|
||||||
|
<li><a href="search-map.php">Saloni vicino a te</a></li>
|
||||||
|
<li><a href="how-it-works.php">Come funziona</a></li>
|
||||||
|
<li><a href="about.php">Chi siamo</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna: Per i saloni -->
|
||||||
|
<div class="col-xxl-3 col-xl-3 col-lg-4 col-md-6 col-sm-6">
|
||||||
|
<div class="tp-footer-widget tp-footer-col-3 mb-30">
|
||||||
|
<h3 class="tp-footer-widget-title fw-600 mb-15">Per i saloni</h3>
|
||||||
|
<div class="tp-footer-widget-menu">
|
||||||
|
<ul>
|
||||||
|
<li><a href="business.php">Sei un salone?</a></li>
|
||||||
|
<li><a href="userarea/login.php">Area titolari</a></li>
|
||||||
|
<li><a href="faq.php">Domande frequenti</a></li>
|
||||||
|
<li><a href="contact.php">Contatti</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna: Contatti -->
|
||||||
|
<div class="col-xxl-3 col-xl-3 col-lg-4 col-md-6 col-sm-6">
|
||||||
|
<div class="tp-footer-widget tp-footer-col-5 mb-30">
|
||||||
|
<h3 class="tp-footer-widget-title fw-600 mb-15">Contatti</h3>
|
||||||
|
<div class="tp-footer-contact">
|
||||||
|
<span class="support d-block mb-10">Scrivici :</span>
|
||||||
|
<a href="mailto:info@comelifacciamo.it" class="phone mb-20 d-inline-block">info@comelifacciamo.it</a>
|
||||||
|
<span class="support d-block mb-10">Assistenza :</span>
|
||||||
|
<a href="tel:+390000000000" class="phone">+39 000 000 0000</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== Copyright ===== -->
|
||||||
|
<div class="tp-copyright-area pt-25 pb-15">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="tp-footer-subscribe-wrap pb-60">
|
<div class="row align-items-center">
|
||||||
<div class="row justify-content-center">
|
<div class="col-lg-6">
|
||||||
<div class="col-xxl-5 col-xl-6 col-lg-7 col-md-9">
|
<div class="tp-copyright-text">
|
||||||
<div class="tp-footer-subscribe-inner text-center">
|
<p class="tp-ff-inter mb-5">© <?= date('Y') ?> <a href="search.php">Come Li Facciamo</a> — Tutti i diritti riservati</p>
|
||||||
<h2 class="tp-footer-subscribe-title fw-500 mb-30">Sign up now for amazing travel offers and deals!</h2>
|
|
||||||
<div class="tp-footer-subscribe-form p-relative">
|
|
||||||
<form action="#" class="mb-20 p-relative">
|
|
||||||
<span class="tp-footer-subscribe-icon">
|
|
||||||
<svg width="17" height="14" viewBox="0 0 17 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2.1499 0.649902H14.1499C14.9749 0.649902 15.6499 1.3249 15.6499 2.1499V11.1499C15.6499 11.9749 14.9749 12.6499 14.1499 12.6499H2.1499C1.3249 12.6499 0.649902 11.9749 0.649902 11.1499V2.1499C0.649902 1.3249 1.3249 0.649902 2.1499 0.649902Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
|
|
||||||
<path d="M15.6499 2.1499L8.1499 7.3999L0.649902 2.1499" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<input class="tp-input" type="text" placeholder="Enter your email ...">
|
|
||||||
<button class="tp-footer-subscribe-btn" type="submit">Subscribe</button>
|
|
||||||
</form>
|
|
||||||
<p>We are committed to protecting your <a href="#">privacy policy.</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-lg-6">
|
||||||
<div class="tp-footer-widget-wrap pt-75 pb-65">
|
<div class="tp-copyright-payment d-flex flex-wrap align-items-center justify-content-lg-end gap-3">
|
||||||
<div class="row">
|
<a href="privacy.php">Privacy Policy</a>
|
||||||
<div class="col-xxl-3 col-xl-3 col-lg-4 col-md-6 col-sm-6">
|
<span class="d-none d-sm-inline">·</span>
|
||||||
<div class="tp-footer-widget tp-footer-col-1 mb-30">
|
<a href="cookie.php">Cookie Policy</a>
|
||||||
<div class="tp-footer-logo mb-20">
|
|
||||||
<a href="index.html">
|
|
||||||
<img data-width="112" src="assets/img/logo/logo.png" alt="">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<p class="tp-footer-dec">This service has taken my business to a<br>
|
|
||||||
whole new level. The design and functionality<br>
|
|
||||||
and user friendly.</p>
|
|
||||||
<div class="tp-footer-social">
|
|
||||||
<a href="#">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="18" viewBox="0 0 12 18" fill="none">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.62839 7.77713C0.911363 7.77713 0.761719 7.91782 0.761719 8.59194V9.81416C0.761719 10.4883 0.911363 10.629 1.62839 10.629H3.36172V15.5179C3.36172 16.192 3.51136 16.3327 4.22839 16.3327H5.96172C6.67874 16.3327 6.82839 16.192 6.82839 15.5179V10.629H8.77466C9.31846 10.629 9.45859 10.5296 9.60798 10.038L9.97941 8.81579C10.2353 7.97368 10.0776 7.77713 9.14609 7.77713H6.82839V5.74009C6.82839 5.29008 7.21641 4.92527 7.69505 4.92527H10.1617C10.8787 4.92527 11.0284 4.78458 11.0284 4.11046V2.48083C11.0284 1.80671 10.8787 1.66602 10.1617 1.66602H7.69505C5.30182 1.66602 3.36172 3.49004 3.36172 5.74009V7.77713H1.62839Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="14" viewBox="0 0 16 14" fill="none">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.28884 0.714844H0.666992L6.14691 7.9153L1.01754 13.9556H3.38746L7.26697 9.38713L10.7118 13.9136H15.3337L9.69453 6.50391L9.70451 6.51669L14.5599 0.798959H12.19L8.58427 5.04503L5.28884 0.714844ZM3.21817 1.97588H4.65702L12.7825 12.6525H11.3436L3.21817 1.97588Z" fill="currentColor" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="9.99991" cy="9.99991" r="8.38077" stroke="currentColor" stroke-width="1.5" />
|
|
||||||
<path d="M18.3799 11.0604C17.6032 10.9148 16.8043 10.8389 15.9891 10.8389C11.5034 10.8389 7.51372 13.1373 4.9707 16.7054" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
|
||||||
<path d="M15.8665 4.13281C13.2437 7.2064 9.30255 9.16128 4.8957 9.16128C3.76828 9.16128 2.67133 9.03332 1.61914 8.79143" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
|
||||||
<path d="M12.1938 18.3815C12.4039 17.3641 12.5142 16.3104 12.5142 15.2309C12.5142 9.93756 9.86111 5.26259 5.80957 2.45801" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a href="#">
|
|
||||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1.66602 8.99935C1.66602 5.54238 1.66602 3.8139 2.73996 2.73996C3.8139 1.66602 5.54238 1.66602 8.99935 1.66602C12.4563 1.66602 14.1848 1.66602 15.2587 2.73996C16.3327 3.8139 16.3327 5.54238 16.3327 8.99935C16.3327 12.4563 16.3327 14.1848 15.2587 15.2587C14.1848 16.3327 12.4563 16.3327 8.99935 16.3327C5.54238 16.3327 3.8139 16.3327 2.73996 15.2587C1.66602 14.1848 1.66602 12.4563 1.66602 8.99935Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />
|
|
||||||
<path d="M12.4747 9.00103C12.4747 10.9195 10.9195 12.4747 9.00103 12.4747C7.08256 12.4747 5.52734 10.9195 5.52734 9.00103C5.52734 7.08256 7.08256 5.52734 9.00103 5.52734C10.9195 5.52734 12.4747 7.08256 12.4747 9.00103Z" stroke="currentColor" stroke-width="1.5" />
|
|
||||||
<path d="M13.251 4.75391L13.242 4.75391" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xxl-2 col-xl-2 col-lg-4 col-md-6 col-sm-6">
|
|
||||||
<div class="tp-footer-widget tp-footer-col-2 mb-30">
|
|
||||||
<h3 class="tp-footer-widget-title fw-600 mb-15">Company</h3>
|
|
||||||
<div class="tp-footer-widget-menu">
|
|
||||||
<ul>
|
|
||||||
<li><a href="#">About us</a></li>
|
|
||||||
<li><a href="#">Carrer</a></li>
|
|
||||||
<li><a href="#">Blog</a></li>
|
|
||||||
<li><a href="#">Partner</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xxl-2 col-xl-2 col-lg-4 col-md-4 col-sm-6">
|
|
||||||
<div class="tp-footer-widget tp-footer-col-3 mb-30">
|
|
||||||
<h3 class="tp-footer-widget-title fw-600 mb-15">Useful Links</h3>
|
|
||||||
<div class="tp-footer-widget-menu">
|
|
||||||
<ul>
|
|
||||||
<li><a href="#">Privacy Policy</a></li>
|
|
||||||
<li><a href="#">Terms & Conditions</a></li>
|
|
||||||
<li><a href="#">Register</a></li>
|
|
||||||
<li><a href="#">Stories</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xxl-3 col-xl-2 col-lg-4 col-md-4 col-sm-6">
|
|
||||||
<div class="tp-footer-widget tp-footer-col-4 mb-30">
|
|
||||||
<h3 class="tp-footer-widget-title fw-600 mb-15">Services</h3>
|
|
||||||
<div class="tp-footer-widget-menu">
|
|
||||||
<ul>
|
|
||||||
<li><a href="#">Tour Packages</a></li>
|
|
||||||
<li><a href="#">Travel Guides & Tips</a></li>
|
|
||||||
<li><a href="#">Local Experiences</a></li>
|
|
||||||
<li><a href="#">Travel Insurance</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xxl-2 col-xl-3 col-lg-4 col-md-4 col-sm-6">
|
|
||||||
<div class="tp-footer-widget tp-footer-col-5 mb-30">
|
|
||||||
<h3 class="tp-footer-widget-title fw-600 mb-15">Contact</h3>
|
|
||||||
<div class="tp-footer-contact">
|
|
||||||
<a href="https://www.google.com/maps" target="_blank" class="location mb-20 d-inline-block">4140 Parker Rd. Allentown New Mexico 31134</a>
|
|
||||||
<span class="support d-block mb-10">Supporter :</span>
|
|
||||||
<a href="mailto:+(704)555-0127" class="phone">+(704) 555-0127</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container-fluid container-1856">
|
|
||||||
<div class="tp-instagram-wrap">
|
|
||||||
<div class="swiper tp-instagram-slide">
|
|
||||||
<div class="swiper-wrapper slide-transtion">
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="tp-instagram-thumb p-relative">
|
|
||||||
<a href="#">
|
|
||||||
<img class="w-100" src="assets/img/footer/thumb.jpg" alt="instagram">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="tp-instagram-thumb p-relative">
|
|
||||||
<a href="#">
|
|
||||||
<img class="w-100" src="assets/img/footer/thumb-2.jpg" alt="instagram">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="tp-instagram-thumb p-relative">
|
|
||||||
<a href="#">
|
|
||||||
<img class="w-100" src="assets/img/footer/thumb-3.jpg" alt="instagram">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="tp-instagram-thumb p-relative">
|
|
||||||
<a href="#">
|
|
||||||
<img class="w-100" src="assets/img/footer/thumb-4.jpg" alt="instagram">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="tp-instagram-thumb p-relative">
|
|
||||||
<a href="#">
|
|
||||||
<img class="w-100" src="assets/img/footer/thumb-5.jpg" alt="instagram">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="tp-instagram-thumb p-relative">
|
|
||||||
<a href="#">
|
|
||||||
<img class="w-100" src="assets/img/footer/thumb-3.jpg" alt="instagram">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="swiper-slide">
|
|
||||||
<div class="tp-instagram-thumb p-relative">
|
|
||||||
<a href="#">
|
|
||||||
<img class="w-100" src="assets/img/footer/thumb-4.jpg" alt="instagram">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tp-copyright-area pt-25 pb-15">
|
|
||||||
<div class="container">
|
|
||||||
<div class="row align-items-center">
|
|
||||||
<div class="col-lg-5">
|
|
||||||
<div class="tp-copyright-text">
|
|
||||||
<p class="tp-ff-inter mb-5">2026 <a href="#">Themepure </a>© All rights reserved</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-7">
|
|
||||||
<div class="tp-copyright-payment d-flex flex-wrap align-items-center justify-content-lg-end">
|
|
||||||
<p class="fw-500 mb-5">Payment Channels :</p>
|
|
||||||
<a href="#"><img src="assets/img/footer/payment.png" alt=""></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
+758
@@ -0,0 +1,758 @@
|
|||||||
|
<?php
|
||||||
|
// index.php — Homepage HairBook (Come Li Facciamo)
|
||||||
|
// Hero + ricerca, VANTAGGI cliente, mappa Leaflet, griglia saloni, come funziona, CTA salone.
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$salons = $pdo->query("
|
||||||
|
SELECT sh.id, sh.name, sh.slug, sh.description, sh.address, sh.city, sh.province,
|
||||||
|
sh.latitude, sh.longitude, sh.logo, sh.cover_image,
|
||||||
|
(SELECT MIN(sv.price) FROM services sv WHERE sv.shop_id = sh.id AND sv.is_active = 1) AS min_price
|
||||||
|
FROM shops sh
|
||||||
|
WHERE sh.active = 1
|
||||||
|
ORDER BY sh.name ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$mapSalons = array_values(array_filter($salons, function ($s) {
|
||||||
|
return $s['latitude'] !== null && $s['longitude'] !== null;
|
||||||
|
}));
|
||||||
|
|
||||||
|
$cities = $pdo->query("
|
||||||
|
SELECT DISTINCT city FROM shops
|
||||||
|
WHERE active = 1 AND city IS NOT NULL AND city <> ''
|
||||||
|
ORDER BY city ASC
|
||||||
|
")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$favIds = [];
|
||||||
|
if (!empty($_SESSION['iduserlogin'])) {
|
||||||
|
$fstmt = $pdo->prepare("SELECT shop_id FROM user_favorite_shops WHERE user_id = ?");
|
||||||
|
$fstmt->execute([$_SESSION['iduserlogin']]);
|
||||||
|
$favIds = $fstmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapData = array_map(function ($s) {
|
||||||
|
return [
|
||||||
|
'name' => $s['name'],
|
||||||
|
'slug' => $s['slug'],
|
||||||
|
'lat' => (float)$s['latitude'],
|
||||||
|
'lng' => (float)$s['longitude'],
|
||||||
|
'city' => $s['city'],
|
||||||
|
'addr' => $s['address']
|
||||||
|
];
|
||||||
|
}, $mapSalons);
|
||||||
|
|
||||||
|
$totalSalons = count($salons);
|
||||||
|
function euro($v): string
|
||||||
|
{
|
||||||
|
return number_format((float)$v, 2, ',', '.');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html class="no-js" lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<?php include('tophead.php'); ?>
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<style>
|
||||||
|
.hm-accent {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-btn {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 34px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-btn-primary {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 12px 28px rgba(230, 0, 126, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-btn-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
opacity: .95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-btn-ghost {
|
||||||
|
background: #fff;
|
||||||
|
color: #e6007e;
|
||||||
|
border: 1.5px solid #ffd0e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-btn-ghost:hover {
|
||||||
|
background: #e6007e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HERO */
|
||||||
|
.hm-hero {
|
||||||
|
position: relative;
|
||||||
|
padding: 100px 0 70px;
|
||||||
|
background: linear-gradient(160deg, #fff5f9 0%, #fff 60%, #fff7f3 100%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-hero h1 {
|
||||||
|
font-size: 56px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
letter-spacing: -.03em;
|
||||||
|
line-height: 1.06;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-hero p {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #6b6480;
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto 38px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:575px) {
|
||||||
|
.hm-hero h1 {
|
||||||
|
font-size: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-hero p {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-search {
|
||||||
|
max-width: 680px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid #ffe0ee;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: 0 18px 50px rgba(230, 0, 126, .1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:600px) {
|
||||||
|
.hm-search {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-search select,
|
||||||
|
.hm-search input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #faf7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-search select:focus,
|
||||||
|
.hm-search input:focus {
|
||||||
|
outline: 2px solid #ffd0e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-search button {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-trust {
|
||||||
|
margin-top: 26px;
|
||||||
|
display: flex;
|
||||||
|
gap: 26px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
color: #8b8399;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-trust span i {
|
||||||
|
color: #1db954;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SEZIONE VANTAGGI (perché usarla) */
|
||||||
|
.hm-why {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-sec-head {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-sec-head h2 {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-sec-head p {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 18px;
|
||||||
|
max-width: 580px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-why-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 26px;
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:850px) {
|
||||||
|
.hm-why-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:560px) {
|
||||||
|
.hm-why-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-why-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid #f0e6ee;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 32px 26px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform .16s ease, box-shadow .16s ease, border-color .16s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-why-card:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
box-shadow: 0 24px 54px rgba(43, 37, 64, .12);
|
||||||
|
border-color: #ffd0e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-why-ico {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(135deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 14px 30px rgba(230, 0, 126, .26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-why-card h4 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-why-card p {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FASCIA gratis */
|
||||||
|
.hm-free {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
padding: 56px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-free h3 {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-free p {
|
||||||
|
color: rgba(255, 255, 255, .94);
|
||||||
|
font-size: 18px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SECTION generica */
|
||||||
|
.hm-sec {
|
||||||
|
padding: 70px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MAPPA */
|
||||||
|
.hm-map-wrap {
|
||||||
|
position: relative;
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hm-map {
|
||||||
|
height: 440px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1.5px solid #f0e6ee;
|
||||||
|
box-shadow: 0 20px 50px rgba(43, 37, 64, .1);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-map-geo {
|
||||||
|
margin-top: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-map-geo button {
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid #ffd0e4;
|
||||||
|
color: #e6007e;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-map-geo button:hover {
|
||||||
|
background: #e6007e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GRIGLIA saloni */
|
||||||
|
.hm-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0e6ee;
|
||||||
|
border-radius: 18px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform .16s ease, box-shadow .16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 22px 50px rgba(43, 37, 64, .13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-img {
|
||||||
|
position: relative;
|
||||||
|
height: 170px;
|
||||||
|
background: linear-gradient(135deg, #e6007e, #ff6a3d);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-img img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-fav {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, .92);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-fav i {
|
||||||
|
color: #e11d6b;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-body {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-body h4 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-loc {
|
||||||
|
color: #9a93ac;
|
||||||
|
font-size: 13.5px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-loc i {
|
||||||
|
color: #e6007e;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-price {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-price b {
|
||||||
|
color: #e6007e;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-btn {
|
||||||
|
background: #fff5f9;
|
||||||
|
color: #e6007e;
|
||||||
|
border: 1.5px solid #ffd9e8;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13.5px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-card-btn:hover {
|
||||||
|
background: #e6007e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COME FUNZIONA */
|
||||||
|
.hm-how {
|
||||||
|
background: #fff5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 28px;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:800px) {
|
||||||
|
.hm-steps {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-step {
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-step-ico {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(135deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 26px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 12px 26px rgba(230, 0, 126, .24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-step h4 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-step p {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 14.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA salone */
|
||||||
|
.hm-cta-salon {
|
||||||
|
background: #2b2540;
|
||||||
|
padding: 70px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-cta-salon h2 {
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hm-cta-salon p {
|
||||||
|
color: rgba(255, 255, 255, .8);
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<?php include('preloader.php'); ?>
|
||||||
|
<?php include('offcanvas.php'); ?>
|
||||||
|
<header class="tp-header-height"><?php include('topbar.php'); ?></header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<section class="hm-hero">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Il tuo parrucchiere,<br><span class="hm-accent">prenotato in un tap</span></h1>
|
||||||
|
<p>Trova i saloni migliori vicino a te, scegli servizio e orario, e prenota online in pochi secondi. Senza chiamare, a qualsiasi ora.</p>
|
||||||
|
<form class="hm-search" action="search.php" method="get">
|
||||||
|
<select name="city">
|
||||||
|
<option value="">Tutte le città</option>
|
||||||
|
<?php foreach ($cities as $c): ?>
|
||||||
|
<option value="<?= htmlspecialchars($c) ?>"><?= htmlspecialchars($c) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<input type="text" name="service" placeholder="Che servizio cerchi? (es. taglio)">
|
||||||
|
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i> Cerca</button>
|
||||||
|
</form>
|
||||||
|
<div class="hm-trust">
|
||||||
|
<span><i class="fa-solid fa-circle-check"></i> Gratis per te</span>
|
||||||
|
<span><i class="fa-solid fa-circle-check"></i> Nessuna registrazione per cercare</span>
|
||||||
|
<span><i class="fa-solid fa-circle-check"></i> Prenoti 24 ore su 24</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- PERCHÉ USARLA -->
|
||||||
|
<section class="hm-why">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hm-sec-head">
|
||||||
|
<h2>Perché prenotare con noi</h2>
|
||||||
|
<p>Dì addio alle telefonate e alle attese. Il tuo salone a portata di tap, quando vuoi tu.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-why-grid">
|
||||||
|
<div class="hm-why-card">
|
||||||
|
<div class="hm-why-ico"><i class="fa-solid fa-clock"></i></div>
|
||||||
|
<h4>Prenoti 24/7</h4>
|
||||||
|
<p>Anche di notte o nel weekend, quando il salone è chiuso. Scegli il momento, ci pensi quando puoi tu.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-why-card">
|
||||||
|
<div class="hm-why-ico"><i class="fa-solid fa-calendar-day"></i></div>
|
||||||
|
<h4>Disponibilità reali</h4>
|
||||||
|
<p>Vedi gli orari liberi in tempo reale e prenoti solo slot davvero disponibili. Niente telefonate a vuoto.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-why-card">
|
||||||
|
<div class="hm-why-ico"><i class="fa-solid fa-user-check"></i></div>
|
||||||
|
<h4>Il tuo parrucchiere preferito</h4>
|
||||||
|
<p>Scegli l'operatore da cui vuoi farti servire e ritrovalo a ogni prenotazione.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-why-card">
|
||||||
|
<div class="hm-why-ico"><i class="fa-solid fa-bell"></i></div>
|
||||||
|
<h4>Promemoria e storico</h4>
|
||||||
|
<p>Ricordi i tuoi appuntamenti a colpo d'occhio e riprenoti in un attimo quello che hai già fatto.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-why-card">
|
||||||
|
<div class="hm-why-ico"><i class="fa-solid fa-heart"></i></div>
|
||||||
|
<h4>I tuoi saloni preferiti</h4>
|
||||||
|
<p>Salva i saloni che ami e ritrovali subito, senza cercarli ogni volta.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-why-card">
|
||||||
|
<div class="hm-why-ico"><i class="fa-solid fa-location-dot"></i></div>
|
||||||
|
<h4>Vicino a te</h4>
|
||||||
|
<p>Scopri i saloni della tua zona sulla mappa e raggiungi quello più comodo.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FASCIA GRATIS -->
|
||||||
|
<section class="hm-free">
|
||||||
|
<h3>Per te è tutto gratis</h3>
|
||||||
|
<p>Cercare, confrontare e prenotare non ti costa nulla. Paghi solo il servizio, direttamente al salone.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MAPPA -->
|
||||||
|
<section class="hm-sec">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hm-sec-head">
|
||||||
|
<h2>Saloni vicino a te</h2>
|
||||||
|
<p>Esplora la mappa e scopri i parrucchieri nella tua zona.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-map-wrap">
|
||||||
|
<div id="hm-map"></div>
|
||||||
|
<div class="hm-map-geo">
|
||||||
|
<button type="button" id="hm-geo-btn"><i class="fa-solid fa-location-crosshairs"></i> Usa la mia posizione</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- GRIGLIA SALONI -->
|
||||||
|
<section class="hm-sec" style="padding-top:0;">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hm-sec-head">
|
||||||
|
<h2>Saloni in evidenza</h2>
|
||||||
|
<p><?= $totalSalons ?> salon<?= $totalSalons === 1 ? 'e' : 'i' ?> ti aspetta<?= $totalSalons === 1 ? '' : 'no' ?> online.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (empty($salons)): ?>
|
||||||
|
<p class="text-center text-muted">Ancora nessun salone disponibile. Torna presto!</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="hm-grid">
|
||||||
|
<?php foreach (array_slice($salons, 0, 8) as $s):
|
||||||
|
$cover = !empty($s['cover_image']) ? htmlspecialchars($s['cover_image']) : '';
|
||||||
|
$isFav = in_array($s['id'], $favIds);
|
||||||
|
$loc = trim(($s['city'] ?? '') . ($s['province'] ? ' (' . $s['province'] . ')' : ''));
|
||||||
|
?>
|
||||||
|
<div class="hm-card">
|
||||||
|
<div class="hm-card-img">
|
||||||
|
<?php if ($cover): ?><img src="<?= $cover ?>" alt="<?= htmlspecialchars($s['name']) ?>"><?php endif; ?>
|
||||||
|
<button class="hm-card-fav js-fav <?= $isFav ? 'is-active' : '' ?>" data-id="<?= (int)$s['id'] ?>" title="Preferito" type="button">
|
||||||
|
<i class="<?= $isFav ? 'fa-solid' : 'fa-regular' ?> fa-heart"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="hm-card-body">
|
||||||
|
<h4><?= htmlspecialchars($s['name']) ?></h4>
|
||||||
|
<div class="hm-card-loc"><i class="fa-solid fa-location-dot"></i><?= htmlspecialchars($loc ?: '—') ?></div>
|
||||||
|
<div class="hm-card-foot">
|
||||||
|
<div class="hm-card-price">
|
||||||
|
<?php if ($s['min_price'] !== null): ?>da <b>€<?= euro($s['min_price']) ?></b><?php else: ?><span class="text-muted">Servizi vari</span><?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<a href="salon.php?slug=<?= urlencode($s['slug']) ?>" class="hm-card-btn">Prenota</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php if ($totalSalons > 8): ?>
|
||||||
|
<div class="text-center mt-5">
|
||||||
|
<a href="search.php" class="hm-btn hm-btn-ghost">Vedi tutti i saloni</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- COME FUNZIONA -->
|
||||||
|
<section class="hm-sec hm-how">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hm-sec-head">
|
||||||
|
<h2>Prenotare è semplicissimo</h2>
|
||||||
|
<p>Tre passi e sei a posto.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-steps">
|
||||||
|
<div class="hm-step">
|
||||||
|
<div class="hm-step-ico"><i class="fa-solid fa-magnifying-glass"></i></div>
|
||||||
|
<h4>1. Cerca</h4>
|
||||||
|
<p>Trova il salone giusto per città, servizio o vicinanza sulla mappa.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-step">
|
||||||
|
<div class="hm-step-ico"><i class="fa-solid fa-calendar-check"></i></div>
|
||||||
|
<h4>2. Scegli</h4>
|
||||||
|
<p>Seleziona servizio, operatore e l'orario libero che preferisci.</p>
|
||||||
|
</div>
|
||||||
|
<div class="hm-step">
|
||||||
|
<div class="hm-step-ico"><i class="fa-solid fa-check"></i></div>
|
||||||
|
<h4>3. Prenota</h4>
|
||||||
|
<p>Confermi in pochi tap. Ritrovi tutto nella tua area personale.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA SALONE -->
|
||||||
|
<section class="hm-cta-salon">
|
||||||
|
<h2>Hai un salone?</h2>
|
||||||
|
<p>Porta le tue prenotazioni online e riempi l'agenda senza pensieri.</p>
|
||||||
|
<a href="business.php" class="hm-btn hm-btn-primary">Scopri come funziona</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer><?php include('footer.php'); ?></footer>
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script>
|
||||||
|
var salons = <?= json_encode($mapData, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
var defaultCenter = [45.4642, 9.1900];
|
||||||
|
var map = L.map('hm-map').setView(defaultCenter, 11);
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap',
|
||||||
|
maxZoom: 19
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
var markers = [];
|
||||||
|
salons.forEach(function(s) {
|
||||||
|
if (!s.lat || !s.lng) return;
|
||||||
|
var m = L.marker([s.lat, s.lng]).addTo(map);
|
||||||
|
m.bindPopup('<strong>' + s.name + '</strong><br>' + (s.addr ? s.addr + '<br>' : '') +
|
||||||
|
'<a href="salon.php?slug=' + encodeURIComponent(s.slug) + '" style="color:#e6007e;font-weight:700;">Vai al salone →</a>');
|
||||||
|
markers.push(m);
|
||||||
|
});
|
||||||
|
if (markers.length > 0) {
|
||||||
|
var group = L.featureGroup(markers);
|
||||||
|
map.fitBounds(group.getBounds().pad(0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('hm-geo-btn').addEventListener('click', function() {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
alert('Geolocalizzazione non supportata dal browser.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.geolocation.getCurrentPosition(function(pos) {
|
||||||
|
var lat = pos.coords.latitude,
|
||||||
|
lng = pos.coords.longitude;
|
||||||
|
map.setView([lat, lng], 13);
|
||||||
|
L.circleMarker([lat, lng], {
|
||||||
|
radius: 9,
|
||||||
|
color: '#e6007e',
|
||||||
|
fillColor: '#e6007e',
|
||||||
|
fillOpacity: .8
|
||||||
|
})
|
||||||
|
.addTo(map).bindPopup('Sei qui').openPopup();
|
||||||
|
}, function() {
|
||||||
|
alert('Non riusciamo a rilevare la tua posizione.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
<?php
|
||||||
|
// pricing.php — Pagina pubblica piani abbonamento (rivolta ai SALONI/titolari)
|
||||||
|
// Legge i piani reali da subscription_plans. Annuale = mensile x11 (1 mese gratis).
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/userarea/class/db-functions.php');
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
// Piani attivi, dal più economico al più caro
|
||||||
|
$plans = $pdo->query("
|
||||||
|
SELECT id, name, description, price, billing_period
|
||||||
|
FROM subscription_plans
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY price ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// --- Feature per piano (mappate per nome). Modificabili qui. ---
|
||||||
|
// Ogni voce: [testo, incluso(true/false)]. Le non incluse appaiono barrate.
|
||||||
|
$featuresByPlan = [
|
||||||
|
'Base' => [
|
||||||
|
['Prenotazioni online illimitate', true],
|
||||||
|
['Agenda e calendario appuntamenti', true],
|
||||||
|
['Gestione clienti del salone', true],
|
||||||
|
['Pagina salone pubblica + QR', true],
|
||||||
|
['1 operatore', true],
|
||||||
|
['Statistiche avanzate', false],
|
||||||
|
['Priorità nei risultati di ricerca', false],
|
||||||
|
],
|
||||||
|
'Pro' => [
|
||||||
|
['Prenotazioni online illimitate', true],
|
||||||
|
['Agenda e calendario appuntamenti', true],
|
||||||
|
['Gestione clienti del salone', true],
|
||||||
|
['Pagina salone pubblica + QR', true],
|
||||||
|
['Operatori illimitati', true],
|
||||||
|
['Statistiche avanzate', true],
|
||||||
|
['Priorità nei risultati di ricerca', true],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
// Fallback per piani senza mappa dedicata
|
||||||
|
$defaultFeatures = [
|
||||||
|
['Prenotazioni online', true],
|
||||||
|
['Agenda appuntamenti', true],
|
||||||
|
['Gestione clienti', true],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Qual è il piano "consigliato" (badge)? Il più caro tra gli attivi.
|
||||||
|
$recommendedId = !empty($plans) ? (int)$plans[count($plans) - 1]['id'] : 0;
|
||||||
|
|
||||||
|
function euro($v): string
|
||||||
|
{
|
||||||
|
return number_format((float)$v, 2, ',', '.');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html class="no-js" lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<?php include('tophead.php'); ?>
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<style>
|
||||||
|
.pr-hero {
|
||||||
|
padding: 90px 0 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-hero h1 {
|
||||||
|
font-size: 44px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-hero h1 .accent {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-hero p {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #6b6480;
|
||||||
|
max-width: 620px;
|
||||||
|
margin: 16px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle mensile/annuale */
|
||||||
|
.pr-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0e6ee;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px;
|
||||||
|
margin: 32px auto 10px;
|
||||||
|
box-shadow: 0 8px 30px rgba(230, 0, 126, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-toggle button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px 22px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6b6480;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s ease;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-toggle button.active {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 6px 16px rgba(230, 0, 126, .28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-save-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #fff0e8;
|
||||||
|
color: #ff6a3d;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Griglia piani */
|
||||||
|
.pr-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 26px;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 40px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-card {
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid #f0e6ee;
|
||||||
|
border-radius: 22px;
|
||||||
|
padding: 34px 30px;
|
||||||
|
transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-card:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
box-shadow: 0 24px 60px rgba(43, 37, 64, .12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-card.recommended {
|
||||||
|
border-color: #e6007e;
|
||||||
|
box-shadow: 0 24px 60px rgba(230, 0, 126, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -14px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 8px 20px rgba(230, 0, 126, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-name {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-desc {
|
||||||
|
color: #6b6480;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 6px 0 22px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-price {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-price .cur {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-price .amt {
|
||||||
|
font-size: 46px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #2b2540;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-price .per {
|
||||||
|
color: #9a93ac;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-price-sub {
|
||||||
|
color: #ff6a3d;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
min-height: 20px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-btn {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all .15s ease;
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-btn-primary {
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 10px 24px rgba(230, 0, 126, .28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-btn-primary:hover {
|
||||||
|
color: #fff;
|
||||||
|
opacity: .93;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-btn-ghost {
|
||||||
|
background: #fff5f9;
|
||||||
|
color: #e6007e;
|
||||||
|
border: 1.5px solid #ffd9e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-btn-ghost:hover {
|
||||||
|
background: #e6007e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-feat {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-feat li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14.5px;
|
||||||
|
color: #4a4458;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-feat li i {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-feat li.off {
|
||||||
|
color: #b7b1c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-check {
|
||||||
|
color: #1db954;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-cross {
|
||||||
|
color: #d9d3e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sezione benefici sotto */
|
||||||
|
.pr-why {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 90px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-why h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-why-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-why-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-why-ico {
|
||||||
|
width: 62px;
|
||||||
|
height: 62px;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #e6007e, #ff6a3d);
|
||||||
|
box-shadow: 0 12px 26px rgba(230, 0, 126, .24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-why-item h4 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-why-item p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b6480;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FAQ mini */
|
||||||
|
.pr-faq {
|
||||||
|
max-width: 760px;
|
||||||
|
margin: 80px auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-faq h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-faq details {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0e6ee;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 18px 22px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-faq summary {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2b2540;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-faq p {
|
||||||
|
color: #6b6480;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
font-size: 14.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-cta {
|
||||||
|
text-align: center;
|
||||||
|
margin: 80px auto 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-cta h2 {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-cta p {
|
||||||
|
color: #6b6480;
|
||||||
|
margin: 12px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
.pr-hero h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<?php include('preloader.php'); ?>
|
||||||
|
<?php include('offcanvas.php'); ?>
|
||||||
|
<header class="tp-header-height">
|
||||||
|
<?php include('topbar.php'); ?>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<div class="pr-hero">
|
||||||
|
<h1>Fai crescere il tuo salone con <span class="accent">Come Li Facciamo</span></h1>
|
||||||
|
<p>Prenotazioni online 24/7, agenda sempre in ordine e nuovi clienti che ti trovano. Scegli il piano su misura per la tua attività.</p>
|
||||||
|
|
||||||
|
<!-- Toggle -->
|
||||||
|
<div class="pr-toggle">
|
||||||
|
<button type="button" id="btnMonthly" class="active" onclick="setCycle('monthly')">Mensile</button>
|
||||||
|
<button type="button" id="btnYearly" onclick="setCycle('yearly')">Annuale <span class="pr-save-badge">1 mese gratis</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PIANI -->
|
||||||
|
<?php if (empty($plans)): ?>
|
||||||
|
<p class="text-center text-muted py-5">Nessun piano disponibile al momento.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="pr-grid">
|
||||||
|
<?php foreach ($plans as $p):
|
||||||
|
$isReco = ((int)$p['id'] === $recommendedId);
|
||||||
|
$monthly = (float)$p['price'];
|
||||||
|
$yearly = $monthly * 11; // 1 mese gratis
|
||||||
|
$yearlyPerMonth = $yearly / 12;
|
||||||
|
$feats = $featuresByPlan[$p['name']] ?? $defaultFeatures;
|
||||||
|
?>
|
||||||
|
<div class="pr-card <?= $isReco ? 'recommended' : '' ?>">
|
||||||
|
<?php if ($isReco): ?><span class="pr-badge">★ Più scelto</span><?php endif; ?>
|
||||||
|
|
||||||
|
<div class="pr-name"><?= htmlspecialchars($p['name']) ?></div>
|
||||||
|
<div class="pr-desc"><?= htmlspecialchars($p['description'] ?? '') ?></div>
|
||||||
|
|
||||||
|
<!-- Prezzo mensile -->
|
||||||
|
<div class="js-price-monthly">
|
||||||
|
<div class="pr-price">
|
||||||
|
<span class="cur">€</span>
|
||||||
|
<span class="amt"><?= euro($monthly) ?></span>
|
||||||
|
<span class="per">/mese</span>
|
||||||
|
</div>
|
||||||
|
<div class="pr-price-sub"> </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prezzo annuale (nascosto di default) -->
|
||||||
|
<div class="js-price-yearly" style="display:none;">
|
||||||
|
<div class="pr-price">
|
||||||
|
<span class="cur">€</span>
|
||||||
|
<span class="amt"><?= euro($yearlyPerMonth) ?></span>
|
||||||
|
<span class="per">/mese</span>
|
||||||
|
</div>
|
||||||
|
<div class="pr-price-sub">Fatturati €<?= euro($yearly) ?>/anno · risparmi €<?= euro($monthly) ?></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="checkout.php?plan=<?= (int)$p['id'] ?>&cycle=monthly"
|
||||||
|
class="js-plan-btn pr-btn <?= $isReco ? 'pr-btn-primary' : 'pr-btn-ghost' ?>">Inizia ora</a>
|
||||||
|
|
||||||
|
<ul class="pr-feat">
|
||||||
|
<?php foreach ($feats as $f): ?>
|
||||||
|
<li class="<?= $f[1] ? '' : 'off' ?>">
|
||||||
|
<?php if ($f[1]): ?>
|
||||||
|
<i class="fa-solid fa-circle-check pr-check"></i>
|
||||||
|
<?php else: ?>
|
||||||
|
<i class="fa-solid fa-circle-xmark pr-cross"></i>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span><?= htmlspecialchars($f[0]) ?></span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- PERCHÉ SCEGLIERCI -->
|
||||||
|
<div class="pr-why">
|
||||||
|
<h2>Perché i saloni scelgono Come Li Facciamo</h2>
|
||||||
|
<div class="pr-why-grid">
|
||||||
|
<div class="pr-why-item">
|
||||||
|
<div class="pr-why-ico"><i class="fa-solid fa-calendar-check"></i></div>
|
||||||
|
<h4>Prenotazioni 24/7</h4>
|
||||||
|
<p>I clienti prenotano quando vogliono, anche a salone chiuso. Meno telefonate, più appuntamenti.</p>
|
||||||
|
</div>
|
||||||
|
<div class="pr-why-item">
|
||||||
|
<div class="pr-why-ico"><i class="fa-solid fa-user-group"></i></div>
|
||||||
|
<h4>Nuovi clienti</h4>
|
||||||
|
<p>Comparì nella ricerca dei clienti della tua zona e riempi gli spazi liberi in agenda.</p>
|
||||||
|
</div>
|
||||||
|
<div class="pr-why-item">
|
||||||
|
<div class="pr-why-ico"><i class="fa-solid fa-clock-rotate-left"></i></div>
|
||||||
|
<h4>Meno assenze</h4>
|
||||||
|
<p>Promemoria automatici e storico clienti: riduci i "buchi" e i mancati appuntamenti.</p>
|
||||||
|
</div>
|
||||||
|
<div class="pr-why-item">
|
||||||
|
<div class="pr-why-ico"><i class="fa-solid fa-bolt"></i></div>
|
||||||
|
<h4>Pronto subito</h4>
|
||||||
|
<p>Attivi il salone in pochi minuti. Nessuna installazione, nessun hardware.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FAQ -->
|
||||||
|
<div class="pr-faq">
|
||||||
|
<h2>Domande frequenti</h2>
|
||||||
|
<details>
|
||||||
|
<summary>Posso cambiare piano in seguito?</summary>
|
||||||
|
<p>Sì, puoi passare a un piano superiore o inferiore quando vuoi dalla tua area titolare.</p>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Come funziona il pagamento?</summary>
|
||||||
|
<p>Il pagamento è online e sicuro. Puoi scegliere la fatturazione mensile o annuale (con un mese in omaggio).</p>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Cosa succede se non rinnovo?</summary>
|
||||||
|
<p>Alla scadenza il salone smette di essere visibile ai clienti, ma i tuoi dati restano al sicuro: ti basta rinnovare per riattivarlo.</p>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Serve un contratto a lungo termine?</summary>
|
||||||
|
<p>No. Nessun vincolo: sei libero di disdire quando vuoi.</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA finale -->
|
||||||
|
<div class="pr-cta">
|
||||||
|
<h2>Pronto a far risplendere il tuo salone?</h2>
|
||||||
|
<p>Unisciti ai saloni che gestiscono le prenotazioni senza stress.</p>
|
||||||
|
<a href="javascript:;" onclick="document.querySelector('.pr-grid').scrollIntoView({behavior:'smooth'});" class="pr-btn pr-btn-primary" style="display:inline-block; padding:15px 40px;">Scegli il tuo piano</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<?php include('footer.php'); ?>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function setCycle(cycle) {
|
||||||
|
var monthly = cycle === 'monthly';
|
||||||
|
document.getElementById('btnMonthly').classList.toggle('active', monthly);
|
||||||
|
document.getElementById('btnYearly').classList.toggle('active', !monthly);
|
||||||
|
document.querySelectorAll('.js-price-monthly').forEach(function(el) {
|
||||||
|
el.style.display = monthly ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.js-price-yearly').forEach(function(el) {
|
||||||
|
el.style.display = monthly ? 'none' : 'block';
|
||||||
|
});
|
||||||
|
// Aggiorna il ciclo nei link "Inizia ora" così il checkout riceve la scelta giusta
|
||||||
|
document.querySelectorAll('.js-plan-btn').forEach(function(a) {
|
||||||
|
a.href = a.href.replace(/cycle=(monthly|yearly)/, 'cycle=' + cycle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
+1
-1
@@ -11,7 +11,7 @@ $roleId = $_SESSION['kindofrole'] ?? null; // se lo salvi in sessione; altrim
|
|||||||
<div class="col-xl-7 col-6">
|
<div class="col-xl-7 col-6">
|
||||||
<div class="tp-header-left d-flex align-items-center">
|
<div class="tp-header-left d-flex align-items-center">
|
||||||
<div class="tp-header-logo mr-60">
|
<div class="tp-header-logo mr-60">
|
||||||
<a href="index.php">
|
<a href="home.php">
|
||||||
<img width="115" src="assets/img/logo/logo.png" alt="HairBook">
|
<img width="115" src="assets/img/logo/logo.png" alt="HairBook">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
// admin_plan_action.php — Azioni admin sui piani di abbonamento (crea/modifica/elimina/attiva)
|
||||||
|
// Solo ROLE_ADMIN.
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlash(string $type, string $text): void
|
||||||
|
{
|
||||||
|
$_SESSION['flash'] = ['type' => $type, 'text' => $text];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
setFlash('danger', 'Richiesta non valida.');
|
||||||
|
header("Location: admin_plans.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = trim($_POST['action'] ?? '');
|
||||||
|
$plan_id = (int)($_POST['plan_id'] ?? 0);
|
||||||
|
|
||||||
|
function back(): void
|
||||||
|
{
|
||||||
|
header("Location: admin_plans.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ===== Crea / modifica =====
|
||||||
|
if ($action === 'save') {
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$desc = trim($_POST['description'] ?? '');
|
||||||
|
$price = (float)str_replace(',', '.', $_POST['price'] ?? '0');
|
||||||
|
$period = trim($_POST['billing_period'] ?? 'monthly');
|
||||||
|
$active = (int)($_POST['is_active'] ?? 0) === 1 ? 1 : 0;
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
setFlash('danger', 'Il nome del piano è obbligatorio.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
if (!in_array($period, ['monthly', 'yearly'], true)) {
|
||||||
|
setFlash('danger', 'Periodo di fatturazione non valido.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
if ($price < 0) {
|
||||||
|
setFlash('danger', 'Il prezzo non può essere negativo.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($plan_id > 0) {
|
||||||
|
// Modifica
|
||||||
|
$upd = $pdo->prepare("
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET name = ?, description = ?, price = ?, billing_period = ?, is_active = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$upd->execute([$name, ($desc ?: null), $price, $period, $active, $plan_id]);
|
||||||
|
setFlash('success', 'Piano aggiornato.');
|
||||||
|
} else {
|
||||||
|
// Crea
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO subscription_plans (name, description, price, billing_period, is_active)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$ins->execute([$name, ($desc ?: null), $price, $period, $active]);
|
||||||
|
setFlash('success', 'Piano creato.');
|
||||||
|
}
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Attiva / disattiva =====
|
||||||
|
if ($action === 'toggle_active') {
|
||||||
|
if ($plan_id <= 0) {
|
||||||
|
setFlash('danger', 'Piano non valido.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
$new = (int)($_POST['is_active'] ?? 0) === 1 ? 1 : 0;
|
||||||
|
$upd = $pdo->prepare("UPDATE subscription_plans SET is_active = ? WHERE id = ?");
|
||||||
|
$upd->execute([$new, $plan_id]);
|
||||||
|
setFlash('success', $new ? 'Piano attivato.' : 'Piano disattivato.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Elimina (solo se non in uso) =====
|
||||||
|
if ($action === 'delete') {
|
||||||
|
if ($plan_id <= 0) {
|
||||||
|
setFlash('danger', 'Piano non valido.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// È usato da qualche abbonamento? Se sì, NON eliminare.
|
||||||
|
$chk = $pdo->prepare("SELECT COUNT(*) FROM shop_subscriptions WHERE plan_id = ?");
|
||||||
|
$chk->execute([$plan_id]);
|
||||||
|
$inUse = (int)$chk->fetchColumn();
|
||||||
|
|
||||||
|
if ($inUse > 0) {
|
||||||
|
setFlash('warning', "Impossibile eliminare: il piano è usato da {$inUse} salone/i. Disattivalo invece, così non sarà più proponibile ma gli abbonamenti esistenti restano validi.");
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
$del = $pdo->prepare("DELETE FROM subscription_plans WHERE id = ?");
|
||||||
|
$del->execute([$plan_id]);
|
||||||
|
setFlash('success', 'Piano eliminato.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
setFlash('danger', 'Azione non riconosciuta.');
|
||||||
|
back();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('admin_plan_action: ' . $e->getMessage());
|
||||||
|
setFlash('danger', 'Operazione non riuscita.');
|
||||||
|
back();
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function e(?string $v): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
function getFlash(): ?array
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['flash'])) return null;
|
||||||
|
$f = $_SESSION['flash'];
|
||||||
|
unset($_SESSION['flash']);
|
||||||
|
return $f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piani + quanti saloni li usano (per decidere se eliminabili)
|
||||||
|
$plans = $pdo->query("
|
||||||
|
SELECT p.id, p.name, p.description, p.price, p.billing_period, p.is_active,
|
||||||
|
(SELECT COUNT(*) FROM shop_subscriptions s WHERE s.plan_id = p.id) AS in_use
|
||||||
|
FROM subscription_plans p
|
||||||
|
ORDER BY p.price ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$flash = getFlash();
|
||||||
|
?>
|
||||||
|
<!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'); ?>
|
||||||
|
<title>Piani Abbonamento</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--soft-border: #e9edf3;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--soft-border);
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell .card-header {
|
||||||
|
background: linear-gradient(180deg, #fff, #fbfcff);
|
||||||
|
border-bottom: 1px solid var(--soft-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table> :not(caption)>*>* {
|
||||||
|
padding: .85rem .75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-sm {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-price {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-status {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-on {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-off {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</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-shell">
|
||||||
|
<div class="card-header p-3 p-md-4">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
|
<div class="page-title h5 mb-0"><i class="bx bx-package me-1"></i> Piani Abbonamento</div>
|
||||||
|
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#planModal" id="btnNewPlan">
|
||||||
|
<i class="bx bx-plus"></i> Nuovo piano
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= e($flash['type']) ?> alert-dismissible fade show" role="alert">
|
||||||
|
<?= e($flash['text']) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Piano</th>
|
||||||
|
<th>Prezzo</th>
|
||||||
|
<th>Fatturazione</th>
|
||||||
|
<th>In uso</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th class="text-end">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($plans)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center text-muted py-4">Nessun piano. Creane uno con "Nuovo piano".</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: foreach ($plans as $p): ?>
|
||||||
|
<?php
|
||||||
|
$isOn = (int)$p['is_active'] === 1;
|
||||||
|
$inUse = (int)$p['in_use'];
|
||||||
|
$payload = e(json_encode([
|
||||||
|
'id' => (int)$p['id'],
|
||||||
|
'name' => $p['name'],
|
||||||
|
'description' => $p['description'],
|
||||||
|
'price' => $p['price'],
|
||||||
|
'billing_period' => $p['billing_period'],
|
||||||
|
'is_active' => (int)$p['is_active'],
|
||||||
|
], JSON_UNESCAPED_UNICODE));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold"><?= e($p['name']) ?></div>
|
||||||
|
<?php if ($p['description']): ?><div class="muted-sm"><?= e($p['description']) ?></div><?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="plan-price">€<?= number_format((float)$p['price'], 2, ',', '.') ?></td>
|
||||||
|
<td><?= $p['billing_period'] === 'monthly' ? 'Mensile' : 'Annuale' ?></td>
|
||||||
|
<td><?= $inUse > 0 ? $inUse . ' salon' . ($inUse === 1 ? 'e' : 'i') : '<span class="text-muted">—</span>' ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="dot-status <?= $isOn ? 'st-on' : 'st-off' ?>"></span>
|
||||||
|
<?= $isOn ? 'Attivo' : 'Disattivato' ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary js-edit-plan"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#planModal" data-plan='<?= $payload ?>'>
|
||||||
|
<i class="bx bx-edit"></i>
|
||||||
|
</button>
|
||||||
|
<form method="POST" action="admin_plan_action.php" class="d-inline">
|
||||||
|
<input type="hidden" name="action" value="toggle_active">
|
||||||
|
<input type="hidden" name="plan_id" value="<?= (int)$p['id'] ?>">
|
||||||
|
<input type="hidden" name="is_active" value="<?= $isOn ? 0 : 1 ?>">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" title="<?= $isOn ? 'Disattiva' : 'Attiva' ?>">
|
||||||
|
<i class="bx <?= $isOn ? 'bx-toggle-right' : 'bx-toggle-left' ?>"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="admin_plan_action.php" class="d-inline"
|
||||||
|
onsubmit="return confirm('Eliminare questo piano?');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="plan_id" value="<?= (int)$p['id'] ?>">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" <?= $inUse > 0 ? 'disabled title="In uso: disattivalo invece di eliminarlo"' : '' ?>>
|
||||||
|
<i class="bx bx-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach;
|
||||||
|
endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="muted-sm mt-2"><i class="bx bx-info-circle me-1"></i>Un piano <strong>in uso</strong> non è eliminabile: disattivalo per non proporlo più ai nuovi saloni senza toccare gli abbonamenti esistenti.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modale crea/modifica piano -->
|
||||||
|
<div class="modal fade" id="planModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form method="POST" action="admin_plan_action.php" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="pmTitle">Nuovo piano</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="plan_id" id="pmId" value="0">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Nome *</label>
|
||||||
|
<input type="text" name="name" id="pmName" class="form-control" required placeholder="Es. Base, Pro, Premium">
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Descrizione</label>
|
||||||
|
<textarea name="description" id="pmDesc" class="form-control" rows="2" placeholder="Cosa include il piano"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<label class="form-label">Prezzo (€) *</label>
|
||||||
|
<input type="number" name="price" id="pmPrice" class="form-control" step="0.01" min="0" required value="0.00">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<label class="form-label">Fatturazione</label>
|
||||||
|
<select name="billing_period" id="pmPeriod" class="form-select">
|
||||||
|
<option value="monthly">Mensile</option>
|
||||||
|
<option value="yearly">Annuale</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_active" id="pmActive" value="1" checked>
|
||||||
|
<label class="form-check-label" for="pmActive">Piano attivo (proponibile ai saloni)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button class="btn btn-primary">Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
<script>
|
||||||
|
var planModal = document.getElementById('planModal');
|
||||||
|
|
||||||
|
// Nuovo piano: reset campi
|
||||||
|
var btnNew = document.getElementById('btnNewPlan');
|
||||||
|
if (btnNew) btnNew.addEventListener('click', function() {
|
||||||
|
document.getElementById('pmTitle').textContent = 'Nuovo piano';
|
||||||
|
document.getElementById('pmId').value = 0;
|
||||||
|
document.getElementById('pmName').value = '';
|
||||||
|
document.getElementById('pmDesc').value = '';
|
||||||
|
document.getElementById('pmPrice').value = '0.00';
|
||||||
|
document.getElementById('pmPeriod').value = 'monthly';
|
||||||
|
document.getElementById('pmActive').checked = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modifica: popola dal data-plan
|
||||||
|
if (planModal) planModal.addEventListener('show.bs.modal', function(ev) {
|
||||||
|
var b = ev.relatedTarget;
|
||||||
|
if (!b || !b.classList.contains('js-edit-plan')) return;
|
||||||
|
var d;
|
||||||
|
try {
|
||||||
|
d = JSON.parse(b.getAttribute('data-plan'));
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('pmTitle').textContent = 'Modifica piano';
|
||||||
|
document.getElementById('pmId').value = d.id;
|
||||||
|
document.getElementById('pmName').value = d.name || '';
|
||||||
|
document.getElementById('pmDesc').value = d.description || '';
|
||||||
|
document.getElementById('pmPrice').value = parseFloat(d.price).toFixed(2);
|
||||||
|
document.getElementById('pmPeriod').value = d.billing_period || 'monthly';
|
||||||
|
document.getElementById('pmActive').checked = (parseInt(d.is_active, 10) === 1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
// admin_shop_action.php — Azioni admin sui saloni
|
||||||
|
// Gestisce: attiva/disattiva, modifica dati, elimina. Solo ROLE_ADMIN.
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlash(string $type, string $text): void
|
||||||
|
{
|
||||||
|
$_SESSION['flash'] = ['type' => $type, 'text' => $text];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
setFlash('danger', 'Richiesta non valida.');
|
||||||
|
header("Location: admin_shops.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shop_id = (int)($_POST['shop_id'] ?? 0);
|
||||||
|
$action = trim($_POST['action'] ?? '');
|
||||||
|
$ret = trim($_POST['ret'] ?? '');
|
||||||
|
|
||||||
|
function back(string $ret): void
|
||||||
|
{
|
||||||
|
header("Location: admin_shops.php" . ($ret !== '' ? '?' . $ret : ''));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shop_id <= 0 || $action === '') {
|
||||||
|
setFlash('danger', 'Dati azione mancanti.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$s = $pdo->prepare("SELECT id FROM shops WHERE id = ?");
|
||||||
|
$s->execute([$shop_id]);
|
||||||
|
if (!$s->fetch()) {
|
||||||
|
setFlash('danger', 'Salone non trovato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Attiva / disattiva =====
|
||||||
|
if ($action === 'set_active') {
|
||||||
|
$active = (int)($_POST['active'] ?? 0) === 1 ? 1 : 0;
|
||||||
|
$upd = $pdo->prepare("UPDATE shops SET active = ? WHERE id = ?");
|
||||||
|
$upd->execute([$active, $shop_id]);
|
||||||
|
setFlash('success', $active ? 'Salone attivato.' : 'Salone disattivato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Modifica dati essenziali =====
|
||||||
|
if ($action === 'edit') {
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$address = trim($_POST['address'] ?? '');
|
||||||
|
$city = trim($_POST['city'] ?? '');
|
||||||
|
$province = trim($_POST['province'] ?? '');
|
||||||
|
$zip = trim($_POST['zip_code'] ?? '');
|
||||||
|
$phone = trim($_POST['phone'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
|
||||||
|
// Campi NOT NULL della tabella: name, address, city, zip_code
|
||||||
|
if ($name === '' || $address === '' || $city === '' || $zip === '') {
|
||||||
|
setFlash('danger', 'Nome, indirizzo, città e CAP sono obbligatori.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd = $pdo->prepare("
|
||||||
|
UPDATE shops
|
||||||
|
SET name = ?, address = ?, city = ?, province = ?, zip_code = ?, phone = ?, email = ?
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$upd->execute([$name, $address, $city, ($province ?: null), $zip, ($phone ?: null), ($email ?: null), $shop_id]);
|
||||||
|
setFlash('success', 'Dati salone aggiornati.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Elimina =====
|
||||||
|
if ($action === 'delete') {
|
||||||
|
// shops ha FK con ON DELETE CASCADE su molte tabelle figlie (staff, services,
|
||||||
|
// appointments...), quindi eliminare un salone rimuove anche i suoi dati collegati.
|
||||||
|
$del = $pdo->prepare("DELETE FROM shops WHERE id = ?");
|
||||||
|
$del->execute([$shop_id]);
|
||||||
|
setFlash('success', 'Salone eliminato (con tutti i dati collegati).');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFlash('danger', 'Azione non riconosciuta.');
|
||||||
|
back($ret);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('admin_shop_action: ' . $e->getMessage());
|
||||||
|
setFlash('danger', 'Operazione non riuscita.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function e(?string $v): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
function getFlash(): ?array
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['flash'])) return null;
|
||||||
|
$f = $_SESSION['flash'];
|
||||||
|
unset($_SESSION['flash']);
|
||||||
|
return $f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Filtri ---
|
||||||
|
$q_search = trim($_GET['q'] ?? '');
|
||||||
|
$q_active = $_GET['active'] ?? ''; // '', '1', '0'
|
||||||
|
|
||||||
|
$where = ["1=1"];
|
||||||
|
$params = [];
|
||||||
|
if ($q_search !== '') {
|
||||||
|
$where[] = "(sh.name LIKE ? OR sh.city LIKE ? OR sh.email LIKE ?)";
|
||||||
|
$like = '%' . $q_search . '%';
|
||||||
|
array_push($params, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($q_active === '1' || $q_active === '0') {
|
||||||
|
$where[] = "sh.active = ?";
|
||||||
|
$params[] = (int)$q_active;
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT sh.id, sh.name, sh.city, sh.province, sh.address, sh.zip_code,
|
||||||
|
sh.phone, sh.email, sh.logo, sh.active, sh.created_at,
|
||||||
|
CONCAT(u.first_name, ' ', u.last_name) AS owner_name, u.email AS owner_email,
|
||||||
|
(SELECT COUNT(*) FROM staff st WHERE st.shop_id = sh.id) AS staff_count,
|
||||||
|
(SELECT COUNT(*) FROM appointments a WHERE a.shop_id = sh.id) AS appt_count
|
||||||
|
FROM shops sh
|
||||||
|
LEFT JOIN auth_users u ON sh.owner_id = u.id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY sh.created_at DESC
|
||||||
|
";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$shops = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$flash = getFlash();
|
||||||
|
$ret = http_build_query(array_filter(['q' => $q_search, 'active' => $q_active], fn($v) => $v !== '' && $v !== null));
|
||||||
|
?>
|
||||||
|
<!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'); ?>
|
||||||
|
<title>Gestione Saloni</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--soft-border: #e9edf3;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--soft-border);
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell .card-header {
|
||||||
|
background: linear-gradient(180deg, #fff, #fbfcff);
|
||||||
|
border-bottom: 1px solid var(--soft-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-logo {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #eef2f7;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table> :not(caption)>*>* {
|
||||||
|
padding: .85rem .75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-sm {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-status {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-on {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-off {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</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-shell">
|
||||||
|
<div class="card-header p-3 p-md-4">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
|
<div class="page-title h5 mb-0"><i class="bx bx-store me-1"></i> Gestione Saloni</div>
|
||||||
|
<span class="text-muted small"><?= count($shops) ?> saloni</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= e($flash['type']) ?> alert-dismissible fade show" role="alert">
|
||||||
|
<?= e($flash['text']) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="GET" class="row g-2 mb-3">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="Cerca nome, città o email…" value="<?= e($q_search) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<select name="active" class="form-select">
|
||||||
|
<option value="">Tutti</option>
|
||||||
|
<option value="1" <?= $q_active === '1' ? 'selected' : '' ?>>Attivi</option>
|
||||||
|
<option value="0" <?= $q_active === '0' ? 'selected' : '' ?>>Disattivati</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-grid">
|
||||||
|
<button class="btn btn-primary"><i class="bx bx-search"></i> Filtra</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Salone</th>
|
||||||
|
<th>Titolare</th>
|
||||||
|
<th>Città</th>
|
||||||
|
<th>Staff</th>
|
||||||
|
<th>Appunt.</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th class="text-end">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($shops)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center text-muted py-4">Nessun salone trovato.</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: foreach ($shops as $sh): ?>
|
||||||
|
<?php
|
||||||
|
$initial = strtoupper(mb_substr($sh['name'], 0, 1));
|
||||||
|
$isOn = (int)$sh['active'] === 1;
|
||||||
|
$payload = e(json_encode([
|
||||||
|
'id' => (int)$sh['id'],
|
||||||
|
'name' => $sh['name'],
|
||||||
|
'address' => $sh['address'],
|
||||||
|
'city' => $sh['city'],
|
||||||
|
'province' => $sh['province'],
|
||||||
|
'zip_code' => $sh['zip_code'],
|
||||||
|
'phone' => $sh['phone'],
|
||||||
|
'email' => $sh['email'],
|
||||||
|
], JSON_UNESCAPED_UNICODE));
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<?php if (!empty($sh['logo'])): ?>
|
||||||
|
<img src="../<?= e($sh['logo']) ?>" class="shop-logo" alt="">
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="shop-logo"><?= e($initial) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold"><?= e($sh['name']) ?></div>
|
||||||
|
<div class="muted-sm"><?= e($sh['email'] ?: '—') ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div><?= e($sh['owner_name'] ?: '—') ?></div>
|
||||||
|
<div class="muted-sm"><?= e($sh['owner_email'] ?: '') ?></div>
|
||||||
|
</td>
|
||||||
|
<td><?= e($sh['city']) ?><?= $sh['province'] ? ' (' . e($sh['province']) . ')' : '' ?></td>
|
||||||
|
<td><?= (int)$sh['staff_count'] ?></td>
|
||||||
|
<td><?= (int)$sh['appt_count'] ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="dot-status <?= $isOn ? 'st-on' : 'st-off' ?>"></span>
|
||||||
|
<?= $isOn ? 'Attivo' : 'Disattivato' ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">Gestisci</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><button class="dropdown-item js-edit-shop" data-bs-toggle="modal" data-bs-target="#shopModal" data-shop='<?= $payload ?>'><i class="bx bx-edit me-1"></i> Modifica</button></li>
|
||||||
|
<li>
|
||||||
|
<form method="POST" action="admin_shop_action.php">
|
||||||
|
<input type="hidden" name="action" value="set_active">
|
||||||
|
<input type="hidden" name="shop_id" value="<?= (int)$sh['id'] ?>">
|
||||||
|
<input type="hidden" name="active" value="<?= $isOn ? 0 : 1 ?>">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<button class="dropdown-item">
|
||||||
|
<?php if ($isOn): ?><i class="bx bx-block me-1"></i> Disattiva<?php else: ?><i class="bx bx-check-circle me-1"></i> Attiva<?php endif; ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<?php /* "Vedi pagina" disattivato finché lo slug non è configurato — vedi TODO */ ?>
|
||||||
|
<li><span class="dropdown-item disabled text-muted" title="Disponibile quando lo slug sarà configurato"><i class="bx bx-link-external me-1"></i> Vedi pagina (presto)</span></li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form method="POST" action="admin_shop_action.php" onsubmit="return confirm('Eliminare questo salone e TUTTI i suoi dati (staff, servizi, appuntamenti)? L\'azione è irreversibile.');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="shop_id" value="<?= (int)$sh['id'] ?>">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<button class="dropdown-item text-danger"><i class="bx bx-trash me-1"></i> Elimina</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach;
|
||||||
|
endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modale modifica salone -->
|
||||||
|
<div class="modal fade" id="shopModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form method="POST" action="admin_shop_action.php" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Modifica salone</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="action" value="edit">
|
||||||
|
<input type="hidden" name="shop_id" id="msId">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Nome *</label>
|
||||||
|
<input type="text" name="name" id="msName" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Indirizzo *</label>
|
||||||
|
<input type="text" name="address" id="msAddress" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<label class="form-label">Città *</label>
|
||||||
|
<input type="text" name="city" id="msCity" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 mb-2">
|
||||||
|
<label class="form-label">Prov.</label>
|
||||||
|
<input type="text" name="province" id="msProvince" class="form-control" maxlength="50">
|
||||||
|
</div>
|
||||||
|
<div class="col-3 mb-2">
|
||||||
|
<label class="form-label">CAP *</label>
|
||||||
|
<input type="text" name="zip_code" id="msZip" class="form-control" required maxlength="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<label class="form-label">Telefono</label>
|
||||||
|
<input type="text" name="phone" id="msPhone" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input type="email" name="email" id="msEmail" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button class="btn btn-primary">Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
<script>
|
||||||
|
var shopModal = document.getElementById('shopModal');
|
||||||
|
if (shopModal) shopModal.addEventListener('show.bs.modal', function(ev) {
|
||||||
|
var b = ev.relatedTarget;
|
||||||
|
if (!b || !b.classList.contains('js-edit-shop')) return;
|
||||||
|
var d;
|
||||||
|
try {
|
||||||
|
d = JSON.parse(b.getAttribute('data-shop'));
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('msId').value = d.id;
|
||||||
|
document.getElementById('msName').value = d.name || '';
|
||||||
|
document.getElementById('msAddress').value = d.address || '';
|
||||||
|
document.getElementById('msCity').value = d.city || '';
|
||||||
|
document.getElementById('msProvince').value = d.province || '';
|
||||||
|
document.getElementById('msZip').value = d.zip_code || '';
|
||||||
|
document.getElementById('msPhone').value = d.phone || '';
|
||||||
|
document.getElementById('msEmail').value = d.email || '';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
// admin_subscription_action.php — Azioni admin sugli abbonamenti dei saloni
|
||||||
|
// Azioni: assegna/aggiorna abbonamento, varia date, regala mesi, blocca (violazione), sblocca.
|
||||||
|
// Ogni cambiamento di stato sincronizza shops.active. Solo ROLE_ADMIN.
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlash(string $type, string $text): void
|
||||||
|
{
|
||||||
|
$_SESSION['flash'] = ['type' => $type, 'text' => $text];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
setFlash('danger', 'Richiesta non valida.');
|
||||||
|
header("Location: admin_subscriptions.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shop_id = (int)($_POST['shop_id'] ?? 0);
|
||||||
|
$action = trim($_POST['action'] ?? '');
|
||||||
|
$ret = trim($_POST['ret'] ?? '');
|
||||||
|
|
||||||
|
function back(string $ret): void
|
||||||
|
{
|
||||||
|
header("Location: admin_subscriptions.php" . ($ret !== '' ? '?' . $ret : ''));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($shop_id <= 0 || $action === '') {
|
||||||
|
setFlash('danger', 'Dati azione mancanti.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sincronizza shops.active in base allo stato dell'abbonamento.
|
||||||
|
* active/trialing non scaduto → salone acceso; expired/blocked/pending → spento.
|
||||||
|
*/
|
||||||
|
function syncShopActive(PDO $pdo, int $shop_id, string $status, ?string $expires): void
|
||||||
|
{
|
||||||
|
$shouldBeActive = 0;
|
||||||
|
if (in_array($status, ['active', 'trialing'], true)) {
|
||||||
|
if ($expires === null || $expires >= date('Y-m-d')) {
|
||||||
|
$shouldBeActive = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$u = $pdo->prepare("UPDATE shops SET active = ? WHERE id = ?");
|
||||||
|
$u->execute([$shouldBeActive, $shop_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Il salone esiste?
|
||||||
|
$s = $pdo->prepare("SELECT id FROM shops WHERE id = ?");
|
||||||
|
$s->execute([$shop_id]);
|
||||||
|
if (!$s->fetch()) {
|
||||||
|
setFlash('danger', 'Salone non trovato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carica l'abbonamento corrente (se esiste)
|
||||||
|
$cur = $pdo->prepare("SELECT * FROM shop_subscriptions WHERE shop_id = ?");
|
||||||
|
$cur->execute([$shop_id]);
|
||||||
|
$sub = $cur->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// ===== Assegna / aggiorna abbonamento (piano + date + stato) =====
|
||||||
|
if ($action === 'save') {
|
||||||
|
$plan_id = (int)($_POST['plan_id'] ?? 0) ?: null;
|
||||||
|
$status = trim($_POST['status'] ?? 'active');
|
||||||
|
$started = trim($_POST['started_at'] ?? '') ?: null;
|
||||||
|
$expires = trim($_POST['expires_at'] ?? '') ?: null;
|
||||||
|
|
||||||
|
$allowed = ['active', 'expired', 'blocked', 'trialing', 'pending'];
|
||||||
|
if (!in_array($status, $allowed, true)) {
|
||||||
|
setFlash('danger', 'Stato non valido.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sub) {
|
||||||
|
$upd = $pdo->prepare("
|
||||||
|
UPDATE shop_subscriptions
|
||||||
|
SET plan_id = ?, status = ?, started_at = ?, expires_at = ?
|
||||||
|
WHERE shop_id = ?
|
||||||
|
");
|
||||||
|
$upd->execute([$plan_id, $status, $started, $expires, $shop_id]);
|
||||||
|
} else {
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO shop_subscriptions (shop_id, plan_id, status, started_at, expires_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
");
|
||||||
|
$ins->execute([$shop_id, $plan_id, $status, $started, $expires]);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncShopActive($pdo, $shop_id, $status, $expires);
|
||||||
|
setFlash('success', 'Abbonamento aggiornato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$sub) {
|
||||||
|
setFlash('warning', 'Questo salone non ha ancora un abbonamento: usa "Assegna abbonamento".');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Regala mesi (estende expires_at) =====
|
||||||
|
if ($action === 'gift_months') {
|
||||||
|
$months = (int)($_POST['months'] ?? 0);
|
||||||
|
if ($months < 1 || $months > 24) {
|
||||||
|
setFlash('danger', 'Numero di mesi non valido (1–24).');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
// Base di partenza: la scadenza attuale se futura, altrimenti oggi
|
||||||
|
$baseDate = ($sub['expires_at'] && $sub['expires_at'] >= date('Y-m-d'))
|
||||||
|
? $sub['expires_at'] : date('Y-m-d');
|
||||||
|
$newExpires = date('Y-m-d', strtotime($baseDate . " +{$months} months"));
|
||||||
|
|
||||||
|
// Regalare mesi riattiva l'abbonamento
|
||||||
|
$upd = $pdo->prepare("UPDATE shop_subscriptions SET expires_at = ?, status = 'active' WHERE shop_id = ?");
|
||||||
|
$upd->execute([$newExpires, $shop_id]);
|
||||||
|
syncShopActive($pdo, $shop_id, 'active', $newExpires);
|
||||||
|
setFlash('success', "Regalati {$months} mese/i. Nuova scadenza: " . date('d/m/Y', strtotime($newExpires)) . ".");
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Blocca per violazione =====
|
||||||
|
if ($action === 'block') {
|
||||||
|
$reason = trim($_POST['block_reason'] ?? '');
|
||||||
|
$upd = $pdo->prepare("UPDATE shop_subscriptions SET status = 'blocked', block_reason = ? WHERE shop_id = ?");
|
||||||
|
$upd->execute([($reason ?: 'Violazione regolamento'), $shop_id]);
|
||||||
|
syncShopActive($pdo, $shop_id, 'blocked', $sub['expires_at']);
|
||||||
|
setFlash('success', 'Salone bloccato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Sblocca (torna active se non scaduto, altrimenti expired) =====
|
||||||
|
if ($action === 'unblock') {
|
||||||
|
$newStatus = ($sub['expires_at'] === null || $sub['expires_at'] >= date('Y-m-d')) ? 'active' : 'expired';
|
||||||
|
$upd = $pdo->prepare("UPDATE shop_subscriptions SET status = ?, block_reason = NULL WHERE shop_id = ?");
|
||||||
|
$upd->execute([$newStatus, $shop_id]);
|
||||||
|
syncShopActive($pdo, $shop_id, $newStatus, $sub['expires_at']);
|
||||||
|
setFlash('success', $newStatus === 'active' ? 'Salone sbloccato e riattivato.' : 'Salone sbloccato (ma abbonamento scaduto).');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFlash('danger', 'Azione non riconosciuta.');
|
||||||
|
back($ret);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('admin_subscription_action: ' . $e->getMessage());
|
||||||
|
setFlash('danger', 'Operazione non riuscita.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
@@ -1,233 +1,98 @@
|
|||||||
<?php
|
<?php
|
||||||
// admin_subscriptions.php
|
|
||||||
|
|
||||||
ini_set('display_errors', 1);
|
ini_set('display_errors', 1);
|
||||||
ini_set('display_startup_errors', 1);
|
ini_set('display_startup_errors', 1);
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
include('include/headscript.php');
|
include('include/headscript.php');
|
||||||
|
|
||||||
// DB
|
|
||||||
$dbHandler = DBHandlerSelect::getInstance();
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
$pdo = $dbHandler->getConnection();
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
// ---- Auth check ----
|
|
||||||
if (!isset($iduserlogin)) {
|
if (!isset($iduserlogin)) {
|
||||||
die("Access denied.");
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Admin check (Vanguard usually uses role_id=1 for admin) ----
|
function e(?string $v): string
|
||||||
// Adjust role_id list if needed.
|
|
||||||
$stmt = $pdo->prepare("SELECT role_id, email FROM auth_users WHERE id = ? LIMIT 1");
|
|
||||||
$stmt->execute([$iduserlogin]);
|
|
||||||
$me = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$me || !in_array((int)$me['role_id'], [1])) {
|
|
||||||
die("Access denied: admin only.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Handle POST actions ----
|
|
||||||
$success_message = null;
|
|
||||||
$error = null;
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|
||||||
$action = $_POST['action'];
|
|
||||||
|
|
||||||
// Update school status (local app status)
|
|
||||||
if ($action === 'update_school_status') {
|
|
||||||
$school_id = (int)($_POST['school_id'] ?? 0);
|
|
||||||
$new_status = $_POST['status'] ?? 'active';
|
|
||||||
$allowed = ['active', 'inactive', 'suspended'];
|
|
||||||
|
|
||||||
if ($school_id <= 0 || !in_array($new_status, $allowed, true)) {
|
|
||||||
$error = "Invalid request.";
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("UPDATE schools SET status = ? WHERE id = ?");
|
|
||||||
if ($stmt->execute([$new_status, $school_id])) {
|
|
||||||
$success_message = "School status updated.";
|
|
||||||
} else {
|
|
||||||
$error = "Failed updating school status.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: Stripe actions (cancel, resume, change plan) should call Stripe API.
|
|
||||||
// Here we provide placeholders so UI is ready.
|
|
||||||
if ($action === 'flag_cancel_at_period_end') {
|
|
||||||
$sub_id = (int)($_POST['subscription_row_id'] ?? 0);
|
|
||||||
$flag = (int)($_POST['flag'] ?? 0);
|
|
||||||
$school_id = (int)($_POST['school_id'] ?? 0);
|
|
||||||
|
|
||||||
if ($sub_id <= 0 || $school_id <= 0 || !in_array($flag, [0, 1], true)) {
|
|
||||||
$error = "Invalid request.";
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("
|
|
||||||
UPDATE school_subscriptions
|
|
||||||
SET cancel_at_period_end = ?
|
|
||||||
WHERE id = ? AND school_id = ?
|
|
||||||
");
|
|
||||||
$ok = $stmt->execute([$flag, $sub_id, $school_id]);
|
|
||||||
|
|
||||||
if ($ok) {
|
|
||||||
$success_message = $flag ? "Marked cancel at period end (LOCAL)." : "Unmarked cancel at period end (LOCAL).";
|
|
||||||
} else {
|
|
||||||
$error = "Failed updating subscription flag.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Filters (GET) ----
|
|
||||||
$q = trim($_GET['q'] ?? '');
|
|
||||||
$plan_id = (int)($_GET['plan_id'] ?? 0);
|
|
||||||
$sub_status = trim($_GET['sub_status'] ?? ''); // e.g. active, trialing, past_due, canceled, incomplete, unpaid
|
|
||||||
$school_status = trim($_GET['school_status'] ?? ''); // active, inactive, suspended
|
|
||||||
$has_sub = $_GET['has_sub'] ?? ''; // '1' or '0' or ''
|
|
||||||
|
|
||||||
$where = [];
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
// Search by school name/email/owner email
|
|
||||||
if ($q !== '') {
|
|
||||||
$where[] = "(s.name LIKE ? OR s.email LIKE ? OR ou.email LIKE ? OR CONCAT(ou.first_name,' ',ou.last_name) LIKE ?)";
|
|
||||||
$like = '%' . $q . '%';
|
|
||||||
$params[] = $like;
|
|
||||||
$params[] = $like;
|
|
||||||
$params[] = $like;
|
|
||||||
$params[] = $like;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($plan_id > 0) {
|
|
||||||
$where[] = "ss.plan_id = ?";
|
|
||||||
$params[] = $plan_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sub_status !== '') {
|
|
||||||
$where[] = "ss.status = ?";
|
|
||||||
$params[] = $sub_status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($school_status !== '') {
|
|
||||||
$where[] = "s.status = ?";
|
|
||||||
$params[] = $school_status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($has_sub === '1') {
|
|
||||||
$where[] = "ss.id IS NOT NULL";
|
|
||||||
} elseif ($has_sub === '0') {
|
|
||||||
$where[] = "ss.id IS NULL";
|
|
||||||
}
|
|
||||||
|
|
||||||
$sqlWhere = '';
|
|
||||||
if (!empty($where)) {
|
|
||||||
$sqlWhere = "WHERE " . implode(" AND ", $where);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Load plans for filter dropdown ----
|
|
||||||
$stmt = $pdo->prepare("SELECT id, code, name, currency, unit_amount, `interval`, interval_count, is_active FROM billing_plans ORDER BY is_active DESC, unit_amount ASC, name ASC");
|
|
||||||
$stmt->execute();
|
|
||||||
$plans = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// ---- Main query: schools + subscription + owner + plan ----
|
|
||||||
$stmt = $pdo->prepare("
|
|
||||||
SELECT
|
|
||||||
s.id AS school_id,
|
|
||||||
s.name AS school_name,
|
|
||||||
s.email AS school_email,
|
|
||||||
s.status AS school_status,
|
|
||||||
s.created_at AS school_created_at,
|
|
||||||
|
|
||||||
ou.id AS owner_id,
|
|
||||||
ou.first_name AS owner_first_name,
|
|
||||||
ou.last_name AS owner_last_name,
|
|
||||||
ou.email AS owner_email,
|
|
||||||
|
|
||||||
ss.id AS subscription_row_id,
|
|
||||||
ss.stripe_customer_id,
|
|
||||||
ss.stripe_subscription_id,
|
|
||||||
COALESCE(ss.status, 'none') AS subscription_status,
|
|
||||||
ss.current_period_start,
|
|
||||||
ss.current_period_end,
|
|
||||||
ss.trial_start,
|
|
||||||
ss.trial_end,
|
|
||||||
ss.cancel_at_period_end,
|
|
||||||
ss.updated_at AS subscription_updated_at,
|
|
||||||
|
|
||||||
bp.id AS plan_id,
|
|
||||||
bp.code AS plan_code,
|
|
||||||
bp.name AS plan_name,
|
|
||||||
bp.currency,
|
|
||||||
bp.unit_amount,
|
|
||||||
bp.`interval`,
|
|
||||||
bp.interval_count
|
|
||||||
|
|
||||||
FROM schools s
|
|
||||||
JOIN auth_users ou ON ou.id = s.owner_id
|
|
||||||
LEFT JOIN school_subscriptions ss ON ss.school_id = s.id
|
|
||||||
LEFT JOIN billing_plans bp ON bp.id = ss.plan_id
|
|
||||||
|
|
||||||
$sqlWhere
|
|
||||||
ORDER BY
|
|
||||||
-- Put schools without subscription at the bottom
|
|
||||||
CASE WHEN ss.id IS NULL THEN 1 ELSE 0 END ASC,
|
|
||||||
|
|
||||||
-- Show problematic subscriptions first
|
|
||||||
FIELD(ss.status, 'past_due','unpaid','incomplete','incomplete_expired','canceled','paused','trialing','active') ASC,
|
|
||||||
|
|
||||||
s.created_at DESC
|
|
||||||
|
|
||||||
");
|
|
||||||
$stmt->execute($params);
|
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// ---- Helpers ----
|
|
||||||
function h($v)
|
|
||||||
{
|
{
|
||||||
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
function getFlash(): ?array
|
||||||
function moneyFmt($unit_amount, $currency)
|
|
||||||
{
|
{
|
||||||
if ($unit_amount === null || $currency === null) return '—';
|
if (!isset($_SESSION['flash'])) return null;
|
||||||
$amount = ((int)$unit_amount) / 100;
|
$f = $_SESSION['flash'];
|
||||||
return number_format($amount, 2, ',', '.') . ' ' . strtoupper($currency);
|
unset($_SESSION['flash']);
|
||||||
|
return $f;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dateFmt($dt)
|
// Etichette + badge per stato abbonamento
|
||||||
|
function subMeta(?string $status): array
|
||||||
{
|
{
|
||||||
if (!$dt) return '—';
|
switch ($status) {
|
||||||
// Accept both timestamp and datetime strings
|
case 'active':
|
||||||
$ts = is_numeric($dt) ? (int)$dt : strtotime($dt);
|
return ['badge' => 'bg-success', 'label' => 'Attivo'];
|
||||||
if (!$ts) return '—';
|
case 'trialing':
|
||||||
return date('d/m/Y', $ts);
|
return ['badge' => 'bg-info text-dark', 'label' => 'In prova'];
|
||||||
|
case 'expired':
|
||||||
|
return ['badge' => 'bg-warning text-dark', 'label' => 'Scaduto'];
|
||||||
|
case 'blocked':
|
||||||
|
return ['badge' => 'bg-danger', 'label' => 'Bloccato'];
|
||||||
|
case 'pending':
|
||||||
|
return ['badge' => 'bg-secondary', 'label' => 'In attesa'];
|
||||||
|
default:
|
||||||
|
return ['badge' => 'bg-light text-dark', 'label' => 'Nessuno'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function badgeClassForSub($status)
|
// --- Filtri ---
|
||||||
{
|
$q_search = trim($_GET['q'] ?? '');
|
||||||
$map = [
|
$q_status = trim($_GET['status'] ?? '');
|
||||||
'active' => 'bg-success',
|
|
||||||
'trialing' => 'bg-info',
|
|
||||||
'past_due' => 'bg-warning',
|
|
||||||
'unpaid' => 'bg-danger',
|
|
||||||
'incomplete' => 'bg-warning',
|
|
||||||
'incomplete_expired' => 'bg-danger',
|
|
||||||
'canceled' => 'bg-secondary',
|
|
||||||
'paused' => 'bg-secondary',
|
|
||||||
'none' => 'bg-secondary',
|
|
||||||
];
|
|
||||||
return $map[$status] ?? 'bg-dark';
|
|
||||||
}
|
|
||||||
|
|
||||||
function badgeClassForSchool($status)
|
$where = ["1=1"];
|
||||||
{
|
$params = [];
|
||||||
$map = [
|
if ($q_search !== '') {
|
||||||
'active' => 'bg-success',
|
$where[] = "(sh.name LIKE ? OR sh.city LIKE ?)";
|
||||||
'inactive' => 'bg-secondary',
|
$like = '%' . $q_search . '%';
|
||||||
'suspended' => 'bg-danger',
|
array_push($params, $like, $like);
|
||||||
];
|
|
||||||
return $map[$status] ?? 'bg-dark';
|
|
||||||
}
|
}
|
||||||
|
if ($q_status !== '') {
|
||||||
|
if ($q_status === 'none') {
|
||||||
|
$where[] = "sub.id IS NULL";
|
||||||
|
} else {
|
||||||
|
$where[] = "sub.status = ?";
|
||||||
|
$params[] = $q_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
// Tutti i saloni + eventuale abbonamento (LEFT JOIN: mostra anche chi non ne ha)
|
||||||
|
$sql = "
|
||||||
|
SELECT sh.id AS shop_id, sh.name AS shop_name, sh.city, sh.active AS shop_active,
|
||||||
|
sub.id AS sub_id, sub.plan_id, sub.status, sub.started_at, sub.expires_at, sub.block_reason,
|
||||||
|
p.name AS plan_name,
|
||||||
|
CONCAT(u.first_name, ' ', u.last_name) AS owner_name
|
||||||
|
FROM shops sh
|
||||||
|
LEFT JOIN shop_subscriptions sub ON sub.shop_id = sh.id
|
||||||
|
LEFT JOIN subscription_plans p ON sub.plan_id = p.id
|
||||||
|
LEFT JOIN auth_users u ON sh.owner_id = u.id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY sh.name ASC
|
||||||
|
";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Piani per le select
|
||||||
|
$plans = $pdo->query("SELECT id, name, price, billing_period FROM subscription_plans WHERE is_active = 1 ORDER BY price ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$flash = getFlash();
|
||||||
|
$ret = http_build_query(array_filter(['q' => $q_search, 'status' => $q_status]));
|
||||||
|
$today = date('Y-m-d');
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
@@ -238,6 +103,59 @@ function badgeClassForSchool($status)
|
|||||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
<?php include('cssinclude.php'); ?>
|
<?php include('cssinclude.php'); ?>
|
||||||
<?php include('siteinfo.php'); ?>
|
<?php include('siteinfo.php'); ?>
|
||||||
|
<title>Abbonamenti Saloni</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--soft-border: #e9edf3;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--soft-border);
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell .card-header {
|
||||||
|
background: linear-gradient(180deg, #fff, #fbfcff);
|
||||||
|
border-bottom: 1px solid var(--soft-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table> :not(caption)>*>* {
|
||||||
|
padding: .85rem .75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-sm {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-soon {
|
||||||
|
color: #b45309;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-past {
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-off {
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -247,298 +165,275 @@ function badgeClassForSchool($status)
|
|||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
<div class="card-shell">
|
||||||
<div class="card radius-10 mb-3">
|
<div class="card-header p-3 p-md-4">
|
||||||
<div class="card-body">
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="page-title h5 mb-0"><i class="bx bx-credit-card me-1"></i> Abbonamenti Saloni</div>
|
||||||
<div>
|
<span class="text-muted small"><?= count($rows) ?> saloni</span>
|
||||||
<h5 class="mb-0">Admin Subscriptions</h5>
|
|
||||||
<div class="text-muted small">Schools + Stripe subscription status overview</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<a href="admin_billing_plans.php" class="btn btn-outline-primary">
|
|
||||||
<i class="bx bx-list-ul"></i> Billing Plans
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($success_message): ?>
|
|
||||||
<div class="alert alert-success"><?php echo h($success_message); ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($error): ?>
|
|
||||||
<div class="alert alert-danger"><?php echo h($error); ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="card radius-10 mb-4">
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="GET" class="row g-2 align-items-end">
|
<?php if ($flash): ?>
|
||||||
<div class="col-md-4">
|
<div class="alert alert-<?= e($flash['type']) ?> alert-dismissible fade show" role="alert">
|
||||||
<label class="form-label">Search</label>
|
<?= e($flash['text']) ?>
|
||||||
<input type="text" class="form-control" name="q" value="<?php echo h($q); ?>" placeholder="School / email / owner">
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="GET" class="row g-2 mb-3">
|
||||||
|
<div class="col-md-7">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="Cerca salone o città…" value="<?= e($q_search) ?>">
|
||||||
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<label class="form-label">Plan</label>
|
<select name="status" class="form-select">
|
||||||
<select class="form-control" name="plan_id">
|
<option value="">Tutti gli stati</option>
|
||||||
<option value="0">All plans</option>
|
<option value="active" <?= $q_status === 'active' ? 'selected' : '' ?>>Attivi</option>
|
||||||
<?php foreach ($plans as $p): ?>
|
<option value="trialing" <?= $q_status === 'trialing' ? 'selected' : '' ?>>In prova</option>
|
||||||
<option value="<?php echo (int)$p['id']; ?>" <?php echo ((int)$p['id'] === $plan_id) ? 'selected' : ''; ?>>
|
<option value="expired" <?= $q_status === 'expired' ? 'selected' : '' ?>>Scaduti</option>
|
||||||
<?php
|
<option value="blocked" <?= $q_status === 'blocked' ? 'selected' : '' ?>>Bloccati</option>
|
||||||
$label = $p['name'] . ' (' . $p['code'] . ') - ' . moneyFmt($p['unit_amount'], $p['currency']);
|
<option value="pending" <?= $q_status === 'pending' ? 'selected' : '' ?>>In attesa</option>
|
||||||
$label .= ' / ' . $p['interval_count'] . ' ' . $p['interval'];
|
<option value="none" <?= $q_status === 'none' ? 'selected' : '' ?>>Senza abbonamento</option>
|
||||||
if (!(int)$p['is_active']) $label .= ' [inactive]';
|
|
||||||
echo h($label);
|
|
||||||
?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2 d-grid">
|
||||||
<div class="col-md-2">
|
<button class="btn btn-primary"><i class="bx bx-search"></i> Filtra</button>
|
||||||
<label class="form-label">Subscription status</label>
|
|
||||||
<select class="form-control" name="sub_status">
|
|
||||||
<option value="">All</option>
|
|
||||||
<?php foreach (['none', 'active', 'trialing', 'past_due', 'unpaid', 'incomplete', 'incomplete_expired', 'canceled', 'paused'] as $st): ?>
|
|
||||||
<option value="<?php echo h($st); ?>" <?php echo ($sub_status === $st) ? 'selected' : ''; ?>>
|
|
||||||
<?php echo h($st); ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">School status</label>
|
|
||||||
<select class="form-control" name="school_status">
|
|
||||||
<option value="">All</option>
|
|
||||||
<?php foreach (['active', 'inactive', 'suspended'] as $st): ?>
|
|
||||||
<option value="<?php echo h($st); ?>" <?php echo ($school_status === $st) ? 'selected' : ''; ?>>
|
|
||||||
<?php echo h($st); ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-1">
|
|
||||||
<label class="form-label">Has sub</label>
|
|
||||||
<select class="form-control" name="has_sub">
|
|
||||||
<option value="" <?php echo ($has_sub === '') ? 'selected' : ''; ?>>All</option>
|
|
||||||
<option value="1" <?php echo ($has_sub === '1') ? 'selected' : ''; ?>>Yes</option>
|
|
||||||
<option value="0" <?php echo ($has_sub === '0') ? 'selected' : ''; ?>>No</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-12 d-flex gap-2 mt-2">
|
|
||||||
<button class="btn btn-primary" type="submit">
|
|
||||||
<i class="bx bx-filter"></i> Apply
|
|
||||||
</button>
|
|
||||||
<a class="btn btn-outline-secondary" href="admin_subscriptions.php">Reset</a>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results -->
|
|
||||||
<div class="card radius-10">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
|
||||||
<div>
|
|
||||||
<h6 class="mb-0">Schools</h6>
|
|
||||||
<div class="text-muted small"><?php echo (int)count($rows); ?> rows</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-bordered align-middle mb-0">
|
<table class="table table-hover align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="min-width:260px;">School</th>
|
<th>Salone</th>
|
||||||
<th style="min-width:220px;">Owner</th>
|
<th>Piano</th>
|
||||||
<th style="min-width:190px;">School status</th>
|
<th>Stato</th>
|
||||||
<th style="min-width:160px;">Subscription</th>
|
<th>Scadenza</th>
|
||||||
<th style="min-width:220px;">Plan</th>
|
<th>Salone attivo?</th>
|
||||||
<th style="min-width:200px;">Period</th>
|
<th class="text-end">Azioni</th>
|
||||||
<th style="min-width:200px;">Trial</th>
|
|
||||||
<th style="min-width:140px;">Cancel at period end</th>
|
|
||||||
<th style="min-width:260px;">Stripe IDs</th>
|
|
||||||
<th style="min-width:160px;">Updated</th>
|
|
||||||
<th style="min-width:180px;">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($rows as $row): ?>
|
|
||||||
<?php
|
|
||||||
$hasSubscription = !empty($row['subscription_row_id']);
|
|
||||||
$subStatus = $row['subscription_status'] ?? 'none';
|
|
||||||
?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="fw-bold"><?php echo h($row['school_name']); ?></div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
ID: <?php echo (int)$row['school_id']; ?> · <?php echo h($row['school_email']); ?>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
Created: <?php echo h(dateFmt($row['school_created_at'])); ?>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<div class="fw-bold"><?php echo h(trim(($row['owner_first_name'] ?? '') . ' ' . ($row['owner_last_name'] ?? ''))); ?></div>
|
|
||||||
<div class="text-muted small"><?php echo h($row['owner_email']); ?></div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<form method="POST" class="d-flex gap-2 align-items-center">
|
|
||||||
<input type="hidden" name="action" value="update_school_status">
|
|
||||||
<input type="hidden" name="school_id" value="<?php echo (int)$row['school_id']; ?>">
|
|
||||||
|
|
||||||
<select name="status" class="form-control form-control-sm" style="min-width:140px;">
|
|
||||||
<?php foreach (['active', 'inactive', 'suspended'] as $st): ?>
|
|
||||||
<option value="<?php echo h($st); ?>" <?php echo ($row['school_status'] === $st) ? 'selected' : ''; ?>>
|
|
||||||
<?php echo h($st); ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-primary">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
|
||||||
<span class="badge <?php echo h(badgeClassForSchool($row['school_status'])); ?>">
|
|
||||||
<?php echo h($row['school_status']); ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span class="badge <?php echo h(badgeClassForSub($subStatus)); ?>">
|
|
||||||
<?php echo h($subStatus); ?>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<?php if ($hasSubscription): ?>
|
|
||||||
<div class="text-muted small mt-2">
|
|
||||||
Qty: <?php echo (int)($row['quantity'] ?? 1); ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php if ($hasSubscription && !empty($row['plan_id'])): ?>
|
|
||||||
<div class="fw-bold"><?php echo h($row['plan_name']); ?></div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
<?php echo h($row['plan_code']); ?> · <?php echo h(moneyFmt($row['unit_amount'], $row['currency'])); ?>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
Every <?php echo (int)$row['interval_count']; ?> <?php echo h($row['interval']); ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
—
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php if ($hasSubscription): ?>
|
|
||||||
<div class="text-muted small">
|
|
||||||
Start: <?php echo h(dateFmt($row['current_period_start'])); ?>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
End: <?php echo h(dateFmt($row['current_period_end'])); ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
—
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php if ($hasSubscription): ?>
|
|
||||||
<div class="text-muted small">
|
|
||||||
Start: <?php echo h(dateFmt($row['trial_start'])); ?>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
End: <?php echo h(dateFmt($row['trial_end'])); ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
—
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php if ($hasSubscription): ?>
|
|
||||||
<?php if ((int)$row['cancel_at_period_end'] === 1): ?>
|
|
||||||
<span class="badge bg-warning">Yes</span>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="badge bg-secondary">No</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
—
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php if ($hasSubscription): ?>
|
|
||||||
<div class="text-muted small">
|
|
||||||
cust: <?php echo h($row['stripe_customer_id'] ?: '—'); ?>
|
|
||||||
</div>
|
|
||||||
<div class="text-muted small">
|
|
||||||
sub: <?php echo h($row['stripe_subscription_id'] ?: '—'); ?>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
—
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php echo h(dateFmt($row['subscription_updated_at'])); ?>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<?php if ($hasSubscription): ?>
|
|
||||||
<form method="POST" class="d-inline">
|
|
||||||
<input type="hidden" name="action" value="flag_cancel_at_period_end">
|
|
||||||
<input type="hidden" name="subscription_row_id" value="<?php echo (int)$row['subscription_row_id']; ?>">
|
|
||||||
<input type="hidden" name="school_id" value="<?php echo (int)$row['school_id']; ?>">
|
|
||||||
<input type="hidden" name="flag" value="<?php echo ((int)$row['cancel_at_period_end'] === 1) ? 0 : 1; ?>">
|
|
||||||
|
|
||||||
<?php if ((int)$row['cancel_at_period_end'] === 1): ?>
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-success">
|
|
||||||
Unmark
|
|
||||||
</button>
|
|
||||||
<?php else: ?>
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-warning">
|
|
||||||
Mark
|
|
||||||
</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</form>
|
|
||||||
<?php else: ?>
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php if (empty($rows)): ?>
|
<?php if (empty($rows)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="11" class="text-center text-muted py-4">No results</td>
|
<td colspan="6" class="text-center text-muted py-4">Nessun salone trovato.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php else: foreach ($rows as $r): ?>
|
||||||
|
<?php
|
||||||
|
$meta = subMeta($r['status']);
|
||||||
|
$expires = $r['expires_at'];
|
||||||
|
$expClass = '';
|
||||||
|
if ($expires) {
|
||||||
|
if ($expires < $today) $expClass = 'exp-past';
|
||||||
|
elseif ($expires <= date('Y-m-d', strtotime('+7 days'))) $expClass = 'exp-soon';
|
||||||
|
}
|
||||||
|
$payload = e(json_encode([
|
||||||
|
'shop_id' => (int)$r['shop_id'],
|
||||||
|
'shop_name' => $r['shop_name'],
|
||||||
|
'plan_id' => $r['plan_id'] ? (int)$r['plan_id'] : 0,
|
||||||
|
'status' => $r['status'] ?: 'active',
|
||||||
|
'started_at' => $r['started_at'] ?: '',
|
||||||
|
'expires_at' => $r['expires_at'] ?: '',
|
||||||
|
'has_sub' => $r['sub_id'] ? 1 : 0,
|
||||||
|
'block_reason' => $r['block_reason'] ?: '',
|
||||||
|
], JSON_UNESCAPED_UNICODE));
|
||||||
|
?>
|
||||||
|
<tr class="<?= (int)$r['shop_active'] === 0 ? 'shop-off' : '' ?>">
|
||||||
|
<td>
|
||||||
|
<div class="fw-bold"><?= e($r['shop_name']) ?></div>
|
||||||
|
<div class="muted-sm"><?= e($r['city']) ?> · <?= e($r['owner_name'] ?: '—') ?></div>
|
||||||
|
</td>
|
||||||
|
<td><?= $r['plan_name'] ? e($r['plan_name']) : '<span class="text-muted">—</span>' ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge <?= $meta['badge'] ?>"><?= $meta['label'] ?></span>
|
||||||
|
<?php if ($r['status'] === 'blocked' && $r['block_reason']): ?>
|
||||||
|
<i class="bx bx-info-circle text-danger" title="<?= e($r['block_reason']) ?>"></i>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($expires): ?>
|
||||||
|
<span class="<?= $expClass ?>"><?= date('d/m/Y', strtotime($expires)) ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= (int)$r['shop_active'] === 1 ? '<span class="text-success fw-bold">Sì</span>' : '<span class="text-muted">No</span>' ?></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">Gestisci</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><button class="dropdown-item js-manage" data-bs-toggle="modal" data-bs-target="#subModal" data-sub='<?= $payload ?>'>
|
||||||
|
<i class="bx bx-edit me-1"></i> <?= $r['sub_id'] ? 'Modifica abbonamento' : 'Assegna abbonamento' ?>
|
||||||
|
</button></li>
|
||||||
|
<?php if ($r['sub_id']): ?>
|
||||||
|
<li><button class="dropdown-item js-gift" data-bs-toggle="modal" data-bs-target="#giftModal" data-shop="<?= (int)$r['shop_id'] ?>" data-name="<?= e($r['shop_name']) ?>">
|
||||||
|
<i class="bx bx-gift me-1"></i> Regala mesi
|
||||||
|
</button></li>
|
||||||
|
<?php if ($r['status'] === 'blocked'): ?>
|
||||||
|
<li>
|
||||||
|
<form method="POST" action="admin_subscription_action.php">
|
||||||
|
<input type="hidden" name="action" value="unblock">
|
||||||
|
<input type="hidden" name="shop_id" value="<?= (int)$r['shop_id'] ?>">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<button class="dropdown-item text-success"><i class="bx bx-check-shield me-1"></i> Sblocca</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<?php else: ?>
|
||||||
|
<li><button class="dropdown-item text-danger js-block" data-bs-toggle="modal" data-bs-target="#blockModal" data-shop="<?= (int)$r['shop_id'] ?>" data-name="<?= e($r['shop_name']) ?>">
|
||||||
|
<i class="bx bx-block me-1"></i> Blocca (violazione)
|
||||||
|
</button></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach;
|
||||||
|
endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="muted-sm mt-2"><i class="bx bx-info-circle me-1"></i>I saloni scaduti vengono spenti automaticamente dallo script giornaliero. Qui puoi intervenire manualmente in qualsiasi momento.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div> <!-- /page-content -->
|
<?php include('include/footer.php'); ?>
|
||||||
</div> <!-- /page-wrapper -->
|
</div>
|
||||||
|
|
||||||
<?php if (file_exists('include/footer.php')) include('include/footer.php'); ?>
|
<!-- Modale assegna/modifica abbonamento -->
|
||||||
</div> <!-- /wrapper -->
|
<div class="modal fade" id="subModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form method="POST" action="admin_subscription_action.php" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Abbonamento — <span id="smShopName"></span></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="action" value="save">
|
||||||
|
<input type="hidden" name="shop_id" id="smShopId">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Piano</label>
|
||||||
|
<select name="plan_id" id="smPlan" class="form-select">
|
||||||
|
<option value="0">Nessuno (omaggio)</option>
|
||||||
|
<?php foreach ($plans as $p): ?>
|
||||||
|
<option value="<?= (int)$p['id'] ?>"><?= e($p['name']) ?> — €<?= number_format($p['price'], 2, ',', '.') ?>/<?= $p['billing_period'] === 'monthly' ? 'mese' : 'anno' ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="form-label">Stato</label>
|
||||||
|
<select name="status" id="smStatus" class="form-select">
|
||||||
|
<option value="active">Attivo</option>
|
||||||
|
<option value="trialing">In prova</option>
|
||||||
|
<option value="pending">In attesa</option>
|
||||||
|
<option value="expired">Scaduto</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<label class="form-label">Inizio</label>
|
||||||
|
<input type="date" name="started_at" id="smStarted" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-2">
|
||||||
|
<label class="form-label">Scadenza</label>
|
||||||
|
<input type="date" name="expires_at" id="smExpires" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="muted-sm mb-0"><i class="bx bx-info-circle me-1"></i>Salvando, il salone viene acceso o spento in base allo stato e alla scadenza.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button class="btn btn-primary">Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if (file_exists('jsinclude.php')) include('jsinclude.php'); ?>
|
<!-- Modale regala mesi -->
|
||||||
|
<div class="modal fade" id="giftModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form method="POST" action="admin_subscription_action.php" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Regala mesi</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="action" value="gift_months">
|
||||||
|
<input type="hidden" name="shop_id" id="gmShopId">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<p class="text-muted">Estende la scadenza di <strong id="gmShopName"></strong> e riattiva l'abbonamento.</p>
|
||||||
|
<label class="form-label">Mesi da regalare</label>
|
||||||
|
<input type="number" name="months" class="form-control" min="1" max="24" value="1" required>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button class="btn btn-primary">Regala</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modale blocca -->
|
||||||
|
<div class="modal fade" id="blockModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form method="POST" action="admin_subscription_action.php" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Blocca salone</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" name="action" value="block">
|
||||||
|
<input type="hidden" name="shop_id" id="bmShopId">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<p class="text-muted">Il salone <strong id="bmShopName"></strong> verrà spento e reso invisibile finché non lo sblocchi.</p>
|
||||||
|
<label class="form-label">Motivo (violazione)</label>
|
||||||
|
<input type="text" name="block_reason" class="form-control" placeholder="Es. Contenuti non conformi" maxlength="255">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button class="btn btn-danger">Blocca</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
<script>
|
||||||
|
var subModal = document.getElementById('subModal');
|
||||||
|
if (subModal) subModal.addEventListener('show.bs.modal', function(ev) {
|
||||||
|
var b = ev.relatedTarget;
|
||||||
|
if (!b || !b.classList.contains('js-manage')) return;
|
||||||
|
var d;
|
||||||
|
try {
|
||||||
|
d = JSON.parse(b.getAttribute('data-sub'));
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('smShopId').value = d.shop_id;
|
||||||
|
document.getElementById('smShopName').textContent = d.shop_name;
|
||||||
|
document.getElementById('smPlan').value = d.plan_id || 0;
|
||||||
|
document.getElementById('smStatus').value = d.status || 'active';
|
||||||
|
document.getElementById('smStarted').value = d.started_at || '';
|
||||||
|
document.getElementById('smExpires').value = d.expires_at || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
var giftModal = document.getElementById('giftModal');
|
||||||
|
if (giftModal) giftModal.addEventListener('show.bs.modal', function(ev) {
|
||||||
|
var b = ev.relatedTarget;
|
||||||
|
document.getElementById('gmShopId').value = b.getAttribute('data-shop');
|
||||||
|
document.getElementById('gmShopName').textContent = b.getAttribute('data-name');
|
||||||
|
});
|
||||||
|
|
||||||
|
var blockModal = document.getElementById('blockModal');
|
||||||
|
if (blockModal) blockModal.addEventListener('show.bs.modal', function(ev) {
|
||||||
|
var b = ev.relatedTarget;
|
||||||
|
document.getElementById('bmShopId').value = b.getAttribute('data-shop');
|
||||||
|
document.getElementById('bmShopName').textContent = b.getAttribute('data-name');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
// admin_user_action.php — Azioni admin sugli utenti
|
||||||
|
// Gestisce: cambio ruolo, cambio stato (attiva/blocca), reset password, elimina.
|
||||||
|
// Protetto: solo role_id = ROLE_ADMIN.
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// Solo admin
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFlash(string $type, string $text): void
|
||||||
|
{
|
||||||
|
$_SESSION['flash'] = ['type' => $type, 'text' => $text];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
setFlash('danger', 'Richiesta non valida.');
|
||||||
|
header("Location: admin_users.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target_id = (int)($_POST['user_id'] ?? 0);
|
||||||
|
$action = trim($_POST['action'] ?? '');
|
||||||
|
|
||||||
|
// Query string di ritorno (per mantenere ricerca/filtri)
|
||||||
|
$ret = trim($_POST['ret'] ?? '');
|
||||||
|
function back(string $ret): void
|
||||||
|
{
|
||||||
|
header("Location: admin_users.php" . ($ret !== '' ? '?' . $ret : ''));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($target_id <= 0 || $action === '') {
|
||||||
|
setFlash('danger', 'Dati azione mancanti.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sicurezza: l'admin non può agire su SE STESSO per azioni distruttive
|
||||||
|
// (evita di auto-bloccarsi, declassarsi o eliminarsi restando fuori dal pannello).
|
||||||
|
$isSelf = ($target_id === (int)$iduserlogin);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verifica che l'utente esista
|
||||||
|
$u = $pdo->prepare("SELECT id, role_id, status FROM auth_users WHERE id = ?");
|
||||||
|
$u->execute([$target_id]);
|
||||||
|
$target = $u->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$target) {
|
||||||
|
setFlash('danger', 'Utente non trovato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Cambio ruolo =====
|
||||||
|
if ($action === 'set_role') {
|
||||||
|
if ($isSelf) {
|
||||||
|
setFlash('warning', 'Non puoi cambiare il ruolo del tuo stesso account.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
$new_role = (int)($_POST['role_id'] ?? 0);
|
||||||
|
if (!in_array($new_role, [ROLE_ADMIN, ROLE_OWNER, ROLE_USER], true)) {
|
||||||
|
setFlash('danger', 'Ruolo non valido.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
$upd = $pdo->prepare("UPDATE auth_users SET role_id = ? WHERE id = ?");
|
||||||
|
$upd->execute([$new_role, $target_id]);
|
||||||
|
setFlash('success', 'Ruolo aggiornato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Cambio stato (attiva / blocca) =====
|
||||||
|
if ($action === 'set_status') {
|
||||||
|
if ($isSelf) {
|
||||||
|
setFlash('warning', 'Non puoi cambiare lo stato del tuo stesso account.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
$new_status = trim($_POST['status'] ?? '');
|
||||||
|
// Valori usati dall'app: "Active" = attivo, "banned" = bloccato
|
||||||
|
if (!in_array($new_status, ['Active', 'banned'], true)) {
|
||||||
|
setFlash('danger', 'Stato non valido.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
$upd = $pdo->prepare("UPDATE auth_users SET status = ? WHERE id = ?");
|
||||||
|
$upd->execute([$new_status, $target_id]);
|
||||||
|
setFlash('success', $new_status === 'Active' ? 'Utente attivato.' : 'Utente bloccato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Reset password =====
|
||||||
|
if ($action === 'reset_password') {
|
||||||
|
$new_pw = (string)($_POST['new_password'] ?? '');
|
||||||
|
if (strlen($new_pw) < 8) {
|
||||||
|
setFlash('danger', 'La password deve avere almeno 8 caratteri.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
// bcrypt, compatibile col login Laravel
|
||||||
|
$hash = password_hash($new_pw, PASSWORD_BCRYPT);
|
||||||
|
$upd = $pdo->prepare("UPDATE auth_users SET password = ? WHERE id = ?");
|
||||||
|
$upd->execute([$hash, $target_id]);
|
||||||
|
setFlash('success', 'Password reimpostata. Comunicala all\'utente in modo sicuro.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Elimina =====
|
||||||
|
if ($action === 'delete') {
|
||||||
|
if ($isSelf) {
|
||||||
|
setFlash('warning', 'Non puoi eliminare il tuo stesso account.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
$del = $pdo->prepare("DELETE FROM auth_users WHERE id = ?");
|
||||||
|
$del->execute([$target_id]);
|
||||||
|
setFlash('success', 'Utente eliminato.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFlash('danger', 'Azione non riconosciuta.');
|
||||||
|
back($ret);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('admin_user_action: ' . $e->getMessage());
|
||||||
|
// Errore tipico: eliminare un owner con saloni collegati (FK). Lo spieghiamo.
|
||||||
|
setFlash('danger', 'Operazione non riuscita. L\'utente potrebbe avere dati collegati (es. saloni) che ne impediscono l\'eliminazione.');
|
||||||
|
back($ret);
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// Solo admin
|
||||||
|
if ((int)$kindofrole !== ROLE_ADMIN) {
|
||||||
|
header("Location: index.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function e(?string $v): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
function getFlash(): ?array
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['flash'])) return null;
|
||||||
|
$f = $_SESSION['flash'];
|
||||||
|
unset($_SESSION['flash']);
|
||||||
|
return $f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Filtri ---
|
||||||
|
$q_search = trim($_GET['q'] ?? '');
|
||||||
|
$q_role = isset($_GET['role']) && $_GET['role'] !== '' ? (int)$_GET['role'] : 0;
|
||||||
|
$q_status = trim($_GET['status'] ?? '');
|
||||||
|
|
||||||
|
$where = ["1=1"];
|
||||||
|
$params = [];
|
||||||
|
if ($q_search !== '') {
|
||||||
|
$where[] = "(u.first_name LIKE ? OR u.last_name LIKE ? OR u.email LIKE ?)";
|
||||||
|
$like = '%' . $q_search . '%';
|
||||||
|
array_push($params, $like, $like, $like);
|
||||||
|
}
|
||||||
|
if ($q_role > 0) {
|
||||||
|
$where[] = "u.role_id = ?";
|
||||||
|
$params[] = $q_role;
|
||||||
|
}
|
||||||
|
if ($q_status !== '') {
|
||||||
|
$where[] = "u.status = ?";
|
||||||
|
$params[] = $q_status;
|
||||||
|
}
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
// --- Utenti ---
|
||||||
|
$sql = "
|
||||||
|
SELECT u.id, u.first_name, u.last_name, u.email, u.phone, u.avatar,
|
||||||
|
u.role_id, u.status, u.last_login, u.created_at,
|
||||||
|
r.display_name AS role_label,
|
||||||
|
(SELECT COUNT(*) FROM shops sh WHERE sh.owner_id = u.id) AS shops_count
|
||||||
|
FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON u.role_id = r.id
|
||||||
|
WHERE $whereSql
|
||||||
|
ORDER BY u.created_at DESC
|
||||||
|
";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Ruoli per i filtri/select
|
||||||
|
$roles = $pdo->query("SELECT id, display_name FROM auth_roles ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$flash = getFlash();
|
||||||
|
$ret = http_build_query(array_filter(['q' => $q_search, 'role' => $q_role ?: '', 'status' => $q_status]));
|
||||||
|
|
||||||
|
function roleBadge(int $role_id): string
|
||||||
|
{
|
||||||
|
switch ($role_id) {
|
||||||
|
case 1:
|
||||||
|
return 'bg-danger'; // Admin
|
||||||
|
case 2:
|
||||||
|
return 'bg-primary'; // Owner
|
||||||
|
case 3:
|
||||||
|
return 'bg-secondary'; // User
|
||||||
|
default:
|
||||||
|
return 'bg-dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!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'); ?>
|
||||||
|
<title>Gestione Utenti</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--soft-border: #e9edf3;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--soft-border);
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell .card-header {
|
||||||
|
background: linear-gradient(180deg, #fff, #fbfcff);
|
||||||
|
border-bottom: 1px solid var(--soft-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-sm {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #eef2f7;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table> :not(caption)>*>* {
|
||||||
|
padding: .85rem .75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-status {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-active {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st-banned {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</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-shell">
|
||||||
|
<div class="card-header p-3 p-md-4">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
|
<div class="page-title h5 mb-0"><i class="bx bx-group me-1"></i> Gestione Utenti</div>
|
||||||
|
<span class="text-muted small"><?= count($users) ?> utenti</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= e($flash['type']) ?> alert-dismissible fade show" role="alert">
|
||||||
|
<?= e($flash['text']) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Filtri -->
|
||||||
|
<form method="GET" class="row g-2 mb-3">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<input type="text" name="q" class="form-control" placeholder="Cerca nome, cognome o email…" value="<?= e($q_search) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<select name="role" class="form-select">
|
||||||
|
<option value="">Tutti i ruoli</option>
|
||||||
|
<?php foreach ($roles as $r): ?>
|
||||||
|
<option value="<?= (int)$r['id'] ?>" <?= $q_role === (int)$r['id'] ? 'selected' : '' ?>>
|
||||||
|
<?= e($r['display_name'] ?: ('Ruolo ' . $r['id'])) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<select name="status" class="form-select">
|
||||||
|
<option value="">Tutti gli stati</option>
|
||||||
|
<option value="Active" <?= $q_status === 'Active' ? 'selected' : '' ?>>Attivi</option>
|
||||||
|
<option value="banned" <?= $q_status === 'banned' ? 'selected' : '' ?>>Bloccati</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-grid">
|
||||||
|
<button class="btn btn-primary"><i class="bx bx-search"></i> Filtra</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Tabella -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Utente</th>
|
||||||
|
<th>Ruolo</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Saloni</th>
|
||||||
|
<th>Ultimo accesso</th>
|
||||||
|
<th class="text-end">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if (empty($users)): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center text-muted py-4">Nessun utente trovato.</td>
|
||||||
|
</tr>
|
||||||
|
<?php else: foreach ($users as $u): ?>
|
||||||
|
<?php
|
||||||
|
$fullName = trim(($u['first_name'] ?? '') . ' ' . ($u['last_name'] ?? '')) ?: '—';
|
||||||
|
$initial = strtoupper(mb_substr($u['first_name'] ?: $u['email'], 0, 1));
|
||||||
|
$isActive = ($u['status'] === 'Active');
|
||||||
|
$isSelf = ((int)$u['id'] === (int)$iduserlogin);
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<?php if (!empty($u['avatar'])): ?>
|
||||||
|
<img src="<?= e($u['avatar']) ?>" class="avatar-sm" alt="">
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="avatar-sm"><?= e($initial) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold"><?= e($fullName) ?> <?php if ($isSelf): ?><span class="badge bg-light text-dark ms-1">tu</span><?php endif; ?></div>
|
||||||
|
<div class="user-email"><?= e($u['email']) ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge <?= roleBadge((int)$u['role_id']) ?>"><?= e($u['role_label'] ?: ('Ruolo ' . $u['role_id'])) ?></span></td>
|
||||||
|
<td>
|
||||||
|
<span class="dot-status <?= $isActive ? 'st-active' : 'st-banned' ?>"></span>
|
||||||
|
<?= $isActive ? 'Attivo' : 'Bloccato' ?>
|
||||||
|
</td>
|
||||||
|
<td><?= (int)$u['shops_count'] > 0 ? (int)$u['shops_count'] : '—' ?></td>
|
||||||
|
<td class="text-muted small"><?= $u['last_login'] ? date('d/m/Y H:i', strtotime($u['last_login'])) : 'Mai' ?></td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" <?= $isSelf ? '' : '' ?>>
|
||||||
|
Gestisci
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><button class="dropdown-item" data-bs-toggle="modal" data-bs-target="#roleModal"
|
||||||
|
data-id="<?= (int)$u['id'] ?>" data-name="<?= e($fullName) ?>" data-role="<?= (int)$u['role_id'] ?>"
|
||||||
|
<?= $isSelf ? 'disabled' : '' ?>><i class="bx bx-user-pin me-1"></i> Cambia ruolo</button></li>
|
||||||
|
<li><button class="dropdown-item" data-bs-toggle="modal" data-bs-target="#pwModal"
|
||||||
|
data-id="<?= (int)$u['id'] ?>" data-name="<?= e($fullName) ?>"><i class="bx bx-key me-1"></i> Reset password</button></li>
|
||||||
|
<?php if (!$isSelf): ?>
|
||||||
|
<li>
|
||||||
|
<form method="POST" action="admin_user_action.php" class="d-inline">
|
||||||
|
<input type="hidden" name="action" value="set_status">
|
||||||
|
<input type="hidden" name="user_id" value="<?= (int)$u['id'] ?>">
|
||||||
|
<input type="hidden" name="status" value="<?= $isActive ? 'banned' : 'Active' ?>">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<button class="dropdown-item">
|
||||||
|
<?php if ($isActive): ?><i class="bx bx-block me-1"></i> Blocca<?php else: ?><i class="bx bx-check-circle me-1"></i> Attiva<?php endif; ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form method="POST" action="admin_user_action.php" onsubmit="return confirm('Eliminare definitivamente questo utente?');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<input type="hidden" name="user_id" value="<?= (int)$u['id'] ?>">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<button class="dropdown-item text-danger"><i class="bx bx-trash me-1"></i> Elimina</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach;
|
||||||
|
endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modale cambio ruolo -->
|
||||||
|
<div class="modal fade" id="roleModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form method="POST" action="admin_user_action.php" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Cambia ruolo</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-muted mb-2">Utente: <strong id="roleUserName"></strong></p>
|
||||||
|
<input type="hidden" name="action" value="set_role">
|
||||||
|
<input type="hidden" name="user_id" id="roleUserId">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<select name="role_id" id="roleSelect" class="form-select">
|
||||||
|
<option value="1">Admin</option>
|
||||||
|
<option value="2">Owner (titolare)</option>
|
||||||
|
<option value="3">User (cliente)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button class="btn btn-primary">Salva ruolo</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modale reset password -->
|
||||||
|
<div class="modal fade" id="pwModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form method="POST" action="admin_user_action.php" class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Reset password</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-muted mb-2">Utente: <strong id="pwUserName"></strong></p>
|
||||||
|
<input type="hidden" name="action" value="reset_password">
|
||||||
|
<input type="hidden" name="user_id" id="pwUserId">
|
||||||
|
<input type="hidden" name="ret" value="<?= e($ret) ?>">
|
||||||
|
<label class="form-label">Nuova password (min. 8 caratteri)</label>
|
||||||
|
<input type="text" name="new_password" class="form-control" minlength="8" required
|
||||||
|
placeholder="Es. HairBook2026!">
|
||||||
|
<small class="text-muted">La password è visibile così puoi copiarla e comunicarla all'utente.</small>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||||
|
<button class="btn btn-primary">Reimposta</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
<script>
|
||||||
|
// Popola la modale ruolo
|
||||||
|
var roleModal = document.getElementById('roleModal');
|
||||||
|
if (roleModal) roleModal.addEventListener('show.bs.modal', function(ev) {
|
||||||
|
var b = ev.relatedTarget;
|
||||||
|
document.getElementById('roleUserId').value = b.getAttribute('data-id');
|
||||||
|
document.getElementById('roleUserName').textContent = b.getAttribute('data-name');
|
||||||
|
document.getElementById('roleSelect').value = b.getAttribute('data-role');
|
||||||
|
});
|
||||||
|
// Popola la modale password
|
||||||
|
var pwModal = document.getElementById('pwModal');
|
||||||
|
if (pwModal) pwModal.addEventListener('show.bs.modal', function(ev) {
|
||||||
|
var b = ev.relatedTarget;
|
||||||
|
document.getElementById('pwUserId').value = b.getAttribute('data-id');
|
||||||
|
document.getElementById('pwUserName').textContent = b.getAttribute('data-name');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -15,6 +15,112 @@
|
|||||||
<!-- Theme Style CSS -->
|
<!-- Theme Style CSS -->
|
||||||
<link rel="stylesheet" href="assets/css/dark-theme.css" />
|
<link rel="stylesheet" href="assets/css/dark-theme.css" />
|
||||||
<link rel="stylesheet" href="assets/css/semi-dark.css" />
|
<link rel="stylesheet" href="assets/css/semi-dark.css" />
|
||||||
<link rel="stylesheet" href="assets/css/header-colors.css" />
|
<!-- header-colors.css disattivato: applicava uno skin colore (il verde) che sovrascriveva il tema HairBook -->
|
||||||
|
<!-- <link rel="stylesheet" href="assets/css/header-colors.css" /> -->
|
||||||
<!-- Datatables -->
|
<!-- Datatables -->
|
||||||
<link href="https://cdn.datatables.net/1.10.25/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
<link href="https://cdn.datatables.net/1.10.25/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* ===== Palette HairBook (magenta + arancione) ===== */
|
||||||
|
:root {
|
||||||
|
--hb-magenta: #e6007e;
|
||||||
|
--hb-pink: #e11d6b;
|
||||||
|
--hb-orange: #ff6a3d;
|
||||||
|
--hb-dark: #b3005f;
|
||||||
|
--hb-grad: linear-gradient(120deg, #e6007e 0%, #ff3d78 55%, #ff6a3d 100%);
|
||||||
|
--hb-grad-v: linear-gradient(180deg, #e6007e 0%, #b3005f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Topbar superiore: gradiente caldo ===== */
|
||||||
|
body .topbar,
|
||||||
|
body .topbar .navbar {
|
||||||
|
background: var(--hb-grad) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* testo e icone in bianco per contrasto sul gradiente */
|
||||||
|
body .topbar .navbar .nav-link,
|
||||||
|
body .topbar .navbar .nav-link i,
|
||||||
|
body .topbar .search-bar .btn,
|
||||||
|
body .topbar .search-bar .btn i,
|
||||||
|
body .topbar .mobile-toggle-menu i,
|
||||||
|
body .topbar .user-info .user-name {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .topbar .user-info .designattion {
|
||||||
|
color: #ffe1ec !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .topbar .navbar .nav-link:hover,
|
||||||
|
body .topbar .navbar .nav-link:hover i {
|
||||||
|
color: #ffe1ec !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar: magenta coordinato ===== */
|
||||||
|
body .sidebar-wrapper,
|
||||||
|
body .sidebar-wrapper .sidebar-header,
|
||||||
|
body .sidebar-wrapper.bg-theme,
|
||||||
|
body .sidebar-wrapper[class*="bg-theme"] {
|
||||||
|
background: var(--hb-grad-v) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .sidebar-header .logo-text {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .sidebar-wrapper .metismenu a {
|
||||||
|
color: #ffd9e8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .sidebar-wrapper .metismenu a:hover,
|
||||||
|
body .sidebar-wrapper .metismenu li.mm-active>a,
|
||||||
|
body .sidebar-wrapper .metismenu a.active {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background: rgba(255, 106, 61, .22) !important;
|
||||||
|
border-left: 3px solid var(--hb-orange) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body .sidebar-wrapper .menu-label {
|
||||||
|
color: #ffb0d0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Bottoni primari ===== */
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--hb-magenta);
|
||||||
|
border-color: var(--hb-magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-primary:focus,
|
||||||
|
.btn-primary:active {
|
||||||
|
background: var(--hb-orange) !important;
|
||||||
|
border-color: var(--hb-orange) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
color: var(--hb-magenta);
|
||||||
|
border-color: var(--hb-magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover {
|
||||||
|
background: var(--hb-magenta);
|
||||||
|
border-color: var(--hb-magenta);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Link e accenti ===== */
|
||||||
|
a {
|
||||||
|
color: var(--hb-magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--hb-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--hb-magenta) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary {
|
||||||
|
background: var(--hb-magenta) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -152,6 +152,12 @@ $isUser = ($kindofrole == ROLE_USER);
|
|||||||
<div class="menu-title">Giorni chiusi</div>
|
<div class="menu-title">Giorni chiusi</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="my_subscription.php">
|
||||||
|
<div class="parent-icon"><i class="bx bx-cog"></i></div>
|
||||||
|
<div class="menu-title">Abbonamenti</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="salon_settings.php">
|
<a href="salon_settings.php">
|
||||||
<div class="parent-icon"><i class="bx bx-cog"></i></div>
|
<div class="parent-icon"><i class="bx bx-cog"></i></div>
|
||||||
@@ -175,6 +181,18 @@ $isUser = ($kindofrole == ROLE_USER);
|
|||||||
<div class="menu-title">Gestione Saloni</div>
|
<div class="menu-title">Gestione Saloni</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="admin_subscriptions.php">
|
||||||
|
<div class="parent-icon"><i class="bx bx-store"></i></div>
|
||||||
|
<div class="menu-title">Gestione Abbonamenti</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="admin_plans.php">
|
||||||
|
<div class="parent-icon"><i class="bx bx-store"></i></div>
|
||||||
|
<div class="menu-title">Gestione Piani</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Voci generiche -->
|
<!-- Voci generiche -->
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<?php
|
||||||
|
// my_subscription.php — L'owner vede lo stato del suo abbonamento (area gestionale)
|
||||||
|
// Storico pagamenti predisposto ma non attivo (arriverà con Stripe).
|
||||||
|
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$dbHandler = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $dbHandler->getConnection();
|
||||||
|
|
||||||
|
if (!isset($iduserlogin)) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function e(?string $v): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
function getFlash(): ?array
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['flash'])) return null;
|
||||||
|
$f = $_SESSION['flash'];
|
||||||
|
unset($_SESSION['flash']);
|
||||||
|
return $f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salone dell'owner (il primo, coerente con appointments.php)
|
||||||
|
$stmt = $pdo->prepare("SELECT id, name FROM shops WHERE owner_id = ? ORDER BY created_at ASC LIMIT 1");
|
||||||
|
$stmt->execute([$iduserlogin]);
|
||||||
|
$shop = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$shop) {
|
||||||
|
header("Location: onboarding_salon.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shop_id = (int)$shop['id'];
|
||||||
|
$shop_name = $shop['name'];
|
||||||
|
|
||||||
|
// Abbonamento corrente + piano
|
||||||
|
$subStmt = $pdo->prepare("
|
||||||
|
SELECT sub.status, sub.started_at, sub.expires_at, sub.block_reason,
|
||||||
|
p.name AS plan_name, p.price, p.billing_period
|
||||||
|
FROM shop_subscriptions sub
|
||||||
|
LEFT JOIN subscription_plans p ON sub.plan_id = p.id
|
||||||
|
WHERE sub.shop_id = ?
|
||||||
|
");
|
||||||
|
$subStmt->execute([$shop_id]);
|
||||||
|
$sub = $subStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Meta stato
|
||||||
|
function subMeta(?string $status): array
|
||||||
|
{
|
||||||
|
switch ($status) {
|
||||||
|
case 'active':
|
||||||
|
return ['badge' => 'bg-success', 'label' => 'Attivo', 'ico' => 'bx-check-circle'];
|
||||||
|
case 'trialing':
|
||||||
|
return ['badge' => 'bg-info text-dark', 'label' => 'In prova', 'ico' => 'bx-time'];
|
||||||
|
case 'expired':
|
||||||
|
return ['badge' => 'bg-warning text-dark', 'label' => 'Scaduto', 'ico' => 'bx-error-circle'];
|
||||||
|
case 'blocked':
|
||||||
|
return ['badge' => 'bg-danger', 'label' => 'Bloccato', 'ico' => 'bx-block'];
|
||||||
|
case 'pending':
|
||||||
|
return ['badge' => 'bg-secondary', 'label' => 'In attesa', 'ico' => 'bx-hourglass'];
|
||||||
|
default:
|
||||||
|
return ['badge' => 'bg-light text-dark', 'label' => 'Nessun abbonamento', 'ico' => 'bx-info-circle'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$meta = subMeta($sub['status'] ?? null);
|
||||||
|
|
||||||
|
// Giorni alla scadenza
|
||||||
|
$daysLeft = null;
|
||||||
|
$expLabel = '—';
|
||||||
|
if (!empty($sub['expires_at'])) {
|
||||||
|
$expLabel = date('d/m/Y', strtotime($sub['expires_at']));
|
||||||
|
$today = new DateTime('today');
|
||||||
|
$exp = new DateTime($sub['expires_at']);
|
||||||
|
$daysLeft = (int)$today->diff($exp)->format('%r%a');
|
||||||
|
}
|
||||||
|
$flash = getFlash();
|
||||||
|
?>
|
||||||
|
<!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'); ?>
|
||||||
|
<title>Il mio abbonamento - <?= e($shop_name) ?></title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--soft-border: #e9edf3;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shell {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--soft-border);
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-hero {
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 28px;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(120deg, #e6007e, #ff6a3d);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-hero .plan {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-hero .price {
|
||||||
|
font-size: 15px;
|
||||||
|
opacity: .95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: .35rem .8rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 1px solid var(--soft-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row .k {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row .v {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2b2540;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-soft {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-size: 14.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-soon {
|
||||||
|
color: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exp-past {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locked {
|
||||||
|
opacity: .65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.soon-badge {
|
||||||
|
background: #fff0e8;
|
||||||
|
color: #ff6a3d;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</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="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<div class="page-title h5 mb-0"><i class="bx bx-credit-card me-1"></i> Il mio abbonamento</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($flash): ?>
|
||||||
|
<div class="alert alert-<?= e($flash['type']) ?> alert-dismissible fade show" role="alert">
|
||||||
|
<?= e($flash['text']) ?>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!$sub): ?>
|
||||||
|
<!-- Nessun abbonamento -->
|
||||||
|
<div class="card-shell p-4 text-center">
|
||||||
|
<i class="bx bx-info-circle" style="font-size:48px;color:#ff6a3d;"></i>
|
||||||
|
<h4 class="mt-2 mb-1">Nessun abbonamento attivo</h4>
|
||||||
|
<p class="text-muted mb-3">Il tuo salone non ha ancora un abbonamento. Attivane uno per essere visibile ai clienti.</p>
|
||||||
|
<div><a href="../pricing.php" class="btn btn-primary">Vedi i piani</a></div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<!-- Colonna sinistra: stato -->
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="sub-hero mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<div class="plan"><?= e($sub['plan_name'] ?: 'Piano personalizzato') ?></div>
|
||||||
|
<?php if ($sub['price'] !== null): ?>
|
||||||
|
<div class="price">€<?= number_format((float)$sub['price'], 2, ',', '.') ?>/<?= $sub['billing_period'] === 'yearly' ? 'anno' : 'mese' ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<span class="sub-badge" style="background:rgba(255,255,255,.25);">
|
||||||
|
<?= e($meta['label']) ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-shell p-4">
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="k">Stato</span>
|
||||||
|
<span class="v"><span class="badge <?= $meta['badge'] ?>"><?= $meta['label'] ?></span></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="k">Attivo dal</span>
|
||||||
|
<span class="v"><?= !empty($sub['started_at']) ? date('d/m/Y', strtotime($sub['started_at'])) : '—' ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="k">Scadenza</span>
|
||||||
|
<span class="v">
|
||||||
|
<?= $expLabel ?>
|
||||||
|
<?php if ($daysLeft !== null): ?>
|
||||||
|
<?php if ($daysLeft < 0): ?>
|
||||||
|
<small class="exp-past">(scaduto)</small>
|
||||||
|
<?php elseif ($daysLeft <= 7): ?>
|
||||||
|
<small class="exp-soon">(tra <?= $daysLeft ?> giorni)</small>
|
||||||
|
<?php else: ?>
|
||||||
|
<small class="text-muted">(tra <?= $daysLeft ?> giorni)</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avvisi contestuali -->
|
||||||
|
<?php if ($sub['status'] === 'blocked'): ?>
|
||||||
|
<div class="alert-soft mt-3" style="background:#fdecef;color:#b3143a;border:1px solid #f5c2cd;">
|
||||||
|
<i class="bx bx-block me-1"></i> Il tuo salone è stato sospeso dall'amministrazione.
|
||||||
|
<?php if (!empty($sub['block_reason'])): ?><br><strong>Motivo:</strong> <?= e($sub['block_reason']) ?><?php endif; ?>
|
||||||
|
<br>Contatta il supporto per maggiori informazioni.
|
||||||
|
</div>
|
||||||
|
<?php elseif ($sub['status'] === 'expired'): ?>
|
||||||
|
<div class="alert-soft mt-3" style="background:#fff8e6;color:#9a7a1a;border:1px solid #ffe4a3;">
|
||||||
|
<i class="bx bx-error-circle me-1"></i> Il tuo abbonamento è scaduto e il salone non è più visibile ai clienti. Rinnova per riattivarlo.
|
||||||
|
</div>
|
||||||
|
<?php elseif ($daysLeft !== null && $daysLeft >= 0 && $daysLeft <= 7): ?>
|
||||||
|
<div class="alert-soft mt-3" style="background:#fff8e6;color:#9a7a1a;border:1px solid #ffe4a3;">
|
||||||
|
<i class="bx bx-time me-1"></i> Il tuo abbonamento scade tra <?= $daysLeft ?> giorni. Rinnova per non interrompere il servizio.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Rinnovo: predisposto per Stripe -->
|
||||||
|
<div class="mt-3">
|
||||||
|
<button class="btn btn-primary" disabled title="Disponibile a breve">
|
||||||
|
<i class="bx bx-refresh me-1"></i> Rinnova abbonamento
|
||||||
|
</button>
|
||||||
|
<span class="soon-badge ms-2">presto con pagamento online</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colonna destra: storico pagamenti (predisposto) -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="card-shell p-4 locked">
|
||||||
|
<h5 class="fw-bold mb-3"><i class="bx bx-receipt me-1"></i> Storico pagamenti <span class="soon-badge ms-1">presto</span></h5>
|
||||||
|
<div class="text-center py-4 text-muted">
|
||||||
|
<i class="bx bx-receipt" style="font-size:44px;opacity:.4;"></i>
|
||||||
|
<p class="mt-2 mb-0">Qui vedrai le tue fatture e i pagamenti effettuati non appena attiveremo il pagamento online.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -192,6 +192,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['action'])) { // norm
|
|||||||
$longitude = !empty($_POST['longitude']) ? (float)$_POST['longitude'] : null;
|
$longitude = !empty($_POST['longitude']) ? (float)$_POST['longitude'] : null;
|
||||||
$active = isset($_POST['active']) ? 1 : 0;
|
$active = isset($_POST['active']) ? 1 : 0;
|
||||||
|
|
||||||
|
// Target pubblico servito (uomo/donna/bambino/unisex). Arriva come array di checkbox.
|
||||||
|
$allowedTargets = ['uomo', 'donna', 'bambino', 'unisex'];
|
||||||
|
$targetArr = array_values(array_intersect($allowedTargets, (array)($_POST['target'] ?? [])));
|
||||||
|
if (empty($targetArr)) $targetArr = ['unisex']; // fallback: mai vuoto
|
||||||
|
$target = implode(',', $targetArr);
|
||||||
|
|
||||||
// Validazioni
|
// Validazioni
|
||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
throw new Exception("Nome salone obbligatorio.");
|
throw new Exception("Nome salone obbligatorio.");
|
||||||
@@ -262,7 +268,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['action'])) { // norm
|
|||||||
// Update DB
|
// Update DB
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE shops SET
|
UPDATE shops SET
|
||||||
name = ?, description = ?, address = ?, address_extra = ?, city = ?, province = ?,
|
name = ?, description = ?, target = ?, address = ?, address_extra = ?, city = ?, province = ?,
|
||||||
zip_code = ?, phone = ?, mobile = ?, email = ?, website = ?, instagram = ?, facebook = ?,
|
zip_code = ?, phone = ?, mobile = ?, email = ?, website = ?, instagram = ?, facebook = ?,
|
||||||
google_maps_url = ?, latitude = ?, longitude = ?, logo = ?, cover_image = ?, active = ?,
|
google_maps_url = ?, latitude = ?, longitude = ?, logo = ?, cover_image = ?, active = ?,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
@@ -271,6 +277,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['action'])) { // norm
|
|||||||
$ok = $stmt->execute([
|
$ok = $stmt->execute([
|
||||||
$name,
|
$name,
|
||||||
$description ?: null,
|
$description ?: null,
|
||||||
|
$target,
|
||||||
$address,
|
$address,
|
||||||
$address_extra ?: null,
|
$address_extra ?: null,
|
||||||
$city,
|
$city,
|
||||||
@@ -373,6 +380,24 @@ $flash = getFlash();
|
|||||||
<textarea class="form-control" name="description" rows="4"><?= htmlspecialchars($shop['description'] ?? '') ?></textarea>
|
<textarea class="form-control" name="description" rows="4"><?= htmlspecialchars($shop['description'] ?? '') ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label d-block">A chi si rivolge il salone</label>
|
||||||
|
<?php $shopTargets = array_filter(array_map('trim', explode(',', $shop['target'] ?? ''))); ?>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<?php
|
||||||
|
$targetOptions = ['uomo' => 'Uomo', 'donna' => 'Donna', 'bambino' => 'Bambini', 'unisex' => 'Unisex'];
|
||||||
|
foreach ($targetOptions as $val => $lbl):
|
||||||
|
?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="target[]" value="<?= $val ?>"
|
||||||
|
id="target_<?= $val ?>" <?= in_array($val, $shopTargets) ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="target_<?= $val ?>"><?= $lbl ?></label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted">Puoi selezionare più opzioni. "Unisex" indica uomo e donna insieme.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label class="form-label">Indirizzo <span class="text-danger">*</span></label>
|
<label class="form-label">Indirizzo <span class="text-danger">*</span></label>
|
||||||
|
|||||||
@@ -94,6 +94,25 @@ function uniqueSlug(PDO $pdo, int $shop_id, string $baseSlug, int $excludeId = 0
|
|||||||
return $slug;
|
return $slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function targetBadges(?string $target): string
|
||||||
|
{
|
||||||
|
$map = [
|
||||||
|
'uomo' => ['Uomo', '#3b82f6'],
|
||||||
|
'donna' => ['Donna', '#ec4899'],
|
||||||
|
'bambino' => ['Bambini', '#f59e0b'],
|
||||||
|
'unisex' => ['Unisex', '#8b5cf6'],
|
||||||
|
];
|
||||||
|
$vals = array_filter(array_map('trim', explode(',', (string)$target)));
|
||||||
|
if (empty($vals)) return '<span class="text-muted">-</span>';
|
||||||
|
$out = '';
|
||||||
|
foreach ($vals as $v) {
|
||||||
|
if (!isset($map[$v])) continue;
|
||||||
|
[$lbl, $col] = $map[$v];
|
||||||
|
$out .= '<span class="badge me-1" style="background:' . $col . ';">' . $lbl . '</span>';
|
||||||
|
}
|
||||||
|
return $out ?: '<span class="text-muted">-</span>';
|
||||||
|
}
|
||||||
|
|
||||||
function clampInt($val, int $min, int $max, int $fallback): int
|
function clampInt($val, int $min, int $max, int $fallback): int
|
||||||
{
|
{
|
||||||
if ($val === null || $val === '') return $fallback;
|
if ($val === null || $val === '') return $fallback;
|
||||||
@@ -134,6 +153,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
$order = clampInt($_POST['order'] ?? null, 0, 9999, 0);
|
$order = clampInt($_POST['order'] ?? null, 0, 9999, 0);
|
||||||
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
||||||
|
|
||||||
|
// Target (uomo/donna/bambino/unisex) — array di checkbox
|
||||||
|
$allowedTargets = ['uomo', 'donna', 'bambino', 'unisex'];
|
||||||
|
$targetArr = array_values(array_intersect($allowedTargets, (array)($_POST['target'] ?? [])));
|
||||||
|
if (empty($targetArr)) $targetArr = ['unisex'];
|
||||||
|
$target = implode(',', $targetArr);
|
||||||
|
|
||||||
// Validazioni base
|
// Validazioni base
|
||||||
if ($name === '') {
|
if ($name === '') {
|
||||||
setFlash('danger', "Il nome del servizio è obbligatorio.");
|
setFlash('danger', "Il nome del servizio è obbligatorio.");
|
||||||
@@ -163,8 +188,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
if ($action === 'add') {
|
if ($action === 'add') {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO services
|
INSERT INTO services
|
||||||
(shop_id, name, slug, description, duration_minutes, price, price_max, category, is_active, color_hex, `order`)
|
(shop_id, name, slug, description, duration_minutes, price, price_max, category, target, is_active, color_hex, `order`)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$ok = $stmt->execute([
|
$ok = $stmt->execute([
|
||||||
$shop_id,
|
$shop_id,
|
||||||
@@ -175,6 +200,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
$price,
|
$price,
|
||||||
$price_max,
|
$price_max,
|
||||||
$category !== '' ? $category : null,
|
$category !== '' ? $category : null,
|
||||||
|
$target,
|
||||||
$is_active,
|
$is_active,
|
||||||
$color_hex !== '' ? $color_hex : null,
|
$color_hex !== '' ? $color_hex : null,
|
||||||
$order
|
$order
|
||||||
@@ -193,7 +219,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE services
|
UPDATE services
|
||||||
SET name = ?, slug = ?, description = ?, duration_minutes = ?, price = ?, price_max = ?,
|
SET name = ?, slug = ?, description = ?, duration_minutes = ?, price = ?, price_max = ?,
|
||||||
category = ?, is_active = ?, color_hex = ?, `order` = ?, updated_at = NOW()
|
category = ?, target = ?, is_active = ?, color_hex = ?, `order` = ?, updated_at = NOW()
|
||||||
WHERE id = ? AND shop_id = ?
|
WHERE id = ? AND shop_id = ?
|
||||||
");
|
");
|
||||||
$ok = $stmt->execute([
|
$ok = $stmt->execute([
|
||||||
@@ -204,6 +230,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
$price,
|
$price,
|
||||||
$price_max,
|
$price_max,
|
||||||
$category !== '' ? $category : null,
|
$category !== '' ? $category : null,
|
||||||
|
$target,
|
||||||
$is_active,
|
$is_active,
|
||||||
$color_hex !== '' ? $color_hex : null,
|
$color_hex !== '' ? $color_hex : null,
|
||||||
$order,
|
$order,
|
||||||
@@ -247,7 +274,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
// Fetch services
|
// Fetch services
|
||||||
// ===========================
|
// ===========================
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT id, name, slug, description, duration_minutes, price, price_max, category, is_active, color_hex, `order`
|
SELECT id, name, slug, description, duration_minutes, price, price_max, category, target, is_active, color_hex, `order`
|
||||||
FROM services
|
FROM services
|
||||||
WHERE shop_id = ?
|
WHERE shop_id = ?
|
||||||
ORDER BY `order` ASC, name ASC
|
ORDER BY `order` ASC, name ASC
|
||||||
@@ -313,6 +340,7 @@ $flash = getFlash();
|
|||||||
<th>Durata</th>
|
<th>Durata</th>
|
||||||
<th>Prezzo</th>
|
<th>Prezzo</th>
|
||||||
<th>Categoria</th>
|
<th>Categoria</th>
|
||||||
|
<th>Rivolto a</th>
|
||||||
<th>Attivo</th>
|
<th>Attivo</th>
|
||||||
<th>Colore</th>
|
<th>Colore</th>
|
||||||
<th>Azioni</th>
|
<th>Azioni</th>
|
||||||
@@ -336,6 +364,7 @@ $flash = getFlash();
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td><?= htmlspecialchars($s['category'] ?? '-') ?></td>
|
<td><?= htmlspecialchars($s['category'] ?? '-') ?></td>
|
||||||
|
<td><?= targetBadges($s['target'] ?? '') ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ((int)$s['is_active'] === 1): ?>
|
<?php if ((int)$s['is_active'] === 1): ?>
|
||||||
<span class="badge bg-success">Sì</span>
|
<span class="badge bg-success">Sì</span>
|
||||||
@@ -363,6 +392,7 @@ $flash = getFlash();
|
|||||||
"price" => (string)$s["price"],
|
"price" => (string)$s["price"],
|
||||||
"price_max" => $s["price_max"] !== null ? (string)$s["price_max"] : "",
|
"price_max" => $s["price_max"] !== null ? (string)$s["price_max"] : "",
|
||||||
"category" => $s["category"] ?? "",
|
"category" => $s["category"] ?? "",
|
||||||
|
"target" => $s["target"] ?? "",
|
||||||
"is_active" => (int)$s["is_active"],
|
"is_active" => (int)$s["is_active"],
|
||||||
"color_hex" => $s["color_hex"] ?? "",
|
"color_hex" => $s["color_hex"] ?? "",
|
||||||
"order" => (int)$s["order"],
|
"order" => (int)$s["order"],
|
||||||
@@ -437,6 +467,18 @@ $flash = getFlash();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold d-block">Rivolto a</label>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<?php foreach (['uomo' => 'Uomo', 'donna' => 'Donna', 'bambino' => 'Bambini', 'unisex' => 'Unisex'] as $val => $lbl): ?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="target[]" value="<?= $val ?>" id="add_target_<?= $val ?>" <?= $val === 'unisex' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="add_target_<?= $val ?>"><?= $lbl ?></label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<label class="form-label fw-bold">Colore (hex)</label>
|
<label class="form-label fw-bold">Colore (hex)</label>
|
||||||
@@ -510,6 +552,18 @@ $flash = getFlash();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold d-block">Rivolto a</label>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<?php foreach (['uomo' => 'Uomo', 'donna' => 'Donna', 'bambino' => 'Bambini', 'unisex' => 'Unisex'] as $val => $lbl): ?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input edit-target" type="checkbox" name="target[]" value="<?= $val ?>" id="edit_target_<?= $val ?>">
|
||||||
|
<label class="form-check-label" for="edit_target_<?= $val ?>"><?= $lbl ?></label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<label class="form-label fw-bold">Colore (hex)</label>
|
<label class="form-label fw-bold">Colore (hex)</label>
|
||||||
@@ -566,6 +620,13 @@ $flash = getFlash();
|
|||||||
document.getElementById('edit_price').value = data.price || '';
|
document.getElementById('edit_price').value = data.price || '';
|
||||||
document.getElementById('edit_price_max').value = data.price_max || '';
|
document.getElementById('edit_price_max').value = data.price_max || '';
|
||||||
document.getElementById('edit_category').value = data.category || '';
|
document.getElementById('edit_category').value = data.category || '';
|
||||||
|
// Spunta i target del servizio
|
||||||
|
var targets = (data.target || '').split(',').map(function(t) {
|
||||||
|
return t.trim();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.edit-target').forEach(function(cb) {
|
||||||
|
cb.checked = targets.indexOf(cb.value) !== -1;
|
||||||
|
});
|
||||||
document.getElementById('edit_color_hex').value = data.color_hex || '';
|
document.getElementById('edit_color_hex').value = data.color_hex || '';
|
||||||
document.getElementById('edit_order').value = data.order || 0;
|
document.getElementById('edit_order').value = data.order || 0;
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,26 @@ function getFlash(): ?array
|
|||||||
return $f;
|
return $f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rende i target (SET "uomo,donna") come badge colorati per la tabella
|
||||||
|
function targetBadges(?string $target): string
|
||||||
|
{
|
||||||
|
$map = [
|
||||||
|
'uomo' => ['Uomo', '#3b82f6'],
|
||||||
|
'donna' => ['Donna', '#ec4899'],
|
||||||
|
'bambino' => ['Bambini', '#f59e0b'],
|
||||||
|
'unisex' => ['Unisex', '#8b5cf6'],
|
||||||
|
];
|
||||||
|
$vals = array_filter(array_map('trim', explode(',', (string)$target)));
|
||||||
|
if (empty($vals)) return '<span class="text-muted">-</span>';
|
||||||
|
$out = '';
|
||||||
|
foreach ($vals as $v) {
|
||||||
|
if (!isset($map[$v])) continue;
|
||||||
|
[$lbl, $col] = $map[$v];
|
||||||
|
$out .= '<span class="badge me-1" style="background:' . $col . ';">' . $lbl . '</span>';
|
||||||
|
}
|
||||||
|
return $out ?: '<span class="text-muted">-</span>';
|
||||||
|
}
|
||||||
|
|
||||||
function clampInt($val, int $min, int $max, int $fallback): int
|
function clampInt($val, int $min, int $max, int $fallback): int
|
||||||
{
|
{
|
||||||
if ($val === null || $val === '') return $fallback;
|
if ($val === null || $val === '') return $fallback;
|
||||||
@@ -94,6 +114,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
$is_active = isset($_POST['is_active']) ? 1 : 0;
|
||||||
$notes = trim($_POST['notes'] ?? '');
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
|
||||||
|
// Target (uomo/donna/bambino/unisex) — array di checkbox
|
||||||
|
$allowedTargets = ['uomo', 'donna', 'bambino', 'unisex'];
|
||||||
|
$targetArr = array_values(array_intersect($allowedTargets, (array)($_POST['target'] ?? [])));
|
||||||
|
if (empty($targetArr)) $targetArr = ['unisex'];
|
||||||
|
$target = implode(',', $targetArr);
|
||||||
|
|
||||||
// Validazioni base
|
// Validazioni base
|
||||||
if ($first_name === '' || $last_name === '') {
|
if ($first_name === '' || $last_name === '') {
|
||||||
setFlash('danger', "Nome e Cognome sono obbligatori.");
|
setFlash('danger', "Nome e Cognome sono obbligatori.");
|
||||||
@@ -109,9 +135,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
if ($action === 'add') {
|
if ($action === 'add') {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO staff
|
INSERT INTO staff
|
||||||
(shop_id, first_name, last_name, nickname, role, phone, email, color_hex,
|
(shop_id, first_name, last_name, nickname, role, target, phone, email, color_hex,
|
||||||
max_appointments_per_day, can_book_online, is_active, notes)
|
max_appointments_per_day, can_book_online, is_active, notes)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$ok = $stmt->execute([
|
$ok = $stmt->execute([
|
||||||
$shop_id,
|
$shop_id,
|
||||||
@@ -119,6 +145,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
$last_name,
|
$last_name,
|
||||||
$nickname ?: null,
|
$nickname ?: null,
|
||||||
$role ?: null,
|
$role ?: null,
|
||||||
|
$target,
|
||||||
$phone ?: null,
|
$phone ?: null,
|
||||||
$email ?: null,
|
$email ?: null,
|
||||||
$color_hex ?: null,
|
$color_hex ?: null,
|
||||||
@@ -140,7 +167,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
|
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE staff
|
UPDATE staff
|
||||||
SET first_name = ?, last_name = ?, nickname = ?, role = ?, phone = ?, email = ?,
|
SET first_name = ?, last_name = ?, nickname = ?, role = ?, target = ?, phone = ?, email = ?,
|
||||||
color_hex = ?, max_appointments_per_day = ?, can_book_online = ?, is_active = ?,
|
color_hex = ?, max_appointments_per_day = ?, can_book_online = ?, is_active = ?,
|
||||||
notes = ?, updated_at = NOW()
|
notes = ?, updated_at = NOW()
|
||||||
WHERE id = ? AND shop_id = ?
|
WHERE id = ? AND shop_id = ?
|
||||||
@@ -150,6 +177,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
$last_name,
|
$last_name,
|
||||||
$nickname ?: null,
|
$nickname ?: null,
|
||||||
$role ?: null,
|
$role ?: null,
|
||||||
|
$target,
|
||||||
$phone ?: null,
|
$phone ?: null,
|
||||||
$email ?: null,
|
$email ?: null,
|
||||||
$color_hex ?: null,
|
$color_hex ?: null,
|
||||||
@@ -197,7 +225,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|||||||
// Fetch staff
|
// Fetch staff
|
||||||
// ===========================
|
// ===========================
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT id, first_name, last_name, nickname, role, phone, email, color_hex,
|
SELECT id, first_name, last_name, nickname, role, target, phone, email, color_hex,
|
||||||
max_appointments_per_day, can_book_online, is_active, notes
|
max_appointments_per_day, can_book_online, is_active, notes
|
||||||
FROM staff
|
FROM staff
|
||||||
WHERE shop_id = ?
|
WHERE shop_id = ?
|
||||||
@@ -261,6 +289,7 @@ $flash = getFlash();
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Nome</th>
|
<th>Nome</th>
|
||||||
<th>Ruolo</th>
|
<th>Ruolo</th>
|
||||||
|
<th>Tratta</th>
|
||||||
<th>Telefono / Email</th>
|
<th>Telefono / Email</th>
|
||||||
<th>Max app/giorno</th>
|
<th>Max app/giorno</th>
|
||||||
<th>Online</th>
|
<th>Online</th>
|
||||||
@@ -281,6 +310,7 @@ $flash = getFlash();
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><?= htmlspecialchars($s['role'] ?: '-') ?></td>
|
<td><?= htmlspecialchars($s['role'] ?: '-') ?></td>
|
||||||
|
<td><?= targetBadges($s['target'] ?? '') ?></td>
|
||||||
<td>
|
<td>
|
||||||
<?= htmlspecialchars($s['phone'] ?: '-') ?><br>
|
<?= htmlspecialchars($s['phone'] ?: '-') ?><br>
|
||||||
<small><?= htmlspecialchars($s['email'] ?: '-') ?></small>
|
<small><?= htmlspecialchars($s['email'] ?: '-') ?></small>
|
||||||
@@ -318,6 +348,7 @@ $flash = getFlash();
|
|||||||
"last_name" => $s["last_name"],
|
"last_name" => $s["last_name"],
|
||||||
"nickname" => $s["nickname"] ?? "",
|
"nickname" => $s["nickname"] ?? "",
|
||||||
"role" => $s["role"] ?? "",
|
"role" => $s["role"] ?? "",
|
||||||
|
"target" => $s["target"] ?? "",
|
||||||
"phone" => $s["phone"] ?? "",
|
"phone" => $s["phone"] ?? "",
|
||||||
"email" => $s["email"] ?? "",
|
"email" => $s["email"] ?? "",
|
||||||
"color_hex" => $s["color_hex"] ?? "",
|
"color_hex" => $s["color_hex"] ?? "",
|
||||||
@@ -386,6 +417,19 @@ $flash = getFlash();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold d-block">Tratta</label>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<?php foreach (['uomo' => 'Uomo', 'donna' => 'Donna', 'bambino' => 'Bambini', 'unisex' => 'Unisex'] as $val => $lbl): ?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="target[]" value="<?= $val ?>" id="add_target_<?= $val ?>" <?= $val === 'unisex' ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label" for="add_target_<?= $val ?>"><?= $lbl ?></label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label fw-bold">Telefono</label>
|
<label class="form-label fw-bold">Telefono</label>
|
||||||
@@ -467,6 +511,19 @@ $flash = getFlash();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold d-block">Tratta</label>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<?php foreach (['uomo' => 'Uomo', 'donna' => 'Donna', 'bambino' => 'Bambini', 'unisex' => 'Unisex'] as $val => $lbl): ?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input edit-target" type="checkbox" name="target[]" value="<?= $val ?>" id="edit_target_<?= $val ?>">
|
||||||
|
<label class="form-check-label" for="edit_target_<?= $val ?>"><?= $lbl ?></label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label fw-bold">Telefono</label>
|
<label class="form-label fw-bold">Telefono</label>
|
||||||
@@ -533,6 +590,13 @@ $flash = getFlash();
|
|||||||
document.getElementById('edit_last_name').value = data.last_name || '';
|
document.getElementById('edit_last_name').value = data.last_name || '';
|
||||||
document.getElementById('edit_nickname').value = data.nickname || '';
|
document.getElementById('edit_nickname').value = data.nickname || '';
|
||||||
document.getElementById('edit_role').value = data.role || '';
|
document.getElementById('edit_role').value = data.role || '';
|
||||||
|
// Spunta i target del collaboratore
|
||||||
|
var targets = (data.target || '').split(',').map(function(t) {
|
||||||
|
return t.trim();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.edit-target').forEach(function(cb) {
|
||||||
|
cb.checked = targets.indexOf(cb.value) !== -1;
|
||||||
|
});
|
||||||
document.getElementById('edit_phone').value = data.phone || '';
|
document.getElementById('edit_phone').value = data.phone || '';
|
||||||
document.getElementById('edit_email').value = data.email || '';
|
document.getElementById('edit_email').value = data.email || '';
|
||||||
document.getElementById('edit_color_hex').value = data.color_hex || '';
|
document.getElementById('edit_color_hex').value = data.color_hex || '';
|
||||||
|
|||||||
Reference in New Issue
Block a user