diff --git a/administrator/sql/updates/1.1.8.sql b/administrator/sql/updates/1.1.8.sql
new file mode 100644
index 0000000..9e10d04
--- /dev/null
+++ b/administrator/sql/updates/1.1.8.sql
@@ -0,0 +1,15 @@
+DROP TABLE IF EXISTS `#__circolari_firme`;
+
+CREATE TABLE `#__circolari_firme` (
+ `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `circolare_id` INT(11) UNSIGNED NOT NULL,
+ `user_id` INT(11) UNSIGNED NOT NULL,
+ `firmatipo_bottone_id` INT(11) UNSIGNED NOT NULL, -- id del bottone scelto
+ `firma_label` VARCHAR(190) NOT NULL, -- copia dell’etichetta al momento della firma
+ `data_firma` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `firma_unica` (`circolare_id`, `user_id`), -- 1 firma per utente x circolare
+ KEY `idx_circolare` (`circolare_id`),
+ KEY `idx_user` (`user_id`),
+ KEY `idx_bottone` (`firmatipo_bottone_id`)
+) ENGINE=InnoDB DEFAULT COLLATE=utf8mb4_unicode_ci;
diff --git a/site/src/Controller/CircolareController.php b/site/src/Controller/CircolareController.php
new file mode 100644
index 0000000..2209afd
--- /dev/null
+++ b/site/src/Controller/CircolareController.php
@@ -0,0 +1,243 @@
+getIdentity();
+ $id = $this->input->getInt('id');
+
+ // Permessi minimi (adatta come preferisci)
+ if (
+ !$user->authorise('core.admin', 'com_circolari')
+ && !$user->authorise('core.manage', 'com_circolari')
+ ) {
+ throw new \RuntimeException(\Joomla\CMS\Language\Text::_('JERROR_ALERTNOAUTHOR'), 403);
+ }
+
+ /** @var \Pcrt\Component\Circolari\Site\Model\CircolareModel $model */
+ $model = $this->getModel('Circolare', 'Site');
+ $rows = $model ? $model->getFirmeCircolare((int)$id) : [];
+
+ // === CLEAR OUTPUT BUFFERS ===
+ while (ob_get_level() > 0) {
+ @ob_end_clean();
+ }
+
+ // === HEADERS PER FORZARE DOWNLOAD ===
+ $filename = 'firme_circolare_' . (int)$id . '_' . date('Ymd_His') . '.csv';
+
+ // alcuni server riscrivono i content-type: duplichiamo la semantica "excel"
+ $app->clearHeaders();
+ $app->setHeader('Content-Description', 'File Transfer', true);
+ $app->setHeader('Content-Type', 'application/vnd.ms-excel; charset=utf-8', true);
+ $app->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"', true);
+ $app->setHeader('Content-Transfer-Encoding', 'binary', true);
+ $app->setHeader('Expires', '0', true);
+ $app->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0', true);
+ $app->setHeader('Pragma', 'public', true);
+ $app->sendHeaders();
+
+ // === OUTPUT CSV (separatore ;) con BOM UTF-8 ===
+ $out = fopen('php://output', 'w');
+
+ // BOM per Excel (non prima degli header!)
+ fwrite($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
+
+ // intestazione
+ fputcsv($out, ['ID Firma','ID Utente','Nome','Username','Email','Scelta','Data firma'], ';');
+
+ foreach ($rows as $r) {
+ $date = \Joomla\CMS\Factory::getDate($r['data_firma'])->format('d/m/Y H:i');
+ fputcsv($out, [
+ $r['id'],
+ $r['user_id'],
+ $r['user_name'],
+ $r['username'],
+ $r['email'],
+ $r['scelta_label'],
+ $date,
+ ], ';');
+ }
+
+ fclose($out);
+
+ // Importantissimo: termina qui, niente layout/HTML
+ $app->close();
+}
+
+
+ public function sign()
+ {
+ if (!Session::checkToken('post')) {
+ throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
+ }
+
+ $app = Factory::getApplication();
+ $user = $app->getIdentity();
+ $in = $app->input;
+
+ $circolareId = $in->getInt('id');
+ $bottoneId = $in->getInt('bottone_id');
+
+ if ($user->guest) {
+ $app->enqueueMessage(Text::_('COM_CIRCOLARI_ERR_LOGIN_REQUIRED'), 'error');
+ $this->setRedirect(Route::_('index.php?option=com_users&view=login', false));
+ return;
+ }
+
+ if ($circolareId <= 0 || $bottoneId <= 0) {
+ $app->enqueueMessage(Text::_('COM_CIRCOLARI_ERR_INVALID_REQUEST'), 'error');
+ $this->back($circolareId);
+ return;
+ }
+
+ $db = Factory::getContainer()->get('DatabaseDriver');
+
+ // 1) il bottone appartiene alla tipologia della circolare?
+ $q = $db->getQuery(true)
+ ->select($db->quoteName('b.label'))
+ ->from($db->quoteName('#__circolari_firmetipi_bottoni', 'b'))
+ ->join('INNER', $db->quoteName('#__circolari', 'c') . ' ON ' .
+ $db->quoteName('c.tipologia_firma_id') . ' = ' . $db->quoteName('b.firmatipo_id'))
+ ->where($db->quoteName('c.id') . ' = ' . (int)$circolareId)
+ ->where($db->quoteName('b.id') . ' = ' . (int)$bottoneId);
+ $db->setQuery($q);
+ $label = (string) $db->loadResult();
+
+ if ($label === '') {
+ $app->enqueueMessage(Text::_('COM_CIRCOLARI_ERR_INVALID_BUTTON'), 'error');
+ $this->back($circolareId);
+ return;
+ }
+
+ // 2) rileva lo schema della tabella firme
+ $cols = [];
+ foreach ($db->getTableColumns('#__circolari_firme', false) as $name => $def) {
+ $cols[strtolower($name)] = true;
+ }
+
+ $now = Factory::getDate()->toSql();
+
+ try {
+ if (isset($cols['firmatipo_bottone_id']) && isset($cols['firma_label'])) {
+ // Schema "nuovo"
+ $insert = $db->getQuery(true)
+ ->insert($db->quoteName('#__circolari_firme'))
+ ->columns($db->quoteName(['circolare_id', 'user_id', 'firmatipo_bottone_id', 'firma_label', 'data_firma']))
+ ->values(implode(',', [
+ (int)$circolareId,
+ (int)$user->id,
+ (int)$bottoneId,
+ $db->quote($label),
+ $db->quote($now)
+ ]));
+ $db->setQuery($insert)->execute();
+ } elseif (isset($cols['firma'])) {
+ // Schema "semplice" con ENUM firma
+ $enumValue = $this->mapLabelToEnum($label, $db, '#__circolari_firme', 'firma');
+ $insert = $db->getQuery(true)
+ ->insert($db->quoteName('#__circolari_firme'))
+ ->columns($db->quoteName(['circolare_id', 'user_id', 'firma', 'data_firma']))
+ ->values(implode(',', [
+ (int)$circolareId,
+ (int)$user->id,
+ $db->quote($enumValue),
+ $db->quote($now),
+ ]));
+ $db->setQuery($insert)->execute();
+ } else {
+ throw new \RuntimeException('Struttura tabella firme non supportata.');
+ }
+
+ $app->enqueueMessage(Text::_('COM_CIRCOLARI_MSG_SIGNED_OK'), 'message');
+ } catch (\RuntimeException $e) {
+ // 1062 = UNIQUE (già firmata)
+ if ((int)$e->getCode() === 1062 || stripos($e->getMessage(), 'Duplicate') !== false) {
+ $app->enqueueMessage(Text::_('COM_CIRCOLARI_ERR_ALREADY_SIGNED'), 'warning');
+ } else {
+ throw $e;
+ }
+ }
+
+ $this->back($circolareId);
+ }
+
+ private function back(int $id)
+ {
+ $this->setRedirect(Route::_('index.php?option=com_circolari&view=circolare&id=' . (int)$id, false));
+ }
+
+ /**
+ * Mappa la label del bottone ad un valore valido dell'ENUM `firma`.
+ * Se non combacia, prova a "sluggare" e, in ultima istanza, usa il primo valore dell'ENUM.
+ */
+ private function mapLabelToEnum(string $label, $db, string $table, string $column): string
+ {
+ // Leggi i valori dell'ENUM dal DDL della colonna
+ $ddl = $db->getTableColumns($table);
+ $type = '';
+ foreach ($ddl as $name => $def) {
+ if (strtolower($name) === strtolower($column) && isset($def->Type)) {
+ $type = $def->Type;
+ break;
+ }
+ }
+ if ($type && preg_match("/^enum\\((.*)\\)$/i", $type, $m)) {
+ $raw = $m[1];
+ $vals = array_map(function ($v) {
+ return trim($v, " '\"");
+ }, explode(',', $raw));
+ } else {
+ // fallback ai valori noti
+ $vals = ['presa_visione', 'aderisco', 'non_aderisco', 'non_in_servizio'];
+ }
+
+ $slug = $this->slugify($label);
+ if (in_array($slug, $vals, true)) {
+ return $slug;
+ }
+ // prova alcune normalizzazioni comuni
+ $map = [
+ 'presa-visione' => 'presa_visione',
+ 'presavisione' => 'presa_visione',
+ 'non-in-servizio' => 'non_in_servizio',
+ 'noninservizio' => 'non_in_servizio',
+ 'non-aderisco' => 'non_aderisco',
+ ];
+ if (isset($map[$slug]) && in_array($map[$slug], $vals, true)) {
+ return $map[$slug];
+ }
+
+ // ultima spiaggia: primo valore dell'ENUM
+ return $vals[0];
+ }
+
+ private function slugify(string $s): string
+ {
+ $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
+ $s = strtolower($s);
+ $s = preg_replace('~[^a-z0-9]+~', '_', $s);
+ $s = trim($s, '_');
+ return $s ?: 'presa_visione';
+ }
+}
diff --git a/site/src/Model/CircolareModel.php b/site/src/Model/CircolareModel.php
index 9a88f6b..20c84b0 100644
--- a/site/src/Model/CircolareModel.php
+++ b/site/src/Model/CircolareModel.php
@@ -7,6 +7,7 @@ namespace Pcrt\Component\Circolari\Site\Model;
use Joomla\CMS\MVC\Model\ItemModel;
use Joomla\CMS\Factory;
use Joomla\CMS\User\UserHelper;
+use Joomla\CMS\Language\Text;
class CircolareModel extends ItemModel
{
@@ -31,7 +32,7 @@ class CircolareModel extends ItemModel
$db = $this->getDatabase();
$q = $db->getQuery(true)
- ->select('c.*') // <<< evita errori di colonne
+ ->select('c.*')
->from($db->quoteName('#__circolari') . ' AS c')
->where('c.id = ' . (int) $pk);
@@ -126,6 +127,127 @@ class CircolareModel extends ItemModel
}
+ public function bottoneValidoPerCircolare(int $circolareId, int $bottoneId): bool
+ {
+ $db = Factory::getContainer()->get('DatabaseDriver');
+ $q = $db->getQuery(true)
+ ->select('1')
+ ->from($db->quoteName('#__circolari', 'c'))
+ ->join('INNER', $db->quoteName('#__circolari_firmetipi_bottoni', 'b')
+ . ' ON ' . $db->quoteName('b.firmatipo_id') . ' = ' . $db->quoteName('c.tipologia_firma_id'))
+ ->where($db->quoteName('c.id') . ' = ' . (int) $circolareId)
+ ->where($db->quoteName('b.id') . ' = ' . (int) $bottoneId);
+ $db->setQuery($q, 0, 1);
+ return (bool) $db->loadResult();
+ }
+
+ public function insertFirma(int $circolareId, int $userId, int $bottoneId): bool
+ {
+ $db = Factory::getContainer()->get('DatabaseDriver');
+ $now = Factory::getDate()->toSql();
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '';
+
+ $columns = ['circolare_id', 'user_id', 'firmatipo_bottone_id', 'signed_at', 'ip'];
+ $values = [
+ (int) $circolareId,
+ (int) $userId,
+ (int) $bottoneId,
+ $db->quote($now),
+ $db->quote($ip)
+ ];
+
+ $q = $db->getQuery(true)
+ ->insert($db->quoteName('#__circolari_firme'))
+ ->columns($db->quoteName($columns))
+ ->values(implode(',', $values));
+
+ try {
+ $db->setQuery($q)->execute();
+ } catch (\RuntimeException $e) {
+ // 1062 = duplicate key (violazione UNIQUE circolare_id+user_id)
+ if ((int) $e->getCode() === 1062 || stripos($e->getMessage(), 'Duplicate') !== false) {
+ throw new \RuntimeException(Text::_('COM_CIRCOLARI_ERR_ALREADY_SIGNED'), 409);
+ }
+ throw $e;
+ }
+ return true;
+ }
+
+ public function getFirmaUtente(int $circolareId, int $userId): ?object
+ {
+ if ($circolareId <= 0 || $userId <= 0) {
+ return null;
+ }
+ $db = Factory::getContainer()->get('DatabaseDriver');
+
+ // Schema compatibile con entrambe le versioni (ENUM "firma" oppure nuovo schema con firmatipo_bottone_id/firma_label)
+ $q = $db->getQuery(true)
+ ->select($db->quoteName(['id', 'circolare_id', 'user_id']))
+ ->select('' . $db->quoteName('firmatipo_bottone_id') . ' AS firmatipo_bottone_id')
+ ->select('' . $db->quoteName('firma_label') . ' AS firma_label')
+ //->select('' . $db->quoteName('firma') . ' AS firma_enum')
+ ->from($db->quoteName('#__circolari_firme'))
+ ->where($db->quoteName('circolare_id') . ' = ' . (int)$circolareId)
+ ->where($db->quoteName('user_id') . ' = ' . (int)$userId);
+ $db->setQuery($q, 0, 1);
+ $row = $db->loadObject();
+
+ return $row ?: null;
+ }
+
+ public function getFirmeCircolare(int $circolareId): array
+ {
+ if ($circolareId <= 0) {
+ return [];
+ }
+
+ $db = Factory::getContainer()->get('DatabaseDriver');
+
+ // Rileva le colonne della tabella firme (schema nuovo vs enum)
+ $cols = array_change_key_case($db->getTableColumns('#__circolari_firme', false));
+ $hasBtn = isset($cols['firmatipo_bottone_id']);
+ $hasLabel = isset($cols['firma_label']);
+ $hasEnum = isset($cols['firma']);
+
+ $q = $db->getQuery(true)
+ ->select([
+ 'f.id',
+ 'f.circolare_id',
+ 'f.user_id',
+ 'f.data_firma',
+ ($hasLabel ? 'f.firma_label' : 'NULL') . ' AS firma_label',
+ ($hasEnum ? 'f.firma' : 'NULL') . ' AS firma_enum'
+ ])
+ ->from($db->quoteName('#__circolari_firme', 'f'))
+ ->join('INNER', $db->quoteName('#__users', 'u') . ' ON u.id = f.user_id')
+ ->select('u.name AS user_name, u.username, u.email')
+ ->where('f.circolare_id = ' . (int) $circolareId)
+ ->order('f.data_firma DESC');
+
+ if ($hasBtn) {
+ $q->select('b.label AS bottone_label')
+ ->join('LEFT', $db->quoteName('#__circolari_firmetipi_bottoni', 'b') . ' ON b.id = f.firmatipo_bottone_id');
+ } else {
+ $q->select('NULL AS bottone_label');
+ }
+
+ $db->setQuery($q);
+ $rows = (array) $db->loadAssocList();
+
+ // Post-processing: normalizza la label scelta
+ foreach ($rows as &$r) {
+ $label = $r['firma_label'] ?: $r['bottone_label'] ?: $r['firma_enum'];
+ if ($label && $label === $r['firma_enum']) {
+ $label = ucwords(str_replace('_', ' ', $label)); // enum -> "Presa Visione"
+ }
+ $r['scelta_label'] = $label ?: '-';
+ }
+ unset($r);
+
+ return $rows;
+ }
+
+
/**
* Incrementa gli hits della circolare.
*/
diff --git a/site/tmpl/circolare/default.php b/site/tmpl/circolare/default.php
index 36131ea..a10ae47 100644
--- a/site/tmpl/circolare/default.php
+++ b/site/tmpl/circolare/default.php
@@ -1,13 +1,49 @@
getIdentity();
+$model = method_exists($this, 'getModel') ? $this->getModel('Circolare') : null;
$item = $this->item;
+$canAdmin = $user->authorise('core.admin', 'com_circolari') || $user->authorise('core.manage', 'com_circolari');
+
+// Firma dell'utente (se esiste)
+$firma = ($model && !$user->guest) ? $model->getFirmaUtente((int)$item->id, (int)$user->id) : null;
+$hasVoted = (bool) $firma;
+
+// Helper per confrontare anche in caso di schema ENUM
+$slugify = function (string $s): string {
+ $s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
+ $s = strtolower($s);
+ $s = preg_replace('~[^a-z0-9]+~', '_', $s);
+ return trim($s, '_');
+};
+
+$selectedBtnId = isset($firma->firmatipo_bottone_id) ? (int)$firma->firmatipo_bottone_id : 0;
+$selectedEnum = isset($firma->firma_enum) ? (string)$firma->firma_enum : '';
$buttons = $this->getModel()->getBottoniFirma((int)$item->tipologia_firma_id);
+
+$firme = [];
+if ($canAdmin) {
+ $model = method_exists($this, 'getModel') ? $this->getModel('Circolare') : null;
+ if (!$model) {
+ // fallback creazione model via MVCFactory se necessario
+ try {
+ $factory = $app->bootComponent('com_circolari')->getMVCFactory();
+ $model = $factory->createModel('Circolare', 'Site', ['ignore_request' => true]);
+ } catch (\Throwable $e) {
+ $model = null;
+ }
+ }
+ $firme = $model ? $model->getFirmeCircolare((int)$item->id) : [];
+}
?>
@@ -23,11 +59,42 @@ $buttons = $this->getModel()->getBottoniFirma((int)$item->tipologia_firma_id);
firma_obbligatoria && $this->getModel()->userCanFirmare($item->id, $this->getModel()->currentUser->id) && !empty($buttons)) : ?>
-
-
-
+
+ id;
+ $isSelected =
+ ($selectedBtnId > 0 && $selectedBtnId === $btnId)
+ || ($selectedEnum !== '' && in_array($selectedEnum, [
+ $slugify($btn->label),
+ str_replace('-', '_', $slugify($btn->label)), // normalizza trattini→underscore
+ ], true));
+
+ // Classi bootstrap: selezionato = "success", altri disattivi = outline-secondary
+ $btnClasses = 'btn btn-sm ' . ($isSelected ? 'btn-success' : ($hasVoted ? 'btn-outline-secondary' : 'btn-primary'));
+ ?>
+
+
+
+
+
+
+
@@ -37,4 +104,83 @@ $buttons = $this->getModel()->getBottoniFirma((int)$item->tipologia_firma_id);
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+ | Nome |
+ Username |
+ Scelta |
+ Data |
+
+
+
+
+
+ | Nessuna firma registrata. |
+
+
+
+
+ |
+ |
+
+
+ |
+
+ format('d/m/Y H:i'), ENT_QUOTES, 'UTF-8'); ?>
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/site/tmpl/circolari/default.php b/site/tmpl/circolari/default.php
index 4fdae4e..eacaf23 100644
--- a/site/tmpl/circolari/default.php
+++ b/site/tmpl/circolari/default.php
@@ -1,44 +1,85 @@
item;
-dump($item);
+$app = Factory::getApplication();
+$input = $app->getInput();
-$buttons = $this->getModel()->getBottoniFirma((int)$item->tipologia_firma_id);
-dump($this->getModel()->currentUser);
+// Prendo dati dalla view con fallback ai getter
+$items = $this->items ?? ($this->get('Items') ?? []);
+$pagination = $this->pagination ?? ($this->get('Pagination') ?? null);
+// Normalizzo items
+if (!is_array($items)) { $items = (array) $items; }
+$items = array_values(array_filter($items, static fn($it) => is_object($it) && !empty($it->id)));
+
+$Itemid = (int) $input->getInt('Itemid', 0);
?>
+
-
-
-
-
-
= $this->escape($item->title); ?>
-
-
-
-
-
Condividi
+
+
+
+
+
- firma_obbligatoria && !empty($buttons)) : ?>
-
-
-
-
+
+
+
+
+
+ | Titolo |
+ Visite |
+
+
+
+ $item) : ?>
+ id
+ . '&catid=' . (int) ($item->catid ?? 0)
+ . ($Itemid ? '&Itemid=' . $Itemid : '')
+ );
+ $title = $item->title ?? ('#' . (int) $item->id);
+ $hits = isset($item->hits) ? (int) $item->hits : null;
+ ?>
+
+ |
+
+
+
+ |
+
+
+
+ :
+
+
+ —
+
+ |
+
-
-
-
-
- description ?: $item->testo ?: $item->descrizione ?: '(Nessun testo)'; ?>
-
-
+
+
+
-
\ No newline at end of file
+ pagesTotal > 1) : ?>
+
+
+
+ getPagesLinks(); ?>
+
+
+ getPagesCounter(); ?>
+
+
+
+
+
+