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 |
|---|---|
.env | Environment variables (database, API key, email) |
app/Config/App.php | Application settings (baseURL, timezone) |
app/Config/Database.php | Database connection settings |
app/Config/Routes.php | Route definitions |
app/Config/Filters.php | HTTP filters (auth, CORS) |
app/Config/Email.php | SMTP and email campaign settings |
| File | Scopo |
|---|---|
.env | Variabili d'ambiente (database, API key, email) |
app/Config/App.php | Impostazioni applicazione (baseURL, timezone) |
app/Config/Database.php | Configurazione connessione database |
app/Config/Routes.php | Definizione delle rotte |
app/Config/Filters.php | Filtri HTTP (autenticazione, CORS) |
app/Config/Email.php | Impostazioni 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 |
|---|---|---|
AuthApiController | 7 | Login, logout, profile, password change |
ClientiApiController | 9 | Client CRUD, search, export |
PrenotazioniApiController | 12 | Reservation CRUD, payments, cancellation |
ServiziApiController | 8 | Service catalog management |
MovimentiApiController | 9 | Financial transactions |
LayoutApiController | 28 | Beach layout, availability |
PeriodiApiController | 9 | Preset periods, price lists |
EmailApiController | 21 | Email campaigns, lists, recipients |
EmailTemplateApiController | 3 | Email template settings (SuperAdmin) |
PlannerApiController | 4 | Planner timeline, drag-and-drop updates |
UsersApiController | 5 | User management (SuperAdmin) |
ModuliApiController | 6 | Module permissions (SuperAdmin) |
DatatablesApiController | 10 | Server-side DataTables |
| Controller | Endpoint | Descrizione |
|---|---|---|
AuthApiController | 7 | Login, logout, profilo, cambio password |
ClientiApiController | 9 | CRUD clienti, ricerca, esportazione |
PrenotazioniApiController | 12 | CRUD prenotazioni, pagamenti, annullamento |
ServiziApiController | 8 | Gestione catalogo servizi |
MovimentiApiController | 9 | Transazioni finanziarie |
LayoutApiController | 28 | Layout spiaggia, disponibilità |
PeriodiApiController | 9 | Periodi preimpostati, listini prezzi |
EmailApiController | 21 | Campagne email, liste, destinatari |
EmailTemplateApiController | 3 | Impostazioni template email (SuperAdmin) |
PlannerApiController | 4 | Planner timeline, aggiornamento drag-and-drop |
UsersApiController | 5 | Gestione utenti (SuperAdmin) |
ModuliApiController | 6 | Permessi moduli (SuperAdmin) |
DatatablesApiController | 10 | DataTables 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';
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 |
|---|---|---|
200 | OK | Successful GET, PUT, DELETE |
201 | Created | Successful POST (resource created) |
400 | Bad Request | Invalid request data |
401 | Unauthorized | Invalid or missing token |
403 | Forbidden | Insufficient permissions |
404 | Not Found | Resource not found |
422 | Unprocessable Entity | Validation errors |
500 | Server Error | Internal error |
| Codice | Significato | Utilizzo |
|---|---|---|
200 | OK | GET, PUT, DELETE riusciti |
201 | Created | POST riuscito (risorsa creata) |
400 | Bad Request | Dati della richiesta non validi |
401 | Unauthorized | Token non valido o mancante |
403 | Forbidden | Permessi insufficienti |
404 | Not Found | Risorsa non trovata |
422 | Unprocessable Entity | Errori di validazione |
500 | Server Error | Errore 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\EmailTemplateApiController — extendsestende 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
| MethodMetodo | Endpoint | DescriptionDescrizione |
|---|---|---|
| GET | /api/v1/email/template-settings | Get template settingsOttieni impostazioni template |
| POST | /api/v1/email/template-settings | Save settings (multipart/form-data)Salva impostazioni (multipart/form-data) |
| GET | /api/v1/email/template-preview | Rendered 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: inviareremove_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): string— Entry point, checks if template is activeEntry point, controlla se template è attivogetTemplateSettings(): ?array— Loads 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): string— Builds complete TABLE-based HTML with:Costruisce HTML completo TABLE-based con:- Meta tag
color-scheme: light darkfor native dark modeMeta tagcolor-scheme: light darkper 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
- Meta tag
Database TableTabella Database
email_template_settings — Single 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 = productionin .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 = productionnel .env - Configurare le credenziali del database di produzione
- Impostare una
API_KEYsicura - 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 spiaggiaCalculateDistance- 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_disponibilitav_disponibilita_serviziv_disponibilita_servizi_checkv_saldo_prenotazioni