OverviewPanoramica

Beach Gest is a full-stack beach resort management application built with CodeIgniter 4 and a JavaScript SPA frontend.

Beach Gest è un'applicazione full-stack per la gestione di stabilimenti balneari, costruita con CodeIgniter 4 e un frontend SPA in JavaScript.

PHP 8.1+

Backend languageLinguaggio backend

CodeIgniter 4

Framework MVC

MySQL

Database

Bootstrap 5

UI Framework

Key FeaturesFunzionalità Principali

  • RESTful API - 125+ endpoints with standardized responses
  • SPA Frontend - Single Page Application served from /admin
  • JWT Authentication - Token-based auth with remember me support
  • Role-based Access - 4 levels: superadmin, admin, operatore, readonly
  • Module System - Granular permission control per user
  • Email Campaigns - Queue-based email system with SMTP
  • Interactive Layout - Drag-and-drop beach layout designer
  • API RESTful - 125+ endpoint con risposte standardizzate
  • Frontend SPA - Single Page Application servita da /admin
  • Autenticazione JWT - Autenticazione basata su token con supporto "ricordami"
  • Accesso basato su ruoli - 4 livelli: superadmin, admin, operatore, readonly
  • Sistema Moduli - Controllo granulare dei permessi per utente
  • Campagne Email - Sistema email con coda di invio e SMTP
  • Layout Interattivo - Designer drag-and-drop per la mappa spiaggia

InstallationInstallazione

RequirementsRequisiti

  • PHP 8.1 or highero superiore
  • MySQL 5.7+ oro MariaDB 10.3+
  • Composer
  • Apache withcon mod_rewrite oro Nginx
  • PHP ExtensionsEstensioni PHP: intl, mbstring, json, mysqlnd, curl

StepsProcedura

1. Clone the repositoryClonare il repository

git clone https://github.com/your-org/beachgest.git
cd beachgest

2. Install dependenciesInstallare le dipendenze

composer install

3. Environment setupConfigurazione ambiente

cp env .env

4. Configure .envConfigurare .env

# Application
CI_ENVIRONMENT = development
app.baseURL = 'http://localhost/beachapp/public/'

# Database
database.default.hostname = localhost
database.default.database = stabilimento_balneare
database.default.username = root
database.default.password =
database.default.DBDriver = MySQLi

# API Key
API_KEY = your-secure-api-key-here

# Email (optional)
email.SMTPHost = smtp.example.com
email.SMTPUser = user@example.com
email.SMTPPass = password
email.SMTPPort = 587

5. Run migrationsEseguire le migrazioni

php spark migrate

6. Seed database (optional)Popolare il database (opzionale)

php spark db:seed MainSeeder

7. Start development serverAvviare il server di sviluppo

php spark serve

Access the application at http://localhost:8080/admin

Accedere all'applicazione su http://localhost:8080/admin

ConfigurationConfigurazione

Key Configuration FilesFile di Configurazione Principali

File Purpose
.envEnvironment variables (database, API key, email)
app/Config/App.phpApplication settings (baseURL, timezone)
app/Config/Database.phpDatabase connection settings
app/Config/Routes.phpRoute definitions
app/Config/Filters.phpHTTP filters (auth, CORS)
app/Config/Email.phpSMTP and email campaign settings
File Scopo
.envVariabili d'ambiente (database, API key, email)
app/Config/App.phpImpostazioni applicazione (baseURL, timezone)
app/Config/Database.phpConfigurazione connessione database
app/Config/Routes.phpDefinizione delle rotte
app/Config/Filters.phpFiltri HTTP (autenticazione, CORS)
app/Config/Email.phpImpostazioni SMTP e campagne email

Email ConfigurationConfigurazione Email

// app/Config/Email.php
public string $SMTPHost = '';
public string $SMTPUser = '';
public string $SMTPPass = '';
public int $SMTPPort = 587;
public string $SMTPCrypto = 'tls';

// Campaign settings
public int $maxEmailsPerBatch = 50;
public int $delayBetweenEmails = 100; // milliseconds
public string $attachmentPath = WRITEPATH . 'uploads/email_attachments/';
public int $maxAttachmentSize = 5242880; // 5MB
public array $allowedAttachmentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png'];

Project StructureStruttura del Progetto

beachapp/
├── app/
│   ├── Commands/           # CLI commands (Spark)
│   │   └── ProcessEmailQueue.php
│   ├── Config/             # Configuration files
│   │   ├── App.php
│   │   ├── Database.php
│   │   ├── Email.php
│   │   ├── Filters.php
│   │   └── Routes.php
│   ├── Controllers/
│   │   └── Api/            # RESTful API controllers
│   │       ├── BaseApiController.php
│   │       ├── AuthApiController.php
│   │       ├── ClientiApiController.php
│   │       ├── PrenotazioniApiController.php
│   │       ├── ServiziApiController.php
│   │       ├── MovimentiApiController.php
│   │       ├── LayoutApiController.php
│   │       ├── PeriodiApiController.php
│   │       ├── EmailApiController.php
│   │       ├── EmailTemplateApiController.php
│   │       ├── PlannerApiController.php
│   │       ├── UsersApiController.php
│   │       ├── ModuliApiController.php
│   │       └── DatatablesApiController.php
│   ├── Filters/            # HTTP filters
│   │   ├── Auth.php
│   │   └── Admin.php
│   ├── Models/             # Database models
│   │   ├── ClienteModel.php
│   │   ├── PrenotazioneModel.php
│   │   ├── ServizioModel.php
│   │   ├── MovimentoModel.php
│   │   ├── LayoutModel.php
│   │   ├── UserModel.php
│   │   ├── EmailTemplateSettingsModel.php
│   │   ├── EmailListModel.php
│   │   ├── EmailListMemberModel.php
│   │   └── ...
│   ├── Database/
│   │   └── Migrations/
│   │       └── 2026-01-27-000001_CreateEmailTemplateSettings.php
│   ├── Services/           # Business logic
│   │   └── EmailService.php
│   └── Views/
│       └── admin/
│           └── index.php   # SPA entry point
├── public/
│   ├── index.php           # Front controller
│   ├── .htaccess
│   └── assets/
│       ├── css/
│       └── js/
│           └── app.js      # SPA JavaScript
├── tests/                  # PHPUnit tests
├── writable/               # Logs, cache, uploads
├── composer.json
├── .env
└── CLAUDE.md               # Project documentation

Controller

BaseApiController

All API controllers extend BaseApiController which provides:

Tutti i controller API estendono BaseApiController che fornisce:

// app/Controllers/Api/BaseApiController.php

abstract class BaseApiController extends Controller
{
    protected $response;
    protected $request;

    // Authentication methods
    protected function validateApiKey(): bool
    protected function validateToken(): bool
    protected function getCurrentUser(): ?array

    // Authorization methods
    protected function isAdmin(): bool
    protected function isSuperAdmin(): bool
    protected function isOperatore(): bool
    protected function isReadonly(): bool
    protected function canWrite(): bool

    // Response helpers
    protected function success($data = [], string $message = '', int $status = 200)
    protected function error(string $message, int $status = 400, array $errors = [])
    protected function forbidden(string $message = 'Accesso negato')

    // Utility methods
    protected function applyMultiWordSearch($builder, $searchValue, $searchFields): void
}

Creating a New API ControllerCreare un Nuovo Controller API

namespace App\Controllers\Api;

class MyApiController extends BaseApiController
{
    protected $myModel;

    public function __construct()
    {
        $this->myModel = new \App\Models\MyModel();
    }

    public function index()
    {
        // Validate token
        if (!$this->validateToken()) {
            return $this->error('Token non valido', 401);
        }

        $data = $this->myModel->findAll();

        return $this->success($data, 'Dati recuperati con successo');
    }

    public function create()
    {
        if (!$this->validateToken()) {
            return $this->error('Token non valido', 401);
        }

        // Check write permission
        if (!$this->canWrite()) {
            return $this->forbidden('Non hai i permessi per questa operazione');
        }

        $data = $this->request->getJSON(true);

        // Validate
        if (!$this->myModel->validate($data)) {
            return $this->error('Errore di validazione', 422, $this->myModel->errors());
        }

        $id = $this->myModel->insert($data);

        return $this->success(['id' => $id], 'Creato con successo', 201);
    }
}

Controller ListElenco Controller

Controller Endpoints Description
AuthApiController7Login, logout, profile, password change
ClientiApiController9Client CRUD, search, export
PrenotazioniApiController12Reservation CRUD, payments, cancellation
ServiziApiController8Service catalog management
MovimentiApiController9Financial transactions
LayoutApiController28Beach layout, availability
PeriodiApiController9Preset periods, price lists
EmailApiController21Email campaigns, lists, recipients
EmailTemplateApiController3Email template settings (SuperAdmin)
PlannerApiController4Planner timeline, drag-and-drop updates
UsersApiController5User management (SuperAdmin)
ModuliApiController6Module permissions (SuperAdmin)
DatatablesApiController10Server-side DataTables
Controller Endpoint Descrizione
AuthApiController7Login, logout, profilo, cambio password
ClientiApiController9CRUD clienti, ricerca, esportazione
PrenotazioniApiController12CRUD prenotazioni, pagamenti, annullamento
ServiziApiController8Gestione catalogo servizi
MovimentiApiController9Transazioni finanziarie
LayoutApiController28Layout spiaggia, disponibilità
PeriodiApiController9Periodi preimpostati, listini prezzi
EmailApiController21Campagne email, liste, destinatari
EmailTemplateApiController3Impostazioni template email (SuperAdmin)
PlannerApiController4Planner timeline, aggiornamento drag-and-drop
UsersApiController5Gestione utenti (SuperAdmin)
ModuliApiController6Permessi moduli (SuperAdmin)
DatatablesApiController10DataTables lato server

ModelsModelli

Model PatternPattern dei Modelli

All models extend CodeIgniter's Model class with:

Tutti i modelli estendono la classe Model di CodeIgniter con:

namespace App\Models;

use CodeIgniter\Model;

class ClienteModel extends Model
{
    protected $table = 'clienti';
    protected $primaryKey = 'id';
    protected $returnType = 'array';
    protected $useSoftDeletes = false;
    protected $useTimestamps = true;
    protected $createdField = 'created_at';
    protected $updatedField = 'updated_at';

    protected $allowedFields = [
        'tipo_cliente', 'stato_cliente', 'nome', 'cognome',
        'email_personale', 'telefono_personale', // ...
    ];

    protected $validationRules = [
        'tipo_cliente' => 'required|in_list[FISICO,AZIENDA]',
        'stato_cliente' => 'permit_empty|in_list[ATTIVO,SOSPESO]',
        'email_personale' => 'permit_empty|valid_email',
        // Dynamic rules based on tipo_cliente
    ];

    protected $validationMessages = [
        'tipo_cliente' => [
            'required' => 'Il tipo cliente è obbligatorio',
            'in_list' => 'Tipo cliente non valido'
        ]
    ];

    // Callbacks
    protected $beforeInsert = ['cleanData'];
    protected $beforeUpdate = ['cleanData'];

    // Custom validation for conditional rules
    public function validate($data): bool
    {
        // Custom logic based on tipo_cliente
        if ($data['tipo_cliente'] === 'FISICO') {
            $this->validationRules['nome'] = 'required|min_length[2]';
            $this->validationRules['cognome'] = 'required|min_length[2]';
        } else {
            $this->validationRules['ragione_sociale'] = 'required|min_length[2]';
        }

        return parent::validate($data);
    }
}

Key ModelsModelli Principali

ClienteModel

  • Supports two types: FISICO (individual) and AZIENDA (company)
  • Conditional validation based on type
  • Methods: getClientiAttivi(), searchClienti($term), getClientiWithMovimenti()
  • Supporta due tipologie: FISICO (persona fisica) e AZIENDA (società)
  • Validazione condizionale in base al tipo
  • Metodi: getClientiAttivi(), searchClienti($term), getClientiWithMovimenti()

PrenotazioneModel

  • Auto-generates codice_prenotazione (format: PREN{YYYY}{00001})
  • States: ATTIVA, COMPLETATA, ANNULLATA
  • Methods: getPrenotazioneConDettagli($id), aggiornaTotalePagato($id)
  • Genera automaticamente il codice_prenotazione (formato: PREN{YYYY}{00001})
  • Stati: ATTIVA, COMPLETATA, ANNULLATA
  • Metodi: getPrenotazioneConDettagli($id), aggiornaTotalePagato($id)

LayoutModel

  • Manages beach layout with positions and instances
  • Complex availability queries with subqueries
  • Methods: getLayoutCompleto(), getDisponibilitaPerData($data), checkDisponibilitaIstanza()
  • Gestisce il layout della spiaggia con posizioni e istanze
  • Query complesse sulla disponibilità con subquery
  • Metodi: getLayoutCompleto(), getDisponibilitaPerData($data), checkDisponibilitaIstanza()

EmailListModel

  • Manages email distribution lists
  • Table: email_lists (id, nome, descrizione, timestamps)
  • Methods: getListWithCount(), getListsForSelect()
  • Gestisce le liste di distribuzione email
  • Tabella: email_lists (id, nome, descrizione, timestamps)
  • Metodi: getListWithCount(), getListsForSelect()

EmailListMemberModel

  • Manages members of email lists (junction table)
  • Table: email_list_members (id, list_id FK, cliente_id FK)
  • Unique constraint on (list_id, cliente_id)
  • Gestisce i membri delle liste email (tabella di collegamento)
  • Tabella: email_list_members (id, list_id FK, cliente_id FK)
  • Vincolo univoco su (list_id, cliente_id)

ServicesServizi

EmailService

Handles email campaign creation and processing.

Gestisce la creazione e l'elaborazione delle campagne email.

namespace App\Services;

class EmailService
{
    protected $emailConfig;
    protected $campaignModel;
    protected $recipientModel;

    // Get all client emails
    public function getAllClientEmails(): array

    // Search client emails
    public function searchClientEmails(string $search): array

    // Create new campaign with recipients and attachments
    public function createCampaign(array $data, array $recipients, ?array $attachments = null): int

    // Handle file attachments upload
    protected function handleAttachments(array $files): array

    // Process email queue (called by cron)
    public function processCoda(): array

    // Process single campaign
    protected function processCampaign(int $campaignId): void

    // Send single email using PHPMailer
    protected function sendSingleEmail(array $campaign, array $recipient): bool

    // Test SMTP connection
    public function testSmtpConnection(): bool

    // Delete campaign attachments
    public function deleteAttachments(array $allegati): void

    // Template Email methods
    public function wrapWithTemplate(string $contentHtml): string
    public function buildEmailHtml(string $contentHtml, array $settings): string
    public function getTemplateSettings(): ?array
}

Creating a New ServiceCreare un Nuovo Servizio

namespace App\Services;

class MyService
{
    protected $model;

    public function __construct()
    {
        $this->model = new \App\Models\MyModel();
    }

    public function processData(array $data): array
    {
        // Business logic here
        return $result;
    }
}

API AuthenticationAutenticazione API

Two-Level AuthenticationAutenticazione a Due Livelli

1. API Key

Required for all API calls. Sent via header:

Richiesta per tutte le chiamate API. Inviata tramite header:

X-API-KEY: your-api-key

2. Bearer Token

Required for protected endpoints. Obtained via login:

Richiesto per gli endpoint protetti. Ottenuto tramite login:

Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOi...

Login FlowFlusso di Login

// 1. Login request
POST /api/v1/auth/login
{
    "username": "admin",
    "password": "admin123",
    "remember_me": true
}

// 2. Response
{
    "status": true,
    "message": "Login effettuato con successo",
    "data": {
        "token": "abc123...",
        "expires_at": "2024-01-02T12:00:00",
        "remember_token": "xyz789...",  // Only if remember_me=true
        "user": {
            "id": 1,
            "username": "admin",
            "email": "admin@example.com",
            "role": "superadmin"
        }
    }
}

// 3. Use token in subsequent requests
GET /api/v1/clienti
Headers:
    X-API-KEY: your-api-key
    Authorization: Bearer abc123...

Token ExpirationScadenza del Token

  • Token regularnormale: 24 hoursore
  • Token remember: 30 daysgiorni

Refresh with Remember TokenRinnovo con Remember Token

POST /api/v1/auth/refresh-remember
{
    "remember_token": "xyz789..."
}

// Returns new token

Changing the API Key in ProductionCambiare API Key in Produzione

The API key must be identical in two files. To generate it, use the PHP terminal:

L'API key deve essere identica in due file. Per generarla usa il terminale PHP:

php -r "echo bin2hex(random_bytes(32));"
# Output esempio: a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1

Then update the same key in both files:

Poi aggiorna la stessa chiave in entrambi i file:

1. File .env (server)

api.key = 'la-tua-nuova-chiave-generata'

2. File public/assets/js/app.js (frontend)

const API_KEY = 'la-tua-nuova-chiave-generata';
Important: The two keys must match exactly. If they don't match, all API calls will return 401 - API Key non valida. Importante: Le due chiavi devono corrispondere esattamente. Se non coincidono, tutte le chiamate API restituiranno 401 - API Key non valida.

Response FormatFormato Risposta

Success ResponseRisposta di Successo

{
    "status": true,
    "message": "Operazione completata con successo",
    "data": {
        // Response data
    }
}

Error ResponseRisposta di Errore

{
    "status": false,
    "message": "Errore di validazione",
    "errors": {
        "email": ["Il campo email è obbligatorio"],
        "nome": ["Il nome deve essere di almeno 2 caratteri"]
    }
}

Paginated ResponseRisposta Paginata

{
    "status": true,
    "message": "Dati recuperati con successo",
    "data": {
        "items": [...],
        "pagination": {
            "current_page": 1,
            "per_page": 10,
            "total": 150,
            "total_pages": 15
        }
    }
}

DataTables ResponseRisposta DataTables

{
    "draw": 1,
    "recordsTotal": 150,
    "recordsFiltered": 50,
    "data": [...]
}

ValidationValidazione

Model Validation RulesRegole di Validazione del Modello

protected $validationRules = [
    'email' => 'required|valid_email|is_unique[clienti.email_personale,id,{id}]',
    'codice_fiscale' => 'permit_empty|alpha_numeric|exact_length[16]',
    'partita_iva' => 'permit_empty|numeric|exact_length[11]',
    'cap' => 'permit_empty|numeric|exact_length[5]',
    'provincia' => 'permit_empty|alpha|exact_length[2]',
    'importo' => 'required|decimal|greater_than[0]',
    'data_inizio' => 'required|valid_date',
];

Custom ValidationValidazione Personalizzata

// In controller
$rules = [
    'email' => [
        'rules' => 'required|valid_email',
        'errors' => [
            'required' => 'L\'email è obbligatoria',
            'valid_email' => 'Formato email non valido'
        ]
    ]
];

if (!$this->validate($rules)) {
    return $this->error('Errore di validazione', 422, $this->validator->getErrors());
}

Error HandlingGestione Errori

HTTP Status CodesCodici di Stato HTTP

Code Meaning Usage
200OKSuccessful GET, PUT, DELETE
201CreatedSuccessful POST (resource created)
400Bad RequestInvalid request data
401UnauthorizedInvalid or missing token
403ForbiddenInsufficient permissions
404Not FoundResource not found
422Unprocessable EntityValidation errors
500Server ErrorInternal error
Codice Significato Utilizzo
200OKGET, PUT, DELETE riusciti
201CreatedPOST riuscito (risorsa creata)
400Bad RequestDati della richiesta non validi
401UnauthorizedToken non valido o mancante
403ForbiddenPermessi insufficienti
404Not FoundRisorsa non trovata
422Unprocessable EntityErrori di validazione
500Server ErrorErrore interno

Exception HandlingGestione delle Eccezioni

try {
    $db = \Config\Database::connect();
    $db->transStart();

    // Operations...

    $db->transComplete();

    if ($db->transStatus() === false) {
        throw new \Exception('Errore durante la transazione');
    }

    return $this->success($data);

} catch (\Exception $e) {
    log_message('error', 'Error: ' . $e->getMessage());
    return $this->error('Si è verificato un errore', 500);
}

Template Email API

API for managing the configurable email template with customizable header/footer.

API per la gestione del template email configurabile con header/footer personalizzabili.

Controller

App\Controllers\Api\EmailTemplateApiControllerextendsestende BaseApiController

Access restricted to SuperAdmin.Accesso riservato ai SuperAdmin.

Model

App\Models\EmailTemplateSettingsModel

  • getSettings()Returns row id=1 (creates it with defaults if it doesn't exist)Restituisce la riga id=1 (la crea con valori default se non esiste)
  • saveSettings(array $data)Updates row id=1Aggiorna la riga id=1

Endpoint

MethodMetodoEndpointDescriptionDescrizione
GET/api/v1/email/template-settingsGet template settingsOttieni impostazioni template
POST/api/v1/email/template-settingsSave settings (multipart/form-data)Salva impostazioni (multipart/form-data)
GET/api/v1/email/template-previewRendered HTML previewAnteprima HTML renderizzata

Logo UploadUpload Logo

  • Supported formats: JPG, PNG, GIFFormati supportati: JPG, PNG, GIF
  • Max size: 2 MBDimensione massima: 2 MB
  • Upload directory:Directory upload: public/assets/uploads/email/
  • To remove the logo: send remove_logo: truePer rimuovere il logo: inviare remove_logo: true

EmailService IntegrationIntegrazione EmailService

The template is automatically applied in EmailService::sendSingleEmail():

Il template viene applicato automaticamente in EmailService::sendSingleEmail():

// In sendSingleEmail()
$mail->Body = $this->wrapWithTemplate($campaign['corpo_html']);

// wrapWithTemplate() controlla se il template è attivo:
// - Se template_active = 0: restituisce il corpo HTML originale
// - Se template_active = 1: chiama buildEmailHtml() per wrappare con header/footer

Methods Added to EmailServiceMetodi Aggiunti a EmailService

  • wrapWithTemplate(string $contentHtml): stringEntry point, checks if template is activeEntry point, controlla se template è attivo
  • getTemplateSettings(): ?arrayLoads settings with in-memory cache (avoids repeated queries during batch)Carica settings con cache in-memory (evita query ripetute durante batch)
  • buildEmailHtml(string $contentHtml, array $settings): stringBuilds complete TABLE-based HTML with:Costruisce HTML completo TABLE-based con:
    • Meta tag color-scheme: light dark for native dark modeMeta tag color-scheme: light dark per dark mode nativo
    • Inline CSS for Gmail compatibilityCSS inline per compatibilità Gmail
    • MSO conditionals <!--[if mso]> for OutlookCondizionali MSO <!--[if mso]> per Outlook
    • Responsive CSS for screens < 620pxResponsive CSS per schermi < 620px
    • Header with logo/company nameHeader con logo/nome azienda
    • Content areaArea contenuto
    • Footer with company info, social links, and unsubscribe textFooter con info aziendali, social e testo disiscrizione

Database TableTabella Database

email_template_settingsSingle configuration row (id=1)Singola riga di configurazione (id=1)

Migration: 2026-01-27-000001_CreateEmailTemplateSettings

SPA ArchitectureArchitettura SPA

OverviewPanoramica

The frontend is a vanilla JavaScript SPA served from /admin.

Il frontend è una SPA in JavaScript vanilla servita da /admin.

File StructureStruttura dei File

app/Views/admin/index.php    # Main SPA template
public/assets/js/app.js      # Application logic
public/assets/css/           # Stylesheets

Routing

// app/Config/Routes.php
$routes->get('/', 'Home::index');
$routes->get('/admin', 'Admin::index');
$routes->get('/admin/(:any)', 'Admin::index');  // Catch-all for SPA

JavaScript StructureStruttura JavaScript

// public/assets/js/app.js

// Global state
let authToken = null;
let currentUser = null;
let currentPage = 'dashboard';

// Initialize
document.addEventListener('DOMContentLoaded', function() {
    checkAuth();
    setupNavigation();
});

// Authentication
async function login(username, password, rememberMe) { ... }
async function logout() { ... }
function checkAuth() { ... }

// Page loaders
function loadDashboard() { ... }
function loadClienti() { ... }
function loadPrenotazioni() { ... }
function loadMovimenti() { ... }
function loadServizi() { ... }
function loadDisponibilita() { ... }
function loadPeriodi() { ... }
function loadEmail() { ... }
function loadStatistiche() { ... }
function loadUtenti() { ... }
function loadModuli() { ... }
function loadProfilo() { ... }
function loadPlanner() { ... }
function loadEmailTemplate() { ... }

// Planner (DayPilot Scheduler)
let scheduler = null;        // DayPilot.Scheduler instance
let plannerDate = null;       // Current DayPilot.Date
let plannerScale = 'month';   // day | week | month
function loadSchedulerData() { ... }
function updateSchedulerRange() { ... }
function bindPlannerEvents() { ... }
function applyCustomRangeMobile() { ... }
function resetPlannerFilters() { ... }

// API helpers
async function apiRequest(endpoint, options = {}) {
    const defaultOptions = {
        headers: {
            'Content-Type': 'application/json',
            'X-API-KEY': API_KEY,
            'Authorization': `Bearer ${authToken}`
        }
    };
    // ...
}

// UI helpers
function showToast(message, type) { ... }
function showConfirm(options) { ... }
function initDataTable(selector, options) { ... }

Frontend ComponentsComponenti Frontend

Libraries UsedLibrerie Utilizzate

  • Bootstrap 5.3 - UI framework
  • DataTables 1.13 - Server-side tablesTabelle lato server
  • Select2 4.1 - Enhanced selectsSelect avanzate
  • Quill 1.3 - Rich text editor (email)Editor di testo (email)
  • intl-tel-input - Phone inputInput telefonico
  • Chart.js - Charts (statistics)Grafici (statistiche)
  • DayPilot Scheduler - Interactive timeline for plannerTimeline interattiva per planner

DataTables IntegrationIntegrazione DataTables

function initClientiTable() {
    $('#clientiTable').DataTable({
        processing: true,
        serverSide: true,
        ajax: {
            url: `${API_BASE_URL}/datatables/clienti`,
            type: 'GET',
            headers: {
                'X-API-KEY': API_KEY,
                'Authorization': `Bearer ${authToken}`
            },
            data: function(d) {
                d.tipo_cliente = $('#filterTipo').val();
                d.stato_cliente = $('#filterStato').val();
            }
        },
        columns: [
            { data: 'id' },
            { data: 'tipo_cliente' },
            { data: 'nome_completo' },
            { data: 'email' },
            { data: 'telefono' },
            { data: 'stato_cliente' },
            { data: 'saldo', render: formatCurrency },
            { data: null, render: renderActions }
        ],
        language: {
            url: '//cdn.datatables.net/plug-ins/1.13.5/i18n/it-IT.json'
        }
    });
}

Select2 IntegrationIntegrazione Select2

$('#clienteSelect').select2({
    theme: 'bootstrap-5',
    placeholder: 'Cerca cliente...',
    ajax: {
        url: `${API_BASE_URL}/clienti/select2`,
        headers: { 'X-API-KEY': API_KEY, 'Authorization': `Bearer ${authToken}` },
        dataType: 'json',
        delay: 250,
        data: params => ({ term: params.term }),
        processResults: data => ({
            results: data.data.map(c => ({
                id: c.id,
                text: c.nome_completo
            }))
        })
    }
});

Database SchemaSchema del Database

Main TablesTabelle Principali

-- Users
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(20) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    role ENUM('superadmin', 'admin', 'operatore', 'readonly') DEFAULT 'operatore',
    is_active TINYINT(1) DEFAULT 1,
    api_token VARCHAR(255),
    token_expires DATETIME,
    remember_token VARCHAR(255),
    remember_expires DATETIME,
    created_at DATETIME,
    updated_at DATETIME
);

-- Clienti
CREATE TABLE clienti (
    id INT AUTO_INCREMENT PRIMARY KEY,
    tipo_cliente ENUM('FISICO', 'AZIENDA') NOT NULL,
    stato_cliente ENUM('ATTIVO', 'SOSPESO') DEFAULT 'ATTIVO',
    nome VARCHAR(100),
    cognome VARCHAR(100),
    email_personale VARCHAR(255),
    telefono_personale VARCHAR(50),
    cellulare_personale VARCHAR(50),
    codice_fiscale VARCHAR(16),
    data_nascita DATE,
    indirizzo_personale_via VARCHAR(255),
    indirizzo_personale_civico VARCHAR(20),
    indirizzo_personale_cap VARCHAR(5),
    indirizzo_personale_citta VARCHAR(100),
    indirizzo_personale_provincia VARCHAR(2),
    note_personali TEXT,
    ragione_sociale VARCHAR(255),
    partita_iva VARCHAR(11),
    codice_fiscale_azienda VARCHAR(16),
    sdi VARCHAR(7),
    pec VARCHAR(255),
    email_aziendale VARCHAR(255),
    telefono_aziendale VARCHAR(50),
    cellulare_aziendale VARCHAR(50),
    indirizzo_via VARCHAR(255),
    indirizzo_civico VARCHAR(20),
    indirizzo_cap VARCHAR(5),
    indirizzo_citta VARCHAR(100),
    indirizzo_provincia VARCHAR(2),
    referente_nome VARCHAR(100),
    referente_cognome VARCHAR(100),
    referente_email VARCHAR(255),
    referente_telefono VARCHAR(50),
    referente_cellulare VARCHAR(50),
    referente_ruolo VARCHAR(100),
    created_at DATETIME,
    updated_at DATETIME
);

-- Servizi
CREATE TABLE servizi (
    id INT AUTO_INCREMENT PRIMARY KEY,
    codice VARCHAR(50) UNIQUE NOT NULL,
    nome VARCHAR(255) NOT NULL,
    descrizione TEXT,
    categoria ENUM('SPIAGGIA', 'BAR', 'SERVIZI', 'ALTRO') NOT NULL,
    tipo_prezzo ENUM('GIORNALIERO', 'SETTIMANALE', 'MENSILE', 'STAGIONALE', 'FISSO') NOT NULL,
    prezzo DECIMAL(10,2) NOT NULL,
    iva INT DEFAULT 22,
    quantita_disponibile INT,
    stato ENUM('ATTIVO', 'SOSPESO') DEFAULT 'ATTIVO',
    created_at DATETIME,
    updated_at DATETIME
);

-- Prenotazioni
CREATE TABLE prenotazioni (
    id INT AUTO_INCREMENT PRIMARY KEY,
    codice_prenotazione VARCHAR(20) UNIQUE NOT NULL,
    cliente_id INT NOT NULL,
    data_inizio DATE NOT NULL,
    data_fine DATE,
    totale_servizi DECIMAL(10,2) DEFAULT 0,
    sconto_percentuale DECIMAL(5,2) DEFAULT 0,
    sconto_importo DECIMAL(10,2) DEFAULT 0,
    totale_finale DECIMAL(10,2) DEFAULT 0,
    totale_pagato DECIMAL(10,2) DEFAULT 0,
    stato ENUM('ATTIVA', 'COMPLETATA', 'ANNULLATA') DEFAULT 'ATTIVA',
    note TEXT,
    operatore_id INT,
    created_at DATETIME,
    updated_at DATETIME,
    FOREIGN KEY (cliente_id) REFERENCES clienti(id),
    FOREIGN KEY (operatore_id) REFERENCES users(id)
);

-- Prenotazione Servizi (junction table)
CREATE TABLE prenotazione_servizi (
    id INT AUTO_INCREMENT PRIMARY KEY,
    prenotazione_id INT NOT NULL,
    servizio_id INT NOT NULL,
    layout_servizio_id INT,
    quantita INT DEFAULT 1,
    prezzo_unitario DECIMAL(10,2),
    prezzo_totale DECIMAL(10,2),
    created_at DATETIME,
    FOREIGN KEY (prenotazione_id) REFERENCES prenotazioni(id) ON DELETE CASCADE,
    FOREIGN KEY (servizio_id) REFERENCES servizi(id),
    FOREIGN KEY (layout_servizio_id) REFERENCES layout_servizi(id)
);

-- Movimenti
CREATE TABLE movimenti (
    id INT AUTO_INCREMENT PRIMARY KEY,
    cliente_id INT NOT NULL,
    prenotazione_id INT,
    tipo_movimento ENUM('ENTRATA', 'USCITA') NOT NULL,
    data_movimento DATE NOT NULL,
    importo DECIMAL(10,2) NOT NULL,
    metodo_pagamento ENUM('Contanti', 'Carta', 'Bonifico', 'Satispay', 'Altro') NOT NULL,
    causale VARCHAR(200) NOT NULL,
    note_movimento TEXT,
    operatore_id INT,
    created_at DATETIME,
    updated_at DATETIME,
    FOREIGN KEY (cliente_id) REFERENCES clienti(id),
    FOREIGN KEY (prenotazione_id) REFERENCES prenotazioni(id),
    FOREIGN KEY (operatore_id) REFERENCES users(id)
);

-- Layout Servizi
CREATE TABLE layout_servizi (
    id INT AUTO_INCREMENT PRIMARY KEY,
    servizio_id INT NOT NULL,
    codice_istanza VARCHAR(50),
    numero_istanza INT DEFAULT 1,
    posizione_x DECIMAL(10,2),
    posizione_y DECIMAL(10,2),
    larghezza INT DEFAULT 80,
    altezza INT DEFAULT 80,
    rotazione INT DEFAULT 0,
    z_index INT DEFAULT 1,
    visibile TINYINT(1) DEFAULT 1,
    configurazione JSON,
    instance_id VARCHAR(36),
    created_at DATETIME,
    updated_at DATETIME,
    FOREIGN KEY (servizio_id) REFERENCES servizi(id)
);

-- Email Lists
CREATE TABLE email_lists (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(255) NOT NULL,
    descrizione TEXT,
    created_at DATETIME,
    updated_at DATETIME
);

CREATE TABLE email_list_members (
    id INT AUTO_INCREMENT PRIMARY KEY,
    list_id INT NOT NULL,
    cliente_id INT NOT NULL,
    created_at DATETIME,
    FOREIGN KEY (list_id) REFERENCES email_lists(id) ON DELETE CASCADE,
    FOREIGN KEY (cliente_id) REFERENCES clienti(id) ON DELETE CASCADE,
    UNIQUE KEY (list_id, cliente_id)
);

CREATE TABLE email_campaign_lists (
    id INT AUTO_INCREMENT PRIMARY KEY,
    campaign_id INT UNSIGNED NOT NULL,
    list_id INT,
    nome_lista VARCHAR(255),
    created_at DATETIME,
    FOREIGN KEY (campaign_id) REFERENCES email_campaigns(id) ON DELETE CASCADE
);

Entity RelationshipRelazioni tra Entità

clienti (1) ←──── (M) prenotazioni
         └────── (M) movimenti

prenotazioni (1) ←──── (M) prenotazione_servizi
           ├────── (M) movimenti
           └────── (1) users (operatore)

servizi (1) ←──── (M) prenotazione_servizi
       └────── (M) layout_servizi

layout_servizi (1) ←──── (M) prenotazione_servizi

users (1) ←──── (M) prenotazioni
      └────── (M) movimenti
      └────── (M) user_modules

MigrationsMigrazioni

Running MigrationsEseguire le Migrazioni

# Run all pending migrations
php spark migrate

# Rollback last batch
php spark migrate:rollback

# Refresh (rollback all + migrate)
php spark migrate:refresh

# Status
php spark migrate:status

Creating MigrationsCreare le Migrazioni

php spark make:migration CreateNewTable
// app/Database/Migrations/2024-01-01-000001_CreateNewTable.php
namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateNewTable extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'id' => [
                'type' => 'INT',
                'constraint' => 11,
                'unsigned' => true,
                'auto_increment' => true,
            ],
            'name' => [
                'type' => 'VARCHAR',
                'constraint' => 255,
            ],
            'created_at' => [
                'type' => 'DATETIME',
                'null' => true,
            ],
            'updated_at' => [
                'type' => 'DATETIME',
                'null' => true,
            ],
        ]);

        $this->forge->addKey('id', true);
        $this->forge->createTable('new_table');
    }

    public function down()
    {
        $this->forge->dropTable('new_table');
    }
}

CLI CommandsComandi CLI

Built-in CommandsComandi Integrati

# Development server
php spark serve

# Routes list
php spark routes

# Cache clear
php spark cache:clear

# Database
php spark db:create dbname
php spark migrate
php spark db:seed SeederName

# Code generation
php spark make:controller MyController
php spark make:model MyModel
php spark make:migration CreateTable
php spark make:seeder MySeeder
php spark make:filter MyFilter
php spark make:command MyCommand

Custom CommandsComandi Personalizzati

Email Queue ProcessorProcessore Coda Email

# Process email queue
php spark email:process --verbose

# Test email configuration
php spark email:test recipient@example.com

Creating a Custom CommandCreare un Comando Personalizzato

// app/Commands/MyCommand.php
namespace App\Commands;

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;

class MyCommand extends BaseCommand
{
    protected $group = 'App';
    protected $name = 'my:command';
    protected $description = 'Description of my command';
    protected $usage = 'my:command [options]';
    protected $arguments = [];
    protected $options = [
        '--verbose' => 'Show detailed output',
    ];

    public function run(array $params)
    {
        $verbose = CLI::getOption('verbose');

        CLI::write('Starting...', 'green');

        // Command logic

        CLI::write('Done!', 'green');
    }
}

Cron SetupConfigurazione Cron

# Add to crontab (crontab -e)
# Process email queue every 5 minutes
*/5 * * * * cd /path/to/beachapp && php spark email:process >> /var/log/beachgest-email.log 2>&1

PHPUnit TestsTest PHPUnit

Running TestsEseguire i Test

# Run all tests
vendor/bin/phpunit

# Run specific test file
vendor/bin/phpunit tests/Controllers/ClientiTest.php

# Run with coverage
vendor/bin/phpunit --coverage-html coverage/

Test StructureStruttura dei Test

tests/
├── _support/
│   ├── Database/
│   │   └── Seeds/
│   └── TestCase.php
├── Controllers/
│   └── Api/
│       ├── ClientiTest.php
│       └── ...
├── Models/
│   ├── ClienteModelTest.php
│   └── ...
└── bootstrap.php

Example TestEsempio di Test

namespace Tests\Controllers\Api;

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;

class ClientiTest extends CIUnitTestCase
{
    use FeatureTestTrait;

    protected function setUp(): void
    {
        parent::setUp();
        // Setup
    }

    public function testGetClientiRequiresAuth()
    {
        $result = $this->get('api/v1/clienti');
        $result->assertStatus(401);
    }

    public function testGetClientiWithAuth()
    {
        $result = $this->withHeaders([
            'X-API-KEY' => getenv('API_KEY'),
            'Authorization' => 'Bearer ' . $this->getTestToken()
        ])->get('api/v1/clienti');

        $result->assertStatus(200);
        $result->assertJSONFragment(['status' => true]);
    }

    public function testCreateClienteValidation()
    {
        $result = $this->withHeaders([
            'X-API-KEY' => getenv('API_KEY'),
            'Authorization' => 'Bearer ' . $this->getTestToken()
        ])->post('api/v1/clienti', [
            'tipo_cliente' => 'FISICO'
            // Missing required fields
        ]);

        $result->assertStatus(422);
    }
}

Production DeploymentDeploy in Produzione

Pre-deployment ChecklistChecklist Pre-deploy

  • Set CI_ENVIRONMENT = production in .env
  • Configure production database credentials
  • Set secure API_KEY
  • Configure SMTP for email
  • Ensure writable/ directory is writable
  • Set up SSL certificate
  • Impostare CI_ENVIRONMENT = production nel .env
  • Configurare le credenziali del database di produzione
  • Impostare una API_KEY sicura
  • Configurare SMTP per le email
  • Verificare che la cartella writable/ sia scrivibile
  • Configurare il certificato SSL

Apache ConfigurationConfigurazione Apache

<VirtualHost *:443>
    ServerName beachgest.example.com
    DocumentRoot /var/www/beachgest/public

    SSLEngine on
    SSLCertificateFile /path/to/certificate.crt
    SSLCertificateKeyFile /path/to/private.key

    <Directory /var/www/beachgest/public>
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/beachgest_error.log
    CustomLog ${APACHE_LOG_DIR}/beachgest_access.log combined
</VirtualHost>

Nginx ConfigurationConfigurazione Nginx

server {
    listen 443 ssl http2;
    server_name beachgest.example.com;
    root /var/www/beachgest/public;
    index index.php;

    ssl_certificate /path/to/certificate.crt;
    ssl_certificate_key /path/to/private.key;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}

Production .env.env di Produzione

CI_ENVIRONMENT = production

app.baseURL = 'https://beachgest.example.com/'

database.default.hostname = localhost
database.default.database = stabilimento_balneare
database.default.username = db_user
database.default.password = secure_password
database.default.DBDriver = MySQLi

API_KEY = your-very-secure-api-key-here

email.SMTPHost = smtp.example.com
email.SMTPUser = noreply@example.com
email.SMTPPass = email_password
email.SMTPPort = 587

Cron Jobs

# Email queue (every 5 minutes)
*/5 * * * * cd /var/www/beachgest && php spark email:process

# Cache clear (daily at 3 AM)
0 3 * * * cd /var/www/beachgest && php spark cache:clear

# Log rotation (weekly)
0 0 * * 0 find /var/www/beachgest/writable/logs -name "*.log" -mtime +30 -delete

Shared HostingHosting Condiviso

When deploying to shared hosting, you may encounter the error #1227 - Access denied. Need SUPER or SET_USER_ID privilege when importing the database.

Durante il deploy su hosting condiviso, potresti incontrare l'errore #1227 - Accesso negato. Serve il privilegio SUPER o SET_USER_ID durante l'importazione del database.

Database Import FixFix Importazione Database

The SQL dump in DB/stabilimento_balneare.sql has been prepared for shared hosting (DEFINER clauses removed). If you have a dump with DEFINER clauses, remove them manually:

Il dump SQL in DB/stabilimento_balneare.sql è già preparato per hosting condiviso (clausole DEFINER rimosse). Se hai un dump con clausole DEFINER, rimuovile manualmente:

-- Remove DEFINER from procedures/functions:
-- FROM: CREATE DEFINER=`root`@`localhost` PROCEDURE ...
-- TO:   CREATE PROCEDURE ...

-- Remove DEFINER from views:
-- FROM: CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW ...
-- TO:   CREATE VIEW ...

Stored Procedures & FunctionsStored Procedure e Functions

The database includes:

Il database include:

  • InitializeDefaultLayout - Initializes beach layoutInizializza il layout spiaggia
  • CalculateDistance - Calculates distance between coordinatesCalcola la distanza tra coordinate

Some shared hosting providers don't support stored procedures/functions. In that case, these features will be handled by the PHP application.

Alcuni hosting condivisi non supportano stored procedure/functions. In tal caso, queste funzionalità saranno gestite dall'applicazione PHP.

Views

The database includes 4 views for reporting:

Il database include 4 views per i report:

  • v_dashboard_layout_disponibilita
  • v_disponibilita_servizi
  • v_disponibilita_servizi_check
  • v_saldo_prenotazioni