visualizzazione e gestione firme e lista firme con ACL

This commit is contained in:
2025-09-05 15:35:03 +02:00
parent 835e123444
commit 9b7f845414
5 changed files with 606 additions and 39 deletions

View File

@ -0,0 +1,243 @@
<?php
namespace Pcrt\Component\Circolari\Site\Controller;
\defined('_JEXEC') or die;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Date\Date;
class CircolareController extends BaseController
{
public function exportFirme()
{
// Token in GET per il download
if (!\Joomla\CMS\Session\Session::checkToken('get')) {
throw new \RuntimeException(\Joomla\CMS\Language\Text::_('JINVALID_TOKEN'), 403);
}
$app = \Joomla\CMS\Factory::getApplication();
$user = $app->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';
}
}

View File

@ -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.
*/