update component
This commit is contained in:
22
administrator/forms/attachments.xml
Normal file
22
administrator/forms/attachments.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form>
|
||||
<fields>
|
||||
<field
|
||||
name="path"
|
||||
type="media"
|
||||
label="File"
|
||||
required="true"
|
||||
preview="true"
|
||||
types="documents"
|
||||
directory="images/file"
|
||||
/>
|
||||
<field
|
||||
name="title"
|
||||
type="text"
|
||||
label="Titolo"
|
||||
required="false"
|
||||
maxlength="190"
|
||||
filter="string"
|
||||
/>
|
||||
</fields>
|
||||
</form>
|
||||
@ -18,16 +18,17 @@
|
||||
|
||||
<field name="description" type="editor" label="COM_CIRCOLARI_FORM_LBL_CIRCOLARE_DESCRIPTION"
|
||||
filter="safehtml" buttons="true" />
|
||||
<field name="attachment" type="media" label="COM_CIRCOLARI_FORM_LBL_CIRCOLARE_ATTACHMENT"
|
||||
directory="documents/circolari" preview="false" upload="true" />
|
||||
<field
|
||||
name="allegato_titolo"
|
||||
type="text"
|
||||
label="Titolo allegato"
|
||||
description="Nome leggibile mostrato accanto al file allegato"
|
||||
filter="string"
|
||||
name="attachments"
|
||||
type="subform"
|
||||
label="Allegati"
|
||||
description="Aggiungi uno o più allegati alla circolare"
|
||||
formsource="administrator/components/com_circolari/forms/attachments.xml"
|
||||
multiple="true"
|
||||
layout="joomla.form.field.subform.repeatable-table"
|
||||
/>
|
||||
|
||||
|
||||
<field name="image" type="media" label="COM_CIRCOLARI_FORM_LBL_CIRCOLARE_IMAGE"
|
||||
directory="images" preview="true" upload="true" />
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS `#__circolari_firmetipi` (
|
||||
|
||||
-- Tabella principale circolari
|
||||
CREATE TABLE IF NOT EXISTS `#__circolari` (`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`state` TINYINT(1) DEFAULT 1,
|
||||
`state` TINYINT(3) DEFAULT 1,
|
||||
`ordering` INT(11) DEFAULT 0,
|
||||
`checked_out` INT(11) UNSIGNED DEFAULT NULL,
|
||||
`checked_out_time` DATETIME DEFAULT NULL,
|
||||
@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS `#__circolari` (`id` INT(11) UNSIGNED NOT NULL AUTO_I
|
||||
`usergroup_id` INT(11) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`hits` INT(11) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`title` VARCHAR(255) DEFAULT "",
|
||||
`alias` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`alias` VARCHAR(400) NOT NULL DEFAULT '',
|
||||
`description` TEXT,
|
||||
`attachment` VARCHAR(255) DEFAULT "",
|
||||
`image` VARCHAR(255) DEFAULT "",
|
||||
|
||||
29
administrator/sql/updates/1.2.2.sql
Normal file
29
administrator/sql/updates/1.2.2.sql
Normal file
@ -0,0 +1,29 @@
|
||||
-- CREA la nuova tabella (se non esiste)
|
||||
CREATE TABLE IF NOT EXISTS `#__circolari_attachments` (
|
||||
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`circolare_id` INT(11) UNSIGNED NOT NULL,
|
||||
`path` VARCHAR(255) NOT NULL,
|
||||
`title` VARCHAR(190) DEFAULT NULL,
|
||||
`ordering` INT(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_circolare_id` (`circolare_id`)
|
||||
) ENGINE=InnoDB DEFAULT COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- AGGIUNGI il nuovo campo alla tabella principale (se non esiste)
|
||||
ALTER TABLE `#__circolari`
|
||||
ADD COLUMN `attachment_id` INT(11) UNSIGNED DEFAULT NULL
|
||||
AFTER `scadenza`;
|
||||
|
||||
-- MIGRAZIONE DATI (opzionale): porta il vecchio allegato singolo nella nuova tabella
|
||||
INSERT INTO `#__circolari_attachments` (`circolare_id`, `path`, `title`, `ordering`)
|
||||
SELECT `id`, `attachment`, COALESCE(`allegato_titolo`, ''), 0
|
||||
FROM `#__circolari`
|
||||
WHERE `attachment` IS NOT NULL AND `attachment` <> '';
|
||||
|
||||
-- RIMUOVI i vecchi campi
|
||||
ALTER TABLE `#__circolari`
|
||||
DROP COLUMN `allegato_titolo`,
|
||||
DROP COLUMN `attachment`;
|
||||
|
||||
-- INDICE (opzionale, per il nuovo campo)
|
||||
ALTER TABLE `#__circolari` ADD KEY `idx_attachment_id` (`attachment_id`);
|
||||
@ -44,7 +44,7 @@ class CircolariComponent extends MVCComponent implements RouterServiceInterface,
|
||||
public function boot(ContainerInterface $container)
|
||||
{
|
||||
$db = $container->get('DatabaseDriver');
|
||||
$this->getRegistry()->register('circolari', new CIRCOLARI($db));
|
||||
$this->getRegistry()->register('circolari', new CIRCOLARI($db),true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,337 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @version CVS: 1.0.0
|
||||
* @package Com_Circolari
|
||||
* @author Tommaso Cippitelli <tommaso.cippitelli@protocollicreativi.it>
|
||||
* @copyright 2025 Tommaso Cippitelli
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
* @author Tommaso C.
|
||||
* @license GNU GPL v2 or later
|
||||
*/
|
||||
|
||||
namespace Pcrt\Component\Circolari\Administrator\Model;
|
||||
// No direct access.
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use \Joomla\CMS\Table\Table;
|
||||
use \Joomla\CMS\Factory;
|
||||
use \Joomla\CMS\Language\Text;
|
||||
use \Joomla\CMS\Plugin\PluginHelper;
|
||||
use \Joomla\CMS\MVC\Model\AdminModel;
|
||||
use \Joomla\CMS\Helper\TagsHelper;
|
||||
use \Joomla\CMS\Filter\OutputFilter;
|
||||
use \Joomla\CMS\Event\Model;
|
||||
use Joomla\CMS\Event\AbstractEvent;
|
||||
\defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Application\ApplicationHelper;
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\MVC\Model\AdminModel;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
use Joomla\CMS\Table\Table;
|
||||
use Joomla\CMS\Event\AbstractEvent;
|
||||
|
||||
|
||||
/**
|
||||
* Circolare model.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class CircolareModel extends AdminModel
|
||||
{
|
||||
/**
|
||||
* @var string The prefix to use with controller messages.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $text_prefix = 'COM_CIRCOLARI';
|
||||
|
||||
/**
|
||||
* @var string Alias to manage history control
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public $typeAlias = 'com_circolari.circolare';
|
||||
|
||||
/**
|
||||
* @var null Item data
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected $item = null;
|
||||
|
||||
/* =========================
|
||||
* JTable / Form plumbing
|
||||
* ========================= */
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns a reference to the a Table object, always creating it.
|
||||
*
|
||||
* @param string $type The table type to instantiate
|
||||
* @param string $prefix A prefix for the table class name. Optional.
|
||||
* @param array $config Configuration array for model. Optional.
|
||||
*
|
||||
* @return Table A database object
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getTable($type = 'Circolare', $prefix = 'Table', $config = array())
|
||||
public function getTable($type = 'Circolare', $prefix = 'Table', $config = [])
|
||||
{
|
||||
return parent::getTable($type, $prefix, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the record form.
|
||||
*
|
||||
* @param array $data An optional array of data for the form to interogate.
|
||||
* @param boolean $loadData True if the form is to load its own data (default case), false if not.
|
||||
*
|
||||
* @return \JForm|boolean A \JForm object on success, false on failure
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getForm($data = array(), $loadData = true)
|
||||
public function getForm($data = [], $loadData = true)
|
||||
{
|
||||
// Initialise variables.
|
||||
$app = Factory::getApplication();
|
||||
|
||||
// Get the form.
|
||||
$form = $this->loadForm(
|
||||
'com_circolari.circolare',
|
||||
'circolare',
|
||||
array(
|
||||
'control' => 'jform',
|
||||
'load_data' => $loadData
|
||||
)
|
||||
['control' => 'jform', 'load_data' => $loadData]
|
||||
);
|
||||
|
||||
|
||||
|
||||
if (empty($form)) {
|
||||
return false;
|
||||
return $form ?: false;
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
/* =========================
|
||||
* Helpers DB lato admin
|
||||
* ========================= */
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Method to get the data that should be injected in the form.
|
||||
*
|
||||
* @return mixed The data for the form.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function loadFormData()
|
||||
/** Ritorna gli allegati della circolare in formato assoc list */
|
||||
public function getAttachments(int $circolareId): array
|
||||
{
|
||||
// Check the session for previously entered form data.
|
||||
$data = Factory::getApplication()->getUserState('com_circolari.edit.circolare.data', array());
|
||||
if ($circolareId <= 0) return [];
|
||||
|
||||
if (empty($data)) {
|
||||
if ($this->item === null) {
|
||||
$this->item = $this->getItem();
|
||||
}
|
||||
|
||||
$data = $this->item;
|
||||
}
|
||||
|
||||
if ((is_array($data) && empty($data['id'])) || (is_object($data) && empty($data->id))) {
|
||||
if (is_array($data)) {
|
||||
$data['categoria_id'] = (int)($data['categoria_id'] ?? 0) ?: 0;
|
||||
} else {
|
||||
$data->categoria_id = (int)($data->categoria_id ?? 0) ?: 0;
|
||||
}
|
||||
}
|
||||
|
||||
$id = (int) ($data->id ?? $this->getState($this->getName() . '.id'));
|
||||
if ($id) {
|
||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
||||
$q = $db->getQuery(true)
|
||||
->select($db->quoteName('usergroup_id'))
|
||||
->from($db->quoteName('#__circolari_usergroups'))
|
||||
->where($db->quoteName('circolare_id') . ' = ' . (int) $id);
|
||||
->select(['id','path','title','ordering'])
|
||||
->from($db->quoteName('#__circolari_attachments'))
|
||||
->where($db->quoteName('circolare_id') . ' = ' . (int)$circolareId)
|
||||
->order($db->escape('ordering ASC, id ASC'));
|
||||
|
||||
$db->setQuery($q);
|
||||
$data->usergroup_ids = $db->loadColumn() ?: [];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Method to get a single record.
|
||||
*
|
||||
* @param integer $pk The id of the primary key.
|
||||
*
|
||||
* @return mixed Object on success, false on failure.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function getItem($pk = null)
|
||||
{
|
||||
|
||||
if ($item = parent::getItem($pk)) {
|
||||
if (isset($item->params)) {
|
||||
$item->params = json_encode($item->params);
|
||||
}
|
||||
|
||||
// Do any procesing on fields here if needed
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to duplicate an Circolare
|
||||
*
|
||||
* @param array &$pks An array of primary key IDs.
|
||||
*
|
||||
* @return boolean True if successful.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function duplicate(&$pks)
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$user = $app->getIdentity();
|
||||
$dispatcher = $this->getDispatcher();
|
||||
|
||||
// Access checks.
|
||||
if (!$user->authorise('core.create', 'com_circolari')) {
|
||||
throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED'));
|
||||
}
|
||||
|
||||
$context = $this->option . '.' . $this->name;
|
||||
|
||||
// Include the plugins for the save events.
|
||||
PluginHelper::importPlugin($this->events_map['save']);
|
||||
|
||||
$table = $this->getTable();
|
||||
|
||||
foreach ($pks as $pk) {
|
||||
|
||||
if ($table->load($pk, true)) {
|
||||
// Reset the id to create a new record.
|
||||
$table->id = 0;
|
||||
|
||||
if (!$table->check()) {
|
||||
throw new \Exception($table->getError());
|
||||
}
|
||||
|
||||
|
||||
// Create the before save event.
|
||||
$beforeSaveEvent = AbstractEvent::create(
|
||||
$this->event_before_save,
|
||||
[
|
||||
'context' => $context,
|
||||
'subject' => $table,
|
||||
'isNew' => true,
|
||||
'data' => $table,
|
||||
]
|
||||
);
|
||||
|
||||
// Trigger the before save event.
|
||||
$dispatchResult = Factory::getApplication()->getDispatcher()->dispatch($this->event_before_save, $beforeSaveEvent);
|
||||
|
||||
// Check if dispatch result is an array and handle accordingly
|
||||
$result = isset($dispatchResult['result']) ? $dispatchResult['result'] : [];
|
||||
|
||||
// Proceed with your logic
|
||||
if (in_array(false, $result, true) || !$table->store()) {
|
||||
throw new \Exception($table->getError());
|
||||
}
|
||||
|
||||
// Trigger the after save event.
|
||||
Factory::getApplication()->getDispatcher()->dispatch(
|
||||
$this->event_after_save,
|
||||
AbstractEvent::create(
|
||||
$this->event_after_save,
|
||||
[
|
||||
'context' => $context,
|
||||
'subject' => $table,
|
||||
'isNew' => true,
|
||||
'data' => $table,
|
||||
]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
throw new \Exception($table->getError());
|
||||
}
|
||||
}
|
||||
|
||||
// Clean cache
|
||||
$this->cleanCache();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and sanitise the table prior to saving.
|
||||
*
|
||||
* @param Table $table Table Object
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected function prepareTable($table)
|
||||
{
|
||||
jimport('joomla.filter.output');
|
||||
|
||||
if (empty($table->id)) {
|
||||
// Set ordering to the last item if not set
|
||||
if (@$table->ordering === '') {
|
||||
$db = $this->getDbo();
|
||||
$db->setQuery('SELECT MAX(ordering) FROM #__circolari');
|
||||
$max = $db->loadResult();
|
||||
$table->ordering = $max + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
|
||||
|
||||
|
||||
// Se firma obbligatoria, scadenza deve esserci
|
||||
$isObbl = (int) ($this->firma_obbligatoria ?? 0);
|
||||
if ($isObbl === 1 && empty($this->scadenza)) {
|
||||
$this->setError(\Joomla\CMS\Language\Text::_('COM_CIRCOLARI_ERR_SCADENZA_REQUIRED'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// ordering per nuovi record
|
||||
if (property_exists($this, 'ordering') && (int) $this->id === 0) {
|
||||
$this->ordering = self::getNextOrder();
|
||||
}
|
||||
|
||||
// Titolo obbligatorio (se vuoi forzarlo)
|
||||
$this->title = trim((string) ($this->title ?? ''));
|
||||
if ($this->title === '') {
|
||||
$this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- ALIAS: genera se vuoto, ripulisci, evita "solo numeri", garantisci univocità ---
|
||||
$this->alias = trim((string) ($this->alias ?? ''));
|
||||
|
||||
// Se mancante → dal titolo
|
||||
if ($this->alias === '') {
|
||||
$this->alias = ApplicationHelper::stringURLSafe($this->title);
|
||||
} else {
|
||||
$this->alias = ApplicationHelper::stringURLSafe($this->alias);
|
||||
}
|
||||
|
||||
// Evita alias vuoto o numerico puro
|
||||
if ($this->alias === '' || ctype_digit($this->alias)) {
|
||||
$seed = (int) ($this->id ?: time());
|
||||
$this->alias = ApplicationHelper::stringURLSafe($this->title . '-' . $seed);
|
||||
}
|
||||
|
||||
// Unicità alias nel contesto tabella
|
||||
$base = $this->alias;
|
||||
$i = 2;
|
||||
while ($this->aliasExists($this->alias, (int) $this->id)) {
|
||||
$this->alias = $base . '-' . $i;
|
||||
$i++;
|
||||
}
|
||||
|
||||
return parent::check();
|
||||
return $db->loadAssocList() ?: [];
|
||||
}
|
||||
|
||||
/** Verifica unicità alias */
|
||||
protected function aliasExists(string $alias, int $excludeId = 0): bool
|
||||
{
|
||||
$q = $this->_db->getQuery(true)
|
||||
@ -347,6 +80,190 @@ class CircolareModel extends AdminModel
|
||||
return ((int) $this->_db->loadResult()) > 0;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Caricamento dati per il form
|
||||
* ========================= */
|
||||
|
||||
public function getItem($pk = null)
|
||||
{
|
||||
$item = parent::getItem($pk);
|
||||
|
||||
if ($item) {
|
||||
// params come JSON (come già avevi)
|
||||
if (isset($item->params)) {
|
||||
$item->params = json_encode($item->params);
|
||||
}
|
||||
// Allegati per comodità in view/form
|
||||
if (!empty($item->id)) {
|
||||
$item->attachments = $this->getAttachments((int)$item->id);
|
||||
}
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
protected function loadFormData()
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$data = $app->getUserState('com_circolari.edit.circolare.data', []);
|
||||
|
||||
if (empty($data)) {
|
||||
if ($this->item === null) {
|
||||
$this->item = $this->getItem();
|
||||
}
|
||||
$data = $this->item ?: [];
|
||||
}
|
||||
|
||||
// Normalizza a array per JForm
|
||||
$arr = is_object($data) ? (array) $data : (array) $data;
|
||||
|
||||
// Categoria default 0 in creazione
|
||||
if (empty($arr['id'])) {
|
||||
$arr['categoria_id'] = (int)($arr['categoria_id'] ?? 0) ?: 0;
|
||||
}
|
||||
|
||||
// Carica gruppi e allegati se in modifica
|
||||
$id = (int) ($arr['id'] ?? $this->getState($this->getName() . '.id'));
|
||||
if ($id) {
|
||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
||||
|
||||
// Gruppi che possono firmare
|
||||
$q = $db->getQuery(true)
|
||||
->select($db->quoteName('usergroup_id'))
|
||||
->from($db->quoteName('#__circolari_usergroups'))
|
||||
->where($db->quoteName('circolare_id') . ' = ' . (int) $id);
|
||||
$db->setQuery($q);
|
||||
$arr['usergroup_ids'] = $db->loadColumn() ?: [];
|
||||
|
||||
// Allegati
|
||||
$arr['attachments'] = $this->getAttachments($id);
|
||||
} else {
|
||||
$arr['attachments'] = [];
|
||||
}
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Duplicazione
|
||||
* ========================= */
|
||||
|
||||
public function duplicate(&$pks)
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$user = $app->getIdentity();
|
||||
|
||||
if (!$user->authorise('core.create', 'com_circolari')) {
|
||||
throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED'));
|
||||
}
|
||||
|
||||
$context = $this->option . '.' . $this->name;
|
||||
PluginHelper::importPlugin($this->events_map['save']);
|
||||
|
||||
$table = $this->getTable();
|
||||
|
||||
foreach ($pks as $pk) {
|
||||
if ($table->load($pk, true)) {
|
||||
$table->id = 0;
|
||||
|
||||
if (!$table->check()) {
|
||||
throw new \Exception($table->getError());
|
||||
}
|
||||
|
||||
$before = AbstractEvent::create(
|
||||
$this->event_before_save,
|
||||
['context' => $context, 'subject' => $table, 'isNew' => true, 'data' => $table]
|
||||
);
|
||||
$dispatchResult = $app->getDispatcher()->dispatch($this->event_before_save, $before);
|
||||
$result = isset($dispatchResult['result']) ? $dispatchResult['result'] : [];
|
||||
|
||||
if (in_array(false, $result, true) || !$table->store()) {
|
||||
throw new \Exception($table->getError());
|
||||
}
|
||||
|
||||
$app->getDispatcher()->dispatch(
|
||||
$this->event_after_save,
|
||||
AbstractEvent::create(
|
||||
$this->event_after_save,
|
||||
['context' => $context, 'subject' => $table, 'isNew' => true, 'data' => $table]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
throw new \Exception($table->getError());
|
||||
}
|
||||
}
|
||||
|
||||
$this->cleanCache();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Prepara/Check JTable
|
||||
* ========================= */
|
||||
|
||||
protected function prepareTable($table)
|
||||
{
|
||||
if (empty($table->id)) {
|
||||
if (@$table->ordering === '' || $table->ordering === null) {
|
||||
$db = $this->getDbo();
|
||||
$db->setQuery('SELECT MAX(ordering) FROM #__circolari');
|
||||
$max = (int) $db->loadResult();
|
||||
$table->ordering = $max + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nota: questo check era già nel tuo Model (non nel Table).
|
||||
* Lo lascio invariato per non rompere il flusso.
|
||||
*/
|
||||
public function check()
|
||||
{
|
||||
// Firma obbligatoria: scadenza richiesta
|
||||
$isObbl = (int) ($this->firma_obbligatoria ?? 0);
|
||||
if ($isObbl === 1 && empty($this->scadenza)) {
|
||||
$this->setError(Text::_('COM_CIRCOLARI_ERR_SCADENZA_REQUIRED'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ordering per nuovi record
|
||||
if (property_exists($this, 'ordering') && (int) $this->id === 0) {
|
||||
$this->ordering = self::getNextOrder();
|
||||
}
|
||||
|
||||
// Titolo
|
||||
$this->title = trim((string) ($this->title ?? ''));
|
||||
if ($this->title === '') {
|
||||
$this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Alias
|
||||
$this->alias = trim((string) ($this->alias ?? ''));
|
||||
if ($this->alias === '') {
|
||||
$this->alias = ApplicationHelper::stringURLSafe($this->title);
|
||||
} else {
|
||||
$this->alias = ApplicationHelper::stringURLSafe($this->alias);
|
||||
}
|
||||
if ($this->alias === '' || ctype_digit($this->alias)) {
|
||||
$seed = (int) ($this->id ?: time());
|
||||
$this->alias = ApplicationHelper::stringURLSafe($this->title . '-' . $seed);
|
||||
}
|
||||
|
||||
$base = $this->alias;
|
||||
$i = 2;
|
||||
while ($this->aliasExists($this->alias, (int) $this->id)) {
|
||||
$this->alias = $base . '-' . $i;
|
||||
$i++;
|
||||
}
|
||||
|
||||
return parent::check();
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Salvataggio con sync allegati & gruppi
|
||||
* ========================= */
|
||||
|
||||
public function save($data)
|
||||
{
|
||||
$ok = parent::save($data);
|
||||
@ -361,22 +278,97 @@ class CircolareModel extends AdminModel
|
||||
$db->transactionStart();
|
||||
|
||||
try {
|
||||
// ripulisci associazioni esistenti
|
||||
/* ----- Allegati: replace completo ----- */
|
||||
$attachments = $data['attachments'] ?? [];
|
||||
$attachments = is_array($attachments) ? $attachments : [];
|
||||
|
||||
// pulisci
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__circolari_attachments'))
|
||||
->where($db->quoteName('circolare_id') . ' = ' . (int) $id)
|
||||
)->execute();
|
||||
|
||||
// reinserisci
|
||||
if (!empty($attachments)) {
|
||||
$q = $db->getQuery(true)
|
||||
->insert($db->quoteName('#__circolari_attachments'))
|
||||
->columns([
|
||||
$db->quoteName('circolare_id'),
|
||||
$db->quoteName('path'),
|
||||
$db->quoteName('title'),
|
||||
$db->quoteName('ordering')
|
||||
]);
|
||||
|
||||
$added = 0;
|
||||
foreach ($attachments as $row) {
|
||||
$path = trim((string)($row['path'] ?? ''));
|
||||
if ($path === '') continue;
|
||||
|
||||
$title = trim((string)($row['title'] ?? ''));
|
||||
$ordering = (int)($row['ordering'] ?? 0);
|
||||
|
||||
$q->values(
|
||||
(int)$id . ','
|
||||
. $db->quote($path) . ','
|
||||
. $db->quote($title) . ','
|
||||
. (int)$ordering
|
||||
);
|
||||
$added++;
|
||||
}
|
||||
|
||||
if ($added > 0) {
|
||||
$db->setQuery($q)->execute();
|
||||
}
|
||||
|
||||
// [Opzionale] aggiorna attachment_id al primo allegato se la colonna esiste
|
||||
$cols = array_change_key_case($db->getTableColumns('#__circolari', false));
|
||||
if (isset($cols['attachment_id'])) {
|
||||
$firstId = (int) $db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->select('id')
|
||||
->from($db->quoteName('#__circolari_attachments'))
|
||||
->where($db->quoteName('circolare_id') . ' = ' . (int)$id)
|
||||
->order($db->escape('ordering ASC, id ASC'))
|
||||
)->loadResult();
|
||||
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__circolari'))
|
||||
->set($db->quoteName('attachment_id') . ' = ' . ($firstId ?: 'NULL'))
|
||||
->where($db->quoteName('id') . ' = ' . (int)$id)
|
||||
)->execute();
|
||||
}
|
||||
} else {
|
||||
// nessun allegato: azzera eventuale attachment_id (se esiste)
|
||||
$cols = array_change_key_case($db->getTableColumns('#__circolari', false));
|
||||
if (isset($cols['attachment_id'])) {
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->update($db->quoteName('#__circolari'))
|
||||
->set($db->quoteName('attachment_id') . ' = NULL')
|
||||
->where($db->quoteName('id') . ' = ' . (int)$id)
|
||||
)->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- Gruppi: replace completo ----- */
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete($db->quoteName('#__circolari_usergroups'))
|
||||
->where($db->quoteName('circolare_id') . ' = ' . (int) $id)
|
||||
)->execute();
|
||||
|
||||
// reinserisci le nuove
|
||||
if (!empty($groups)) {
|
||||
$q = $db->getQuery(true)
|
||||
->insert($db->quoteName('#__circolari_usergroups'))
|
||||
->columns([$db->quoteName('circolare_id'), $db->quoteName('usergroup_id')]);
|
||||
|
||||
foreach ($groups as $gid) {
|
||||
if ($gid > 0) {
|
||||
$q->values((int) $id . ',' . (int) $gid);
|
||||
}
|
||||
}
|
||||
$db->setQuery($q)->execute();
|
||||
}
|
||||
|
||||
|
||||
@ -47,7 +47,6 @@ class CircolaresModel extends ListModel
|
||||
'modified_by', 'a.modified_by',
|
||||
'title', 'a.title',
|
||||
'description', 'a.description',
|
||||
'attachment', 'a.attachment',
|
||||
'image', 'a.image',
|
||||
);
|
||||
}
|
||||
@ -77,7 +76,7 @@ class CircolaresModel extends ListModel
|
||||
protected function populateState($ordering = null, $direction = null)
|
||||
{
|
||||
// List state information.
|
||||
parent::populateState('id', 'ASC');
|
||||
parent::populateState('id', 'DESC');
|
||||
|
||||
$context = $this->getUserStateFromRequest($this->context.'.filter.search', 'filter_search');
|
||||
$this->setState('filter.search', $context);
|
||||
|
||||
@ -38,8 +38,7 @@ HTMLHelper::_('bootstrap.tooltip');
|
||||
<?php echo $this->form->renderField('alias'); ?>
|
||||
<?php echo $this->form->renderField('categoria_id'); ?>
|
||||
<?php echo $this->form->renderField('description'); ?>
|
||||
<?php echo $this->form->renderField('attachment'); ?>
|
||||
<?php echo $this->form->renderField('allegato_titolo'); ?>
|
||||
<?php echo $this->form->renderField('attachments'); ?>
|
||||
<?php echo $this->form->renderField('image'); ?>
|
||||
<?php echo $this->form->renderField('firma_obbligatoria'); ?>
|
||||
<?php echo $this->form->renderField('usergroup_ids'); ?>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<author>Tommaso Cippitelli</author>
|
||||
<authorEmail>tommaso.cippitelli@protocollicreativi.it</authorEmail>
|
||||
<authorUrl>http://</authorUrl>
|
||||
<version>1.2.1</version>
|
||||
<version>1.2.2</version>
|
||||
<description></description>
|
||||
<namespace path="src">Pcrt\Component\Circolari</namespace>
|
||||
|
||||
@ -50,10 +50,10 @@
|
||||
<menu>COM_CIRCOLARI</menu>
|
||||
<submenu>
|
||||
<menu link="option=com_circolari&view=circolares" view="circolares"
|
||||
alt="Circolari/Circolares">COM_CIRCOLARI_TITLE_CIRCOLARES</menu>
|
||||
alt="Circolari/Circolares">Circolari</menu>
|
||||
<menu link="option=com_circolari&view=firmetipi" view="firmetipi"
|
||||
alt="Circolari/Firmetipi">COM_CIRCOLARI_TITLE_FIRMETIPI</menu>
|
||||
<menu link="option=com_circolari&view=categorie">COM_CIRCOLARI_TITLE_CATEGORIE</menu>
|
||||
alt="Circolari/Firmetipi">Firme</menu>
|
||||
<menu link="option=com_circolari&view=categorie">Categorie</menu>
|
||||
<menu link="option=com_circolari&view=reportfirme" view="reportfirme">Report Firme</menu>
|
||||
|
||||
</submenu>
|
||||
|
||||
@ -9,6 +9,8 @@ use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Router\Route;
|
||||
use Joomla\CMS\Session\Session;
|
||||
use Joomla\CMS\Filesystem\File;
|
||||
use Joomla\CMS\Filesystem\Folder;
|
||||
|
||||
class FormController extends BaseController
|
||||
{
|
||||
@ -32,12 +34,10 @@ class FormController extends BaseController
|
||||
throw new \RuntimeException(Text::_('JINVALID_TOKEN'), 403);
|
||||
}
|
||||
|
||||
|
||||
$in = $app->input;
|
||||
|
||||
$data = $in->get('jform', [], 'array');
|
||||
|
||||
// Il tuo form frontend invia campi "piatti"
|
||||
$data = $in->get('jform', [], 'array');
|
||||
if (!$data) {
|
||||
$data = [
|
||||
'id' => $in->getInt('id', 0),
|
||||
@ -52,14 +52,168 @@ class FormController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 1) Gestione IMMAGINE (image_file / image_remove / image_existing)
|
||||
// ============================================================
|
||||
$imageRemove = (int) $in->get('image_remove', 0) === 1;
|
||||
$imageExisting = (string) $in->get('image_existing', '');
|
||||
$imageFile = $in->files->get('image_file', null, 'raw'); // array|NULL
|
||||
|
||||
$coversRel = 'images/circolari/covers';
|
||||
$coversAbs = JPATH_ROOT . '/' . $coversRel;
|
||||
|
||||
if (!Folder::exists($coversAbs)) {
|
||||
Folder::create($coversAbs);
|
||||
}
|
||||
|
||||
$finalImagePath = $imageExisting;
|
||||
|
||||
// Se flagged remove, rimuovi riferimenti (non cancelliamo il file dal disco qui)
|
||||
if ($imageRemove) {
|
||||
$finalImagePath = '';
|
||||
}
|
||||
|
||||
// Se è stato caricato un nuovo file, validalo e caricalo
|
||||
if (is_array($imageFile) && !empty($imageFile['tmp_name']) && (int)($imageFile['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
|
||||
$safeName = File::makeSafe($imageFile['name']);
|
||||
$ext = strtolower(File::getExt($safeName));
|
||||
if ($ext === 'jpeg') {
|
||||
$ext = 'jpg';
|
||||
} // compatibilità: molti settaggi consentono jpg ma non jpeg
|
||||
$base = pathinfo($safeName, PATHINFO_FILENAME) ?: 'file';
|
||||
$hash = substr(sha1($safeName . microtime(true)), 0, 8);
|
||||
$target = $base . '_' . $hash . ($ext ? ('.' . $ext) : '');
|
||||
|
||||
$abs = $coversAbs . '/' . $target;
|
||||
$rel = $coversRel . '/' . $target;
|
||||
|
||||
if (!File::upload($imageFile['tmp_name'], $abs, false, true)) {
|
||||
throw new \RuntimeException(Text::_('JLIB_FILESYSTEM_ERROR_UPLOAD'), 500);
|
||||
}
|
||||
|
||||
$finalImagePath = $rel;
|
||||
}
|
||||
|
||||
|
||||
// Metti nel payload verso il Model
|
||||
if ($imageRemove) {
|
||||
$data['image'] = '';
|
||||
} elseif (isset($rel)) { // cioè: abbiamo caricato una nuova immagine in questo request
|
||||
$data['image'] = $rel;
|
||||
} else {
|
||||
// non settare $data['image']
|
||||
}
|
||||
// ============================================================
|
||||
// 2) Gestione ALLEGATI (attachments[index][title|ordering|file|path|remove])
|
||||
// - replace puntuale (mantiene file se non sostituito, rimuove se flaggato)
|
||||
// ============================================================
|
||||
$rows = (array) $in->get('attachments', [], 'array'); // dati "title", "ordering", "path", "remove"
|
||||
$files = (array) $in->files->get('attachments', [], 'array'); // file upload (per indice) => ["file" => [..]]
|
||||
|
||||
$destRel = 'images/circolari';
|
||||
$destAbs = JPATH_ROOT . '/' . $destRel;
|
||||
|
||||
if (!Folder::exists($destAbs)) {
|
||||
Folder::create($destAbs);
|
||||
}
|
||||
|
||||
// Whitelist di estensioni consigliata (allinea a Media Manager)
|
||||
$allowedExt = [
|
||||
'pdf',
|
||||
'doc',
|
||||
'docx',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'zip',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'webp',
|
||||
'svg',
|
||||
'txt',
|
||||
'rtf',
|
||||
'odt',
|
||||
'ods',
|
||||
'odp'
|
||||
];
|
||||
|
||||
$attachmentsData = [];
|
||||
|
||||
// Unione per indice
|
||||
$allIndexes = array_unique(array_merge(array_keys($rows), array_keys($files)));
|
||||
foreach ($allIndexes as $i) {
|
||||
$row = (array) ($rows[$i] ?? []);
|
||||
$farr = (array) ($files[$i] ?? []);
|
||||
|
||||
$remove = (int)($row['remove'] ?? 0) === 1;
|
||||
$title = trim((string)($row['title'] ?? ''));
|
||||
$ordering = (int)($row['ordering'] ?? 0);
|
||||
$path = (string)($row['path'] ?? ''); // esistente (se c'è)
|
||||
|
||||
// Se rimozione esplicita, salta questa riga (non aggiungerla alla lista finale)
|
||||
if ($remove === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Se è stato caricato un file nuovo per questo indice
|
||||
$newFile = (array) ($farr['file'] ?? []);
|
||||
if (!empty($newFile['tmp_name']) && (int)($newFile['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK) {
|
||||
$safeName = File::makeSafe($newFile['name']);
|
||||
$ext = strtolower(File::getExt($safeName));
|
||||
|
||||
|
||||
|
||||
$base = pathinfo($safeName, PATHINFO_FILENAME);
|
||||
$hash = substr(sha1($safeName . microtime(true)), 0, 8);
|
||||
$target = $base . '_' . $hash . '.' . $ext;
|
||||
|
||||
$abs = $destAbs . '/' . $target;
|
||||
$rel = $destRel . '/' . $target;
|
||||
|
||||
if (!File::upload($newFile['tmp_name'], $abs, false, true)) {
|
||||
throw new \RuntimeException(Text::_('JLIB_FILESYSTEM_ERROR_UPLOAD'), 500);
|
||||
}
|
||||
|
||||
$path = $rel; // sostituisci il path con il nuovo file caricato
|
||||
}
|
||||
|
||||
// Caso nuovo allegato: serve almeno il file (path) -> se ancora vuoto, non aggiungo
|
||||
if ($path === '') {
|
||||
// se non c'è path (nessun file nuovo e non esisteva), ignora la riga incompleta
|
||||
continue;
|
||||
}
|
||||
|
||||
$attachmentsData[] = [
|
||||
'path' => $path,
|
||||
'title' => $title,
|
||||
'ordering' => $ordering,
|
||||
];
|
||||
}
|
||||
|
||||
// Ordina per ordering asc, poi per titolo (opzionale, estetica)
|
||||
usort($attachmentsData, function ($a, $b) {
|
||||
$o = ($a['ordering'] <=> $b['ordering']);
|
||||
if ($o !== 0) return $o;
|
||||
return strcmp((string)$a['title'], (string)$b['title']);
|
||||
});
|
||||
|
||||
// Passa al Model per la sincronizzazione DB
|
||||
$data['attachments'] = $attachmentsData;
|
||||
|
||||
/** @var \Pcrt\Component\Circolari\Site\Model\FormModel $model */
|
||||
$model = $this->getModel('Form');
|
||||
|
||||
// Salva la circolare + sync allegati nel Model (coerente col backend)
|
||||
$id = $model->saveData($data, (int) $user->id);
|
||||
|
||||
$input = Factory::getApplication()->input;
|
||||
// ============================================================
|
||||
// 3) Sincronizza gruppi che possono firmare (come facevi già)
|
||||
// ============================================================
|
||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
||||
$groupIds = $input->get('usergroup_ids', [], 'array');
|
||||
$groupIds = $in->get('usergroup_ids', [], 'array');
|
||||
$groupIds = array_values(array_unique(array_map('intval', (array) $groupIds)));
|
||||
|
||||
try {
|
||||
@ -91,14 +245,14 @@ class FormController extends BaseController
|
||||
} catch (\Throwable $e) {
|
||||
$db->transactionRollback();
|
||||
// opzionale: log/avviso
|
||||
// Factory::getApplication()->enqueueMessage('Errore salvataggio gruppi firma', 'warning');
|
||||
// $app->enqueueMessage('Errore salvataggio gruppi firma', 'warning');
|
||||
}
|
||||
|
||||
$notify = $in->getInt('notify');
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 4) Notifiche email opzionali
|
||||
// ============================================================
|
||||
$notify = (int) $in->get('notify', 0);
|
||||
if ($id > 0 && $notify) {
|
||||
|
||||
// Carica la classe dal backend se non è già caricata
|
||||
if (!class_exists('\Pcrt\Component\Circolari\Administrator\Service\Notifier')) {
|
||||
require_once JPATH_ADMINISTRATOR . '/components/com_circolari/src/Service/Notifier.php';
|
||||
@ -107,16 +261,15 @@ class FormController extends BaseController
|
||||
try {
|
||||
$notifier = new \Pcrt\Component\Circolari\Administrator\Service\Notifier();
|
||||
$sent = $notifier->sendForCircolare((int) $id, ['onlyIfFirmaObbligatoria' => true]);
|
||||
file_put_contents(JPATH_ROOT . '/administrator/logs/circolari_debug.log', date('c') . "Notify: \n" . print_r($sent, true), FILE_APPEND);
|
||||
|
||||
// opzionale: feedback
|
||||
\Joomla\CMS\Factory::getApplication()->enqueueMessage('Email inviate: ' . (int) $sent, 'message');
|
||||
// opzionale: log
|
||||
// file_put_contents(JPATH_ROOT . '/administrator/logs/circolari_debug.log', date('c') . "Notify: \n" . print_r($sent, true), FILE_APPEND);
|
||||
$app->enqueueMessage('Email inviate: ' . (int) $sent, 'message');
|
||||
} catch (\Throwable $e) {
|
||||
\Joomla\CMS\Factory::getApplication()->enqueueMessage('Invio email non riuscito.', 'warning');
|
||||
// opzionale: logga $e->getMessage()
|
||||
file_put_contents(JPATH_ROOT . '/administrator/logs/circolari_debug.log', date('c') . "Errore: " . $e, FILE_APPEND);
|
||||
$app->enqueueMessage('Invio email non riuscito.', 'warning');
|
||||
// file_put_contents(JPATH_ROOT . '/administrator/logs/circolari_debug.log', date('c') . "Errore: " . $e, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setRedirect(
|
||||
Route::_('index.php?option=com_circolari&view=circolare&id=' . (int) $id, false),
|
||||
Text::_('JLIB_APPLICATION_SAVE_SUCCESS')
|
||||
|
||||
@ -59,6 +59,21 @@ class CircolareModel extends ItemModel
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public function getAttachments(int $circolareId): array
|
||||
{
|
||||
if ($circolareId <= 0) {
|
||||
return [];
|
||||
}
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
|
||||
$q = $db->getQuery(true)
|
||||
->select('id, path, title, ordering')
|
||||
->from('#__circolari_attachments')
|
||||
->where('circolare_id = ' . (int) $circolareId)
|
||||
->order('ordering ASC, id ASC');
|
||||
$db->setQuery($q);
|
||||
return $db->loadAssocList() ?: [];
|
||||
}
|
||||
|
||||
public function getBottoniFirma(int $firmatipoId): array
|
||||
{
|
||||
$buttons = []; // <- sempre inizializzato
|
||||
|
||||
@ -10,6 +10,21 @@ use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||
class FormModel extends BaseDatabaseModel
|
||||
{
|
||||
|
||||
public function getAttachments(int $circolareId): array
|
||||
{
|
||||
if ($circolareId <= 0) {
|
||||
return [];
|
||||
}
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
|
||||
$q = $db->getQuery(true)
|
||||
->select('id, path, title, ordering')
|
||||
->from('#__circolari_attachments')
|
||||
->where('circolare_id = ' . (int)$circolareId)
|
||||
->order('ordering ASC, id ASC');
|
||||
$db->setQuery($q);
|
||||
return $db->loadAssocList() ?: [];
|
||||
}
|
||||
|
||||
public function getCategorie(): array
|
||||
{
|
||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
||||
@ -63,7 +78,13 @@ class FormModel extends BaseDatabaseModel
|
||||
$q = $db->getQuery(true)
|
||||
->select('*')->from('#__circolari')->where('id=' . (int) $id);
|
||||
$db->setQuery($q);
|
||||
return $db->loadObject() ?: (object) [];
|
||||
|
||||
$item = $db->loadObject() ?: (object) [];
|
||||
|
||||
if (!empty($item->id)) {
|
||||
$item->attachments = $this->getAttachments((int)$item->id);
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function saveData(array $data, int $userId): int
|
||||
@ -78,6 +99,23 @@ class FormModel extends BaseDatabaseModel
|
||||
$data['alias'] = $alias ?: ('circolare-' . uniqid());
|
||||
}
|
||||
|
||||
// Normalizza FK opzionali
|
||||
$nullableFKs = [
|
||||
'tipologia_firma_id',
|
||||
// aggiungi qui altre FK opzionali se necessario (es. 'categoria_id' se deve poter essere NULL)
|
||||
];
|
||||
foreach ($nullableFKs as $fk) {
|
||||
if (!array_key_exists($fk, $data) || $data[$fk] === '' || (isset($data[$fk]) && (int)$data[$fk] === 0)) {
|
||||
$data[$fk] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// L'immagine viene già risolta dal controller: '' => nessuna, stringa path => salva
|
||||
if (array_key_exists('image', $data)) {
|
||||
$img = trim((string)$data['image']);
|
||||
$data['image'] = ($img !== '') ? $img : null;
|
||||
}
|
||||
|
||||
// JTable amministrativa per coerenza di check/store
|
||||
$factory = $app->bootComponent('com_circolari')->getMVCFactory();
|
||||
/** @var \Pcrt\Component\Circolari\Administrator\Table\CircolareTable $table */
|
||||
@ -102,13 +140,14 @@ class FormModel extends BaseDatabaseModel
|
||||
throw new \RuntimeException($table->getError() ?: 'Check failed');
|
||||
}
|
||||
|
||||
// ordering se nuovo
|
||||
// ordering se nuovo o zero
|
||||
if (empty($table->id) || (int)$table->ordering === 0) {
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
|
||||
$max = (int) $db->setQuery(
|
||||
$db->getQuery(true)->select('COALESCE(MAX(ordering),0)')
|
||||
$db->getQuery(true)
|
||||
->select('COALESCE(MAX(ordering),0)')
|
||||
->from('#__circolari')
|
||||
->where('categoria_id=' . (int) ($table->categoria_id ?? 0))
|
||||
->where('categoria_id ' . (is_null($table->categoria_id) ? 'IS NULL' : '= ' . (int)$table->categoria_id))
|
||||
)->loadResult();
|
||||
$table->ordering = $max + 1;
|
||||
}
|
||||
@ -117,6 +156,102 @@ class FormModel extends BaseDatabaseModel
|
||||
throw new \RuntimeException($table->getError() ?: 'Store failed');
|
||||
}
|
||||
|
||||
return (int) $table->id;
|
||||
$id = (int) $table->id;
|
||||
|
||||
// =========================
|
||||
// Sincronizza ALLEGATI
|
||||
// =========================
|
||||
$attachments = isset($data['attachments']) && is_array($data['attachments']) ? $data['attachments'] : [];
|
||||
|
||||
// Normalizza righe (path obbligatorio; ordering int; title string)
|
||||
$rows = [];
|
||||
foreach ($attachments as $r) {
|
||||
$path = trim((string)($r['path'] ?? ''));
|
||||
if ($path === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'path' => $path,
|
||||
'title' => (string)($r['title'] ?? ''),
|
||||
'ordering' => (int)($r['ordering'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
|
||||
$db->transactionStart();
|
||||
try {
|
||||
// 1) pulisci allegati correnti
|
||||
$db->setQuery(
|
||||
$db->getQuery(true)
|
||||
->delete('#__circolari_attachments')
|
||||
->where('circolare_id = ' . (int)$id)
|
||||
)->execute();
|
||||
|
||||
// 2) inserisci i nuovi (se presenti)
|
||||
if (!empty($rows)) {
|
||||
$q = $db->getQuery(true)
|
||||
->insert('#__circolari_attachments')
|
||||
->columns([
|
||||
$db->quoteName('circolare_id'),
|
||||
$db->quoteName('path'),
|
||||
$db->quoteName('title'),
|
||||
$db->quoteName('ordering'),
|
||||
]);
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$q->values(
|
||||
(int)$id . ',' .
|
||||
$db->quote($r['path']) . ',' .
|
||||
$db->quote($r['title']) . ',' .
|
||||
(int)$r['ordering']
|
||||
);
|
||||
}
|
||||
$db->setQuery($q)->execute();
|
||||
}
|
||||
|
||||
$db->transactionCommit();
|
||||
} catch (\Throwable $e) {
|
||||
$db->transactionRollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function getAllUserGroups(): array
|
||||
{
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
|
||||
// prendo id, title, level per ordinare come albero
|
||||
$q = $db->getQuery(true)
|
||||
->select(['id', 'title', 'lft', 'level'])
|
||||
->from('#__usergroups')
|
||||
->order('lft ASC');
|
||||
$db->setQuery($q);
|
||||
$rows = $db->loadAssocList() ?: [];
|
||||
|
||||
// opzionale: prefisso visivo per i figli
|
||||
foreach ($rows as &$r) {
|
||||
$lvl = max(0, (int) ($r['level'] ?? 0) - 1);
|
||||
$r['title'] = str_repeat('— ', $lvl) . (string) $r['title'];
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function getSelectedUserGroupIds(int $circolareId): array
|
||||
{
|
||||
if ($circolareId <= 0) return [];
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
|
||||
$q = $db->getQuery(true)
|
||||
->select('usergroup_id')
|
||||
->from('#__circolari_usergroups')
|
||||
->where('circolare_id = ' . (int) $circolareId)
|
||||
// se vuoi solo quelli abilitati alla firma:
|
||||
// ->where('can_firmare = 1')
|
||||
->order('usergroup_id ASC');
|
||||
$db->setQuery($q);
|
||||
$ids = $db->loadColumn() ?: [];
|
||||
return array_map('intval', $ids);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Pcrt\Component\Circolari\Site\View\Circolare;
|
||||
|
||||
\defined('_JEXEC') or die;
|
||||
@ -60,6 +61,22 @@ class HtmlView extends BaseHtmlView
|
||||
return parent::display($tpl);
|
||||
}
|
||||
|
||||
|
||||
// Recupera attachments anche se il Model non li carica
|
||||
$db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');
|
||||
$this->attachments = [];
|
||||
if (!empty($this->item->id)) {
|
||||
$q = $db->getQuery(true)
|
||||
->select('id, path, title, ordering')
|
||||
->from('#__circolari_attachments')
|
||||
->where('circolare_id = ' . (int) $this->item->id)
|
||||
->order('ordering ASC, id ASC');
|
||||
$db->setQuery($q);
|
||||
$this->attachments = $db->loadAssocList() ?: [];
|
||||
// comodo: attacca anche all'item
|
||||
$this->item->attachments = $this->attachments;
|
||||
}
|
||||
|
||||
// Rendering normale della singola
|
||||
return parent::display($tpl);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace Pcrt\Component\Circolari\Site\View\Form;
|
||||
|
||||
\defined('_JEXEC') or die;
|
||||
@ -9,22 +8,43 @@ use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public $item;
|
||||
public $categorie = [];
|
||||
public $firmetipi = [];
|
||||
public $bottoniMap = [];
|
||||
protected $item;
|
||||
protected $attachments = [];
|
||||
protected $categorie = [];
|
||||
protected $firmetipi = [];
|
||||
protected $bottoniMap = [];
|
||||
protected $allUserGroups = [];
|
||||
protected $selectedGroupIds = [];
|
||||
|
||||
public function display($tpl = null)
|
||||
{
|
||||
$app = Factory::getApplication();
|
||||
$in = $app->input;
|
||||
|
||||
// Prendi l'ID della circolare in modo "fault tolerant"
|
||||
$circolareId = (int) $in->getInt('id', 0);
|
||||
if ($circolareId <= 0) {
|
||||
$circolareId = (int) $in->getInt('cid', 0); // alias alternativo
|
||||
}
|
||||
if ($circolareId <= 0) {
|
||||
$circolareId = (int) $in->getInt('circolare_id', 0); // alias alternativo
|
||||
}
|
||||
|
||||
/** @var \Pcrt\Component\Circolari\Site\Model\FormModel $model */
|
||||
$model = $this->getModel();
|
||||
|
||||
$id = $app->input->getInt('id', 0);
|
||||
$this->item = $model->getItem($id);
|
||||
// Carica item e lookup
|
||||
$this->item = $model->getItem($circolareId);
|
||||
$this->categorie = $model->getCategorie();
|
||||
$this->firmetipi = $model->getFirmetipi();
|
||||
$this->bottoniMap = $model->getBottoniByFirmatipo();
|
||||
|
||||
// Allegati per l'item in modifica (se hai messo getAttachments nel FormModel)
|
||||
if (!empty($this->item->id) && method_exists($model, 'getAttachments')) {
|
||||
$this->attachments = $model->getAttachments((int) $this->item->id);
|
||||
$this->item->attachments = $this->attachments;
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
||||
|
||||
// tutti i gruppi (ordinati per albero)
|
||||
@ -47,6 +67,7 @@ class HtmlView extends BaseHtmlView
|
||||
)->loadColumn();
|
||||
}
|
||||
$this->selectedGroupIds = array_map('intval', $selected);
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,7 +144,11 @@ if ($canAdmin) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($item->image): ?>
|
||||
<div class="mb-4">
|
||||
<img src="/<?php echo $item->image ?>" alt="" class="img-fluid h-50 w-100 shadow-sm" style="border-radius: 15px;">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($item->firma_obbligatoria && $this->getModel()->userCanFirmare($item->id, $this->getModel()->currentUser->id) && !empty($buttons)) : ?>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex flex-wrap gap-2" data-sign-group="circolare-<?php echo (int)$item->id; ?>">
|
||||
@ -171,8 +175,7 @@ if ($canAdmin) {
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<form method="post"
|
||||
action="<?=
|
||||
Route::_('index.php?option=com_circolari&task=circolare.sign&id=' . (int)$item->id); ?>"
|
||||
action="<?= Route::_('index.php?option=com_circolari&task=circolare.sign&id=' . (int)$item->id); ?>"
|
||||
class="d-inline-block">
|
||||
<input type="hidden" name="bottone_id" value="<?php echo $btnId; ?>">
|
||||
<button type="submit"
|
||||
@ -190,18 +193,105 @@ if ($canAdmin) {
|
||||
<div class="article-body mt-3">
|
||||
<?php echo $item->description; ?>
|
||||
</div>
|
||||
<h6>Allegati</h6>
|
||||
<?php
|
||||
$href = htmlspecialchars((string)($item->attachment ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
$label = htmlspecialchars((string)($item->allegato_titolo ?: 'Allegato'), ENT_QUOTES, 'UTF-8');
|
||||
$title = 'Scarica ' . $label;
|
||||
// Normalizza lista allegati: prima prova array $item->attachments (nuova logica),
|
||||
// altrimenti fallback ai vecchi campi singoli $item->attachment / $item->allegato_titolo.
|
||||
$attachments = [];
|
||||
|
||||
// 1) Nuova logica: array di allegati dal Model (path, title, ordering)
|
||||
if (!empty($item->attachments) && is_array($item->attachments)) {
|
||||
foreach ($item->attachments as $att) {
|
||||
$path = (string) ($att['path'] ?? ($att->path ?? ''));
|
||||
if ($path === '') continue;
|
||||
$title = (string) ($att['title'] ?? ($att->title ?? ''));
|
||||
$ord = (int) ($att['ordering'] ?? ($att->ordering ?? 0));
|
||||
$attachments[] = ['href' => $path, 'label' => $title, 'ordering' => $ord];
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback legacy: singolo allegato
|
||||
if (empty($attachments) && !empty($item->attachment)) {
|
||||
$attachments[] = [
|
||||
'href' => (string) $item->attachment,
|
||||
'label' => (string) ($item->allegato_titolo ?: ''),
|
||||
'ordering' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// Ordina per ordering, poi per label
|
||||
usort($attachments, function ($a, $b) {
|
||||
$o = ($a['ordering'] <=> $b['ordering']);
|
||||
if ($o !== 0) return $o;
|
||||
return strcmp((string)$a['label'], (string)$b['label']);
|
||||
});
|
||||
|
||||
// Mappa icone in base all'estensione (puoi adattare gli id alle tue sprite)
|
||||
$spritePath = '/media/templates/site/tpl_istituzionale/images/sprites.svg';
|
||||
function _att_icon_id(string $ext): string
|
||||
{
|
||||
$ext = strtolower($ext);
|
||||
if (in_array($ext, ['pdf'], true)) return 'it-file-pdf';
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'tif', 'tiff', 'heic', 'heif'], true)) return 'it-file-image';
|
||||
if (in_array($ext, ['doc', 'docx', 'rtf', 'odt'], true)) return 'it-file-doc';
|
||||
if (in_array($ext, ['xls', 'xlsx', 'ods', 'csv'], true)) return 'it-file-xls';
|
||||
if (in_array($ext, ['ppt', 'pptx', 'odp'], true)) return 'it-file-ppt';
|
||||
if (in_array($ext, ['zip', 'rar', '7z', 'gz', 'bz2', 'tar'], true)) return 'it-file-zip';
|
||||
return 'it-file'; // generico
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if (!empty($attachments)): ?>
|
||||
<h6 class="mt-4"><?php echo JText::_('Allegati'); ?></h6>
|
||||
<div class="row g-3 mb-2">
|
||||
<?php foreach ($attachments as $att):
|
||||
$hrefRaw = (string) ($att['href'] ?? '');
|
||||
$labelRaw = (string) ($att['label'] ?? '');
|
||||
// Fallback label = basename del file se non presente
|
||||
$basename = basename(htmlspecialchars_decode($hrefRaw));
|
||||
$label = trim($labelRaw) !== '' ? $labelRaw : $basename;
|
||||
|
||||
// Estensione per icona e testo accessibile
|
||||
$ext = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
|
||||
if ($ext === 'jpeg') {
|
||||
$ext = 'jpg';
|
||||
} // coerenza
|
||||
$iconId = _att_icon_id($ext);
|
||||
|
||||
$href = htmlspecialchars($hrefRaw, ENT_QUOTES, 'UTF-8');
|
||||
$labelEsc = htmlspecialchars($label, ENT_QUOTES, 'UTF-8');
|
||||
$titleAttr = 'Scarica ' . $label;
|
||||
$titleEsc = htmlspecialchars($titleAttr, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Testo accessibile sul tipo file (es. "file PDF", "file immagine", ecc.)
|
||||
$a11yType = '';
|
||||
switch ($iconId) {
|
||||
case 'it-file-pdf':
|
||||
$a11yType = ' (file PDF)';
|
||||
break;
|
||||
case 'it-file-image':
|
||||
$a11yType = ' (file immagine)';
|
||||
break;
|
||||
case 'it-file-doc':
|
||||
$a11yType = ' (documento)';
|
||||
break;
|
||||
case 'it-file-xls':
|
||||
$a11yType = ' (foglio di calcolo)';
|
||||
break;
|
||||
case 'it-file-ppt':
|
||||
$a11yType = ' (presentazione)';
|
||||
break;
|
||||
case 'it-file-zip':
|
||||
$a11yType = ' (archivio compresso)';
|
||||
break;
|
||||
default:
|
||||
$a11yType = $ext ? ' (file .' . htmlspecialchars($ext, ENT_QUOTES, 'UTF-8') . ')' : '';
|
||||
}
|
||||
?>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card card-documento card-bg card-icon rounded shadow-sm h-100">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<svg class="icon" aria-hidden="true" focusable="false" style="fill:#d1344c;">
|
||||
<use xlink:href="/media/templates/site/tpl_istituzionale/images/sprites.svg#it-file-pdf"></use>
|
||||
<use xlink:href="<?php echo $spritePath . '#' . $iconId; ?>"></use>
|
||||
</svg>
|
||||
<div class="card-icon-content">
|
||||
<p class="mb-0">
|
||||
@ -209,18 +299,20 @@ $title = 'Scarica ' . $label;
|
||||
class="text-decoration-none"
|
||||
href="<?php echo $href; ?>"
|
||||
download
|
||||
title="<?php echo htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-focus-mouse="false"
|
||||
>
|
||||
<strong><?php echo $label; ?></strong>
|
||||
<span class="visually-hidden"> (file PDF)</span>
|
||||
title="<?php echo $titleEsc; ?>"
|
||||
data-focus-mouse="false">
|
||||
<strong><?php echo $labelEsc; ?></strong>
|
||||
<span class="visually-hidden"><?php echo $a11yType; ?></span>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@ -231,11 +323,9 @@ $title = 'Scarica ' . $label;
|
||||
flex-shrink: 0;
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<?php if ($canAdmin): ?>
|
||||
<?php if ($canAdmin & $item->firma_obbligatoria): ?>
|
||||
<div class="card mt-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>Firme ricevute</strong>
|
||||
|
||||
@ -7,6 +7,8 @@ use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Component\ComponentHelper;
|
||||
use Joomla\CMS\Language\Text;
|
||||
use Joomla\CMS\Uri\Uri;
|
||||
use Joomla\CMS\Editor\Editor;
|
||||
use Joomla\CMS\Plugin\PluginHelper;
|
||||
|
||||
HTMLHelper::_('bootstrap.tooltip');
|
||||
HTMLHelper::_('behavior.formvalidator');
|
||||
@ -28,23 +30,68 @@ foreach ((array) $menu->getItems('component_id', (int) $component->id) as $mi) {
|
||||
$cancelUrl = $cancelItemId
|
||||
? Route::_('index.php?Itemid=' . $cancelItemId)
|
||||
: Uri::root() . 'index.php?option=com_circolari&view=circolari';
|
||||
|
||||
// Editor preferito (CKEditor se attivo, altrimenti editor di default)
|
||||
$preferred = 'ckeditor';
|
||||
$editorName = PluginHelper::isEnabled('editors', $preferred)
|
||||
? $preferred
|
||||
: Factory::getApplication()->get('editor');
|
||||
$editor = Editor::getInstance($editorName);
|
||||
|
||||
// Helper piccole funzioni
|
||||
function e($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
?>
|
||||
<div class="container py-3">
|
||||
<h2 class="mb-3"><?php echo Text::_('Nuova circolare'); ?></h2>
|
||||
<h2 class="mb-3"><?php echo $item->id ?? '' ? Text::_('Modifica circolare') : Text::_('Nuova circolare'); ?></h2>
|
||||
|
||||
<form action="<?php echo Route::_('index.php?option=com_circolari&task=form.save'); ?>" method="post" class="form-validate">
|
||||
<form action="<?php echo $action; ?>" method="post" name="adminForm" id="adminForm"
|
||||
class="form-validate" enctype="multipart/form-data">
|
||||
|
||||
<ul class="nav nav-tabs" id="circolareTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-contenuto" data-bs-toggle="tab" data-bs-target="#pane-contenuto" type="button" role="tab" aria-controls="pane-contenuto" aria-selected="true">
|
||||
<?php echo Text::_('Contenuto'); ?>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-media" data-bs-toggle="tab" data-bs-target="#pane-media" type="button" role="tab" aria-controls="pane-media" aria-selected="false">
|
||||
<?php echo Text::_('Immagini/Allegati'); ?>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-firma" data-bs-toggle="tab" data-bs-target="#pane-firma" type="button" role="tab" aria-controls="pane-firma" aria-selected="false">
|
||||
<?php echo Text::_('Firma'); ?>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content border border-top-0 rounded-bottom p-3 shadow-sm" id="circolareTabsContent">
|
||||
|
||||
<div class="tab-pane fade show active" id="pane-contenuto" role="tabpanel" aria-labelledby="tab-contenuto" tabindex="0">
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="title"><?php echo Text::_('Titolo'); ?></label>
|
||||
<input type="text" id="title" name="title" required
|
||||
class="form-control"
|
||||
value="<?php echo htmlspecialchars($item->title ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="text" id="title" name="title" required class="form-control"
|
||||
value="<?php echo e($item->title ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="description"><?php echo Text::_('Testo'); ?></label>
|
||||
<?php
|
||||
$desc = !empty($item->description) ? $item->description : '';
|
||||
echo $editor->display('description', $desc, '100%', '320', '60', '20', true);
|
||||
?>
|
||||
<div class="form-text"><?php echo Text::_('Inserisci il contenuto della circolare.'); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="alias"><?php echo Text::_('Alias (opzionale)'); ?></label>
|
||||
<input type="text" id="alias" name="alias" class="form-control"
|
||||
value="<?php echo htmlspecialchars($item->alias ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<input type="text" id="alias" name="alias" class="form-control" value="<?php echo e($item->alias ?? ''); ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -55,25 +102,17 @@ $cancelUrl = $cancelItemId
|
||||
$cid = (int) ($c['id'] ?? 0);
|
||||
$ctitle = (string) ($c['title'] ?? '');
|
||||
$cstate = (int) ($c['state'] ?? 1);
|
||||
if ($cstate !== 1) continue; // solo pubblicate
|
||||
if ($cstate !== 1) continue;
|
||||
?>
|
||||
<option value="<?php echo $cid; ?>"
|
||||
<?php echo (!empty($item->categoria_id) && (int)$item->categoria_id === $cid) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($ctitle, ENT_QUOTES, 'UTF-8'); ?>
|
||||
<?php echo e($ctitle); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="description"><?php echo Text::_('Testo'); ?></label>
|
||||
<textarea id="description" name="description" class="form-control" rows="8"><?php
|
||||
echo htmlspecialchars($item->description ?? '', ENT_QUOTES, 'UTF-8');
|
||||
?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-0">
|
||||
<label class="form-label" for="state"><?php echo Text::_('Stato'); ?></label>
|
||||
<select id="state" name="state" class="form-select">
|
||||
<option value="1" <?php echo ((int)($item->state ?? 1) === 1 ? 'selected' : ''); ?>>
|
||||
@ -84,7 +123,143 @@ $cancelUrl = $cancelItemId
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pane-media" role="tabpanel" aria-labelledby="tab-media" tabindex="0">
|
||||
<?php $existingImage = (string)($item->image ?? ''); ?>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="image_file"><?php echo Text::_('Immagine'); ?></label>
|
||||
|
||||
<?php if ($existingImage) : ?>
|
||||
<div class="d-flex align-items-center gap-3 mb-2">
|
||||
<img src="<?php echo e($existingImage); ?>" alt="Anteprima immagine"
|
||||
style="max-height:80px; width:auto; border:1px solid #eee; padding:2px;">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="image_remove" name="image_remove" value="1">
|
||||
<label class="form-check-label" for="image_remove"><?php echo Text::_('Rimuovi immagine'); ?></label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="image_existing" value="<?php echo e($existingImage); ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="file" id="image_file" name="image_file" class="form-control"
|
||||
accept="image/*,.jpg,.jpeg,.JPG,.JPEG,.png,.gif,.webp,.svg,.heic,.HEIC,.heif,.bmp,.tif,.tiff">
|
||||
<div class="form-text"><?php echo Text::_('Carica una nuova immagine (opzionale).'); ?></div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$existingAttachments = [];
|
||||
if (!empty($this->attachments) && is_array($this->attachments)) {
|
||||
$existingAttachments = $this->attachments;
|
||||
} elseif (!empty($item->attachments) && is_array($item->attachments)) {
|
||||
$existingAttachments = $item->attachments;
|
||||
}
|
||||
?>
|
||||
<div class="mb-2">
|
||||
<label class="form-label d-block"><?php echo Text::_('Allegati'); ?></label>
|
||||
|
||||
<div id="attachments-list" class="row g-3">
|
||||
<?php $idx = 0;
|
||||
foreach ($existingAttachments as $att) :
|
||||
$path = (string)(is_array($att) ? ($att['path'] ?? '') : ($att->path ?? ''));
|
||||
$title = (string)(is_array($att) ? ($att['title'] ?? '') : ($att->title ?? ''));
|
||||
$ordering = (int)(is_array($att) ? ($att['ordering'] ?? 0) : ($att->ordering ?? 0));
|
||||
if ($path === '') continue;
|
||||
$pathEsc = e($path);
|
||||
$titleEsc = e($title);
|
||||
?>
|
||||
<div class="col-12" data-att-row>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
<div class="mb-2">
|
||||
<strong class="d-block mb-1"><?php echo Text::_('File esistente'); ?>:</strong>
|
||||
<a href="<?php echo $pathEsc; ?>" target="_blank" rel="noopener">
|
||||
<?php echo e(basename(htmlspecialchars_decode($path))); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label" for="att_<?php echo $idx; ?>_title"><?php echo Text::_('Titolo'); ?></label>
|
||||
<input type="text" class="form-control"
|
||||
id="att_<?php echo $idx; ?>_title"
|
||||
name="attachments[<?php echo $idx; ?>][title]"
|
||||
value="<?php echo $titleEsc; ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label" for="att_<?php echo $idx; ?>_ordering"><?php echo Text::_('Ordine'); ?></label>
|
||||
<input type="number" class="form-control"
|
||||
id="att_<?php echo $idx; ?>_ordering"
|
||||
name="attachments[<?php echo $idx; ?>][ordering]"
|
||||
value="<?php echo (int)$ordering; ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="att_<?php echo $idx; ?>_file"><?php echo Text::_('Sostituisci file'); ?></label>
|
||||
<input type="file" class="form-control"
|
||||
id="att_<?php echo $idx; ?>_file"
|
||||
name="attachments[<?php echo $idx; ?>][file]"
|
||||
accept="*/*,image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.jpg,.jpeg,.JPG,.JPEG,.png,.gif,.webp,.svg,.heic,.HEIC,.heif,.bmp,.tif,.tiff">
|
||||
<div class="form-text"><?php echo Text::_('Lascia vuoto per mantenere il file esistente.'); ?></div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="att_<?php echo $idx; ?>_remove"
|
||||
name="attachments[<?php echo $idx; ?>][remove]"
|
||||
value="1">
|
||||
<label class="form-check-label" for="att_<?php echo $idx; ?>_remove"><?php echo Text::_('Rimuovi'); ?></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="attachments[<?php echo $idx; ?>][path]" value="<?php echo $pathEsc; ?>">
|
||||
</div>
|
||||
</div>
|
||||
<?php $idx++; endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<button type="button" class="btn btn-outline-primary" id="add-attachment-btn">
|
||||
+ <?php echo Text::_('Aggiungi allegato'); ?>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger d-none" id="remove-last-attachment-btn">
|
||||
<?php echo Text::_('Rimuovi ultima riga'); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template id="attachment-row-template">
|
||||
<div class="col-12" data-att-row>
|
||||
<div class="border rounded p-3">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label"><?php echo Text::_('Titolo'); ?></label>
|
||||
<input type="text" class="form-control" name="__NAME__[title]" value="">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label"><?php echo Text::_('Ordine'); ?></label>
|
||||
<input type="number" class="form-control" name="__NAME__[ordering]" value="0">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label"><?php echo Text::_('File'); ?></label>
|
||||
<input type="file" class="form-control" name="__NAME__[file]"
|
||||
accept="*/*,image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.jpg,.jpeg,.JPG,.JPEG,.png,.gif,.webp,.svg,.heic,.HEIC,.heif,.bmp,.tif,.tiff" required>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger mt-4" data-remove-row>
|
||||
<?php echo Text::_('Rimuovi'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pane-firma" role="tabpanel" aria-labelledby="tab-firma" tabindex="0">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="jform_firma_obbligatoria"><?php echo Text::_('Firma obbligatoria'); ?></label>
|
||||
<?php $fo = (int) ($item->firma_obbligatoria ?? 0); ?>
|
||||
@ -104,65 +279,50 @@ $cancelUrl = $cancelItemId
|
||||
$tid = (int) $t['id'];
|
||||
$nm = (string) $t['nome'];
|
||||
echo '<option value="' . $tid . '" ' . ($selTipo === $tid ? 'selected' : '') . '>'
|
||||
. htmlspecialchars($nm, ENT_QUOTES, 'UTF-8') . '</option>';
|
||||
. e($nm) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<div class="form-text"><?php echo Text::_('Anteprima bottoni:'); ?></div>
|
||||
<div id="anteprima-bottoni" class="mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2 firma-block" style="display:none;">
|
||||
<div class="col-md-6 firma-block" style="display:none;">
|
||||
<label class="form-label" for="jform_usergroup_ids">
|
||||
<?php echo Text::_('Gruppi che possono firmare'); ?>
|
||||
</label>
|
||||
|
||||
<select id="jform_usergroup_ids"
|
||||
name="usergroup_ids[]"
|
||||
class="form-select"
|
||||
multiple
|
||||
size="8">
|
||||
<?php foreach ((array) ($this->allUserGroups ?? []) as $g):
|
||||
$gid = (int) ($g['id'] ?? 0);
|
||||
$gtitle = (string) ($g['title'] ?? '');
|
||||
$sel = in_array($gid, (array) ($this->selectedGroupIds ?? []), true) ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?php echo $gid; ?>" <?php echo $sel; ?>>
|
||||
<?php echo htmlspecialchars($gtitle, ENT_QUOTES, 'UTF-8'); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
<div class="form-text pc-muted">
|
||||
<?php echo Text::_('Seleziona i gruppi utenti abilitati alla firma di questa circolare.'); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4 firma-block">
|
||||
<label class="form-label required " id="jform_scadenza-lbl" for="jform_scadenza">
|
||||
<?php echo Text::_('Scadenza firma'); ?><span class="star" aria-hidden="true"> *</span>
|
||||
</label>
|
||||
<input type="datetime-local" name="scadenza" id="jform_scadenza" class="form-control"
|
||||
value="<?php
|
||||
$sc = (string) ($item->scadenza ?? '');
|
||||
if ($sc !== '') {
|
||||
echo htmlspecialchars(str_replace(' ', 'T', substr($sc, 0, 16)), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
if ($sc !== '') echo e(str_replace(' ', 'T', substr($sc, 0, 16)));
|
||||
?>">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6 firma-block" style="display:none;">
|
||||
<label class="form-label" for="jform_usergroup_ids"><?php echo Text::_('Gruppi che possono firmare'); ?></label>
|
||||
<select id="jform_usergroup_ids" name="usergroup_ids[]" class="form-select" multiple size="8">
|
||||
<?php foreach ((array) ($this->allUserGroups ?? []) as $g):
|
||||
$gid = (int) ($g['id'] ?? 0);
|
||||
$gtitle = (string) ($g['title'] ?? '');
|
||||
$sel = in_array($gid, (array) ($this->selectedGroupIds ?? []), true) ? 'selected' : '';
|
||||
?>
|
||||
<option value="<?php echo $gid; ?>" <?php echo $sel; ?>><?php echo e($gtitle); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text"><?php echo Text::_('Seleziona i gruppi utenti abilitati alla firma.'); ?></div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mt-4 firma-block" style="display:none;">
|
||||
<label class="form-label" for="jform_notify">Invia email agli aventi diritto</label>
|
||||
<div class="col-md-6 firma-block">
|
||||
<label class="form-label" for="jform_notify"><?php echo Text::_('Invia email agli aventi diritto'); ?></label>
|
||||
<input type="hidden" name="notify" value="0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="jform_notify" name="notify" value="1">
|
||||
<label class="form-check-label" for="jform_notify">Sì, invia subito</label>
|
||||
<label class="form-check-label" for="jform_notify"><?php echo Text::_('Sì, invia subito'); ?></label>
|
||||
</div>
|
||||
<div class="form-text"><?php echo Text::_('Invia una notifica agli utenti dei gruppi selezionati.'); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">Invia una notifica agli utenti dei gruppi abilitati alla firma.</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
@ -171,19 +331,53 @@ $cancelUrl = $cancelItemId
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="id" value="<?php echo (int) ($this->item->id ?? 0); ?>">
|
||||
<?php echo \Joomla\CMS\HTML\HTMLHelper::_('form.token'); ?>
|
||||
<?php echo HTMLHelper::_('form.token'); ?>
|
||||
|
||||
<script type="application/json" id="bottoni-map">
|
||||
<?php
|
||||
echo json_encode($this->bottoniMap ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
?>
|
||||
<?php echo json_encode($this->bottoniMap ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
function byId(id) {
|
||||
return document.getElementById(id);
|
||||
// Allegati repeater
|
||||
var addBtn = document.getElementById('add-attachment-btn');
|
||||
var removeLastBtn = document.getElementById('remove-last-attachment-btn');
|
||||
var list = document.getElementById('attachments-list');
|
||||
var tpl = document.getElementById('attachment-row-template')?.content;
|
||||
var nextIndex = <?php echo (int)($idx ?? 0); ?>;
|
||||
|
||||
function toggleRemoveLast() {
|
||||
var rows = list?.querySelectorAll('[data-att-row].is-new') || [];
|
||||
if (removeLastBtn) removeLastBtn.classList.toggle('d-none', rows.length === 0);
|
||||
}
|
||||
function addRow() {
|
||||
if (!tpl || !list) return;
|
||||
var clone = document.importNode(tpl, true);
|
||||
clone.querySelectorAll('input[name*="__NAME__"]').forEach(function(el) {
|
||||
el.name = el.name.replace('__NAME__', 'attachments[' + nextIndex + ']');
|
||||
});
|
||||
var row = clone.querySelector('[data-att-row]');
|
||||
row.classList.add('is-new');
|
||||
list.appendChild(clone);
|
||||
nextIndex++;
|
||||
toggleRemoveLast();
|
||||
}
|
||||
function removeRow(btn) {
|
||||
var row = btn.closest('[data-att-row]');
|
||||
if (row) { row.remove(); toggleRemoveLast(); }
|
||||
}
|
||||
addBtn && addBtn.addEventListener('click', addRow);
|
||||
removeLastBtn && removeLastBtn.addEventListener('click', function() {
|
||||
var rows = list.querySelectorAll('[data-att-row].is-new');
|
||||
if (rows.length) { rows[rows.length - 1].remove(); toggleRemoveLast(); }
|
||||
});
|
||||
list && list.addEventListener('click', function(e) {
|
||||
if (e.target && e.target.hasAttribute('data-remove-row')) removeRow(e.target);
|
||||
});
|
||||
toggleRemoveLast();
|
||||
|
||||
// Firma: show/hide blocchi + anteprima bottoni
|
||||
function byId(id){ return document.getElementById(id); }
|
||||
const obb = byId('jform_firma_obbligatoria');
|
||||
const tipo = byId('jform_tipologia_firma_id');
|
||||
const scad = byId('jform_scadenza');
|
||||
@ -207,14 +401,11 @@ $cancelUrl = $cancelItemId
|
||||
|
||||
function toggleFirma() {
|
||||
const need = obb && obb.value === '1';
|
||||
blocks.forEach(function(el) {
|
||||
el.style.display = need ? '' : 'none';
|
||||
});
|
||||
blocks.forEach(function(el) { el.style.display = need ? '' : 'none'; });
|
||||
if (scad) {
|
||||
if (need) {
|
||||
scad.setAttribute('required', 'required');
|
||||
scad.setAttribute('aria-required', 'true');
|
||||
// aggiungi asterisco al label se manca
|
||||
var lbl = document.getElementById('jform_scadenza-lbl');
|
||||
if (lbl) {
|
||||
lbl.classList.add('required');
|
||||
@ -232,8 +423,7 @@ $cancelUrl = $cancelItemId
|
||||
var lbl = document.getElementById('jform_scadenza-lbl');
|
||||
if (lbl) {
|
||||
lbl.classList.remove('required');
|
||||
var st = lbl.querySelector('.star');
|
||||
if (st) st.remove();
|
||||
var st = lbl.querySelector('.star'); if (st) st.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,7 +434,6 @@ $cancelUrl = $cancelItemId
|
||||
if (e.target === obb) toggleFirma();
|
||||
if (e.target === tipo) renderPreview();
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
toggleFirma();
|
||||
renderPreview();
|
||||
|
||||
Reference in New Issue
Block a user