Introduzione modulo Aggiornamenti con nuove strutture

This commit is contained in:
Dasc3er 2021-03-13 14:13:19 +01:00
parent c3f4abb578
commit 97eaa07057
57 changed files with 1886 additions and 567 deletions

View File

@ -11,8 +11,9 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class LegacyController extends Controller class LegacyController extends Controller
{ {
public function index(Request $request, $path = 'index.php') public function index(Request $request)
{ {
$path = substr($request->getPathInfo(), 1);
$base_path = base_path('legacy'); $base_path = base_path('legacy');
// Fix per redirect all'API // Fix per redirect all'API

View File

@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Util\FileSystem;
class RequirementsController extends Controller
{
protected static $requirements;
public function index(Request $request)
{
$args = [
'requirements' => self::getRequirementsList()
];
return view('config.requirements', $args);
}
public static function getRequirementsList($file = null)
{
$requirements = self::getRequirements($file);
$list = [
tr('Apache') => $requirements['apache'],
tr('PHP (_VERSION_)', [
'_VERSION_' => phpversion(),
]) => $requirements['php'],
tr('Percorsi di servizio') => $requirements['paths'],
];
return $list;
}
public static function getRequirements($file = null)
{
if (empty($file) && isset(self::$requirements)) {
return self::$requirements;
}
$list = config('requirements');
if (!empty($file)) {
$file = realpath($file);
if (string_starts_with($file)) {
$list = include $file;
}
}
// Apache
if (function_exists('apache_get_modules')) {
$available_modules = apache_get_modules();
}
$apache = $list['apache'];
foreach ($apache as $name => $values) {
$status = isset($available_modules) ? in_array($name, $available_modules) : false;
$status = isset($values['server']) && isset($_SERVER[$values['server']]) ? $_SERVER[$values['server']] == 'On' : $status;
$apache[$name]['description'] = tr('Il modulo Apache _MODULE_ deve essere abilitato', [
'_MODULE_' => '<i>'.$name.'</i>',
]);
$apache[$name]['status'] = $status;
}
// PHP
$php = $list['php'];
foreach ($php as $name => $values) {
if ($values['type'] == 'ext') {
$description = !empty($values['required']) ? tr("L'estensione PHP _EXT_ deve essere abilitata", [
'_EXT_' => '<i>'.$name.'</i>',
]) : tr("E' consigliata l'abilitazione dell'estensione PHP _EXT_", [
'_EXT_' => '<i>'.$name.'</i>',
]);
$status = extension_loaded($name);
} else {
$suggested = str_replace(['>', '<'], '', $values['suggested']);
$value = ini_get($name);
$description = tr("Valore consigliato per l'impostazione PHP: _VALUE_ (Valore attuale: _INI_)", [
'_VALUE_' => $suggested,
'_INI_' => ini_get($name),
]);
$suggested = strpos($suggested, 'B') !== false ? $suggested : $suggested.'B';
$value = strpos($value, 'B') !== false ? $value : $value.'B';
$ini = FileSystem::convertBytes($value);
$real = FileSystem::convertBytes($suggested);
if (starts_with($values['suggested'], '>')) {
$status = $ini >= substr($real, 1);
} elseif (starts_with($values['suggested'], '<')) {
$status = $ini <= substr($real, 1);
} else {
$status = ($real == $ini);
}
$php[$name]['value'] = $value;
if (is_bool($suggested)) {
$suggested = !empty($suggested) ? 'On' : 'Off';
}
}
$php[$name]['description'] = $description;
$php[$name]['status'] = $status;
}
// Percorsi di servizio
$paths = [];
foreach ($list['directories'] as $name) {
$status = is_writable(base_path().DIRECTORY_SEPARATOR.$name);
$description = tr('Il percorso _PATH_ deve risultare accessibile da parte del gestionale (permessi di lettura e scrittura)', [
'_PATH_' => '<i>'.$name.'</i>',
]);
$paths[$name]['description'] = $description;
$paths[$name]['status'] = $status;
}
$result = [
'apache' => $apache,
'php' => $php,
'paths' => $paths,
];
if (empty($file)) {
self::$requirements = $result;
}
return $result;
}
public static function isSatisfied()
{
$general_status = true;
$requirements = self::getRequirements();
foreach ($requirements as $key => $values) {
foreach ($values as $value) {
$general_status &= !empty($value['required']) ? $value['status'] : true;
}
}
return $general_status;
}
}

View File

@ -4,6 +4,7 @@ namespace App\Http\Middleware;
use App\Http\Controllers\ConfigurationController; use App\Http\Controllers\ConfigurationController;
use App\Http\Controllers\InitializationController; use App\Http\Controllers\InitializationController;
use App\Http\Controllers\RequirementsController;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -21,6 +22,12 @@ class EnsureConfiguration
return $next($request); return $next($request);
} }
// Test sui requisiti del gestionale
$result = $this->checkRequirements($route);
if ($result !== null) {
return $result;
}
// Test della connessione al database // Test della connessione al database
$result = $this->checkConfiguration($route); $result = $this->checkConfiguration($route);
if ($result !== null) { if ($result !== null) {
@ -42,9 +49,28 @@ class EnsureConfiguration
return $next($request); return $next($request);
} }
protected function checkRequirements($route)
{
$configuration_paths = ['requirements'];
$requirements_satisfied = RequirementsController::isSatisfied();
if ($requirements_satisfied) {
// Redirect nel caso in cui i requisiti siano soddisfatti
if (in_array($route->getName(), $configuration_paths)) {
return redirect(route('configuration'));
}
} else {
// Redirect per requisiti incompleti
if (!in_array($route->getName(), $configuration_paths)) {
return redirect(route('requirements'));
}
}
return null;
}
protected function checkConfiguration($route) protected function checkConfiguration($route)
{ {
// Test della connessione al database
$configuration_paths = ['configuration', 'configuration-save', 'configuration-test']; $configuration_paths = ['configuration', 'configuration-save', 'configuration-test'];
$configuration_completed = ConfigurationController::isConfigured(); $configuration_completed = ConfigurationController::isConfigured();

View File

@ -11,8 +11,6 @@ class Language
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)

View File

@ -13,7 +13,6 @@ class Select extends Component
*/ */
public function __construct() public function __construct()
{ {
//
} }
/** /**

View File

@ -2,8 +2,6 @@
/** /**
* Gets the current locale. * Gets the current locale.
*
* @return string
*/ */
function locale(): string function locale(): string
{ {
@ -12,13 +10,11 @@ function locale(): string
/** /**
* Get the language portion of the locale. * Get the language portion of the locale.
* (ex. en_GB returns en) * (ex. en_GB returns en).
*
* @return string
*/ */
function localeLanguage(): string function localeLanguage(): string
{ {
$locale = locale(); $locale = locale();
return substr($locale, 0, strpos($locale, "_")); return substr($locale, 0, strpos($locale, '_'));
} }

View File

@ -16,6 +16,7 @@
"type": "project", "type": "project",
"require": { "require": {
"php": "^7.3|^8.0", "php": "^7.3|^8.0",
"ext-apache": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-fileinfo": "*", "ext-fileinfo": "*",
@ -117,6 +118,7 @@
"Plugins\\DettagliArticolo\\": ["legacy/plugins/dettagli_articolo/custom/src/", "legacy/plugins/dettagli_articolo/src/"], "Plugins\\DettagliArticolo\\": ["legacy/plugins/dettagli_articolo/custom/src/", "legacy/plugins/dettagli_articolo/src/"],
"App\\": "app/", "App\\": "app/",
"Modules\\": "modules/",
"Database\\Factories\\": "database/factories/", "Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/" "Database\\Seeders\\": "database/seeders/"
}, },

61
config/requirements.php Normal file
View File

@ -0,0 +1,61 @@
<?php
return [
'php' => [
'zip' => [
'type' => 'ext',
'required' => 1,
],
'mbstring' => [
'type' => 'ext',
'required' => 1,
],
'pdo_mysql' => [
'type' => 'ext',
'required' => 1,
],
'dom' => [
'type' => 'ext',
'required' => 1,
],
'xsl' => [
'type' => 'ext',
'required' => 1,
],
'openssl' => [
'type' => 'ext',
'required' => 1,
],
'intl' => [
'type' => 'ext',
'required' => 1,
],
'curl' => [
'type' => 'ext',
'required' => 1,
],
'soap' => [
'type' => 'ext',
'required' => 1,
],
'upload_max_filesize' => [
'type' => 'value',
'suggested' => '>32M',
],
'post_max_size' => [
'type' => 'value',
'suggested' => '>32M',
],
],
'apache' => [
'mod_rewrite' => [
'server' => 'HTTP_MOD_REWRITE',
],
],
'directories' => [
'backup',
'files',
'logs',
],
];

2
legacy

@ -1 +1 @@
Subproject commit 01a293d3650fcf96d8617fab2b14a0c193bca6d9 Subproject commit 5730434cc63c67880049f6adb116791dc8af8a5b

View File

@ -0,0 +1,5 @@
<?php
return [
'name' => 'Aggiornamenti'
];

View File

@ -0,0 +1,21 @@
<?php
namespace Modules\Aggiornamenti\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class AggiornamentiDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Model::unguard();
// $this->call("OthersTableSeeder");
}
}

View File

View File

@ -0,0 +1,466 @@
<?php
namespace Modules\Aggiornamenti\Http;
use Models\Cache;
use Models\Group;
use App\Http\Controllers\RequirementsController as Requirements;
use GuzzleHttp\Client;
use InvalidArgumentException;
use Models\Module;
use Parsedown;
use Symfony\Component\Finder\Finder;
use Update;
use Util\Ini;
use Util\Zip;
class Aggiornamento
{
protected static $client = null;
protected $directory = null;
protected $components = null;
protected $groups = null;
/**
* Crea l'istanza dedicata all'aggiornamento presente nella cartella indicata.
*
* @param string $directory
*
* @throws InvalidArgumentException
*/
public function __construct($directory = null)
{
$this->directory = $directory ?: Zip::getExtractionDirectory();
if (!$this->isCoreUpdate() && empty($this->componentUpdates())) {
throw new InvalidArgumentException();
}
}
/**
* Pulisce la cartella di estrazione.
*/
public function delete()
{
delete($this->directory);
}
/**
* Restituisce il percorso impostato per l'aggiornamento corrente.
*
* @return string
*/
public function getDirectory()
{
return $this->directory;
}
/**
* Controlla se l'aggiornamento è di tipo globale.
*
* @return bool
*/
public function isCoreUpdate()
{
return file_exists($this->directory.'/VERSION');
}
public function getChangelog()
{
if ($this->isCoreUpdate()) {
$changelog = self::readChangelog($this->getDirectory(), Update::getVersion());
} else {
$changelogs = [];
$elements = $this->componentUpdates();
$list = array_merge($elements['modules'], $elements['plugins']);
foreach ($list as $element) {
$changelog = self::readChangelog($element['path'], $element['version']);
if (!empty($changelog)) {
$changelogs[] = '
<h4 class="text-center">'.$element['info']['name'].'<h4>
'.$changelog;
}
}
$changelog = implode('<hr>', $changelogs);
}
return $changelog;
}
public function getRequirements()
{
$file = $this->directory.'/config/requirements.php';
$result = Requirements::getRequirementsList($file);
return $result;
}
public function getVersion()
{
return Update::getFile($this->getDirectory().'/VERSION');
}
/**
* Individua i componenti indipendenti che compongono l'aggiornamento.
*
* @return array
*/
public function componentUpdates()
{
if (!isset($this->components)) {
$finder = Finder::create()
->files()
->ignoreDotFiles(true)
->ignoreVCS(true)
->in($this->directory);
$files = $finder->name('MODULE')->name('PLUGIN');
$results = [];
foreach ($files as $file) {
$is_module = basename($file->getRealPath()) == 'MODULE';
$is_plugin = basename($file->getRealPath()) == 'PLUGIN';
$info = Ini::readFile($file->getRealPath());
$installed = module($info['name']);
if ($is_module) {
$type = 'modules';
} elseif ($is_plugin) {
$type = 'plugins';
}
if (!isset($results[$type])) {
$results[$type] = [];
}
$results[$type][] = [
'path' => dirname($file->getRealPath()),
'config' => $file->getRealPath(),
'is_installed' => !empty($installed),
'current_version' => !empty($installed) ? $installed->version : null,
'info' => $info,
];
}
$this->components = $results;
}
return $this->components;
}
/**
* Effettua l'aggiornamento.
*/
public function execute()
{
if ($this->isCoreUpdate()) {
$this->executeCore();
} else {
$components = $this->componentUpdates();
foreach ((array) $components['modules'] as $module) {
$this->executeModule($module);
}
foreach ((array) $components['plugins'] as $plugin) {
$this->executeModule($plugin);
}
}
$this->delete();
}
/**
* Completa l'aggiornamento globale secondo la procedura apposita.
*/
public function executeCore()
{
// Salva il file di configurazione
$config = file_get_contents(DOCROOT.'/config.inc.php');
// Copia i file dalla cartella temporanea alla root
copyr($this->directory, DOCROOT);
// Ripristina il file di configurazione dell'installazione
file_put_contents(DOCROOT.'/config.inc.php', $config);
}
/**
* Completa l'aggiornamento con le informazioni specifiche per i moduli.
*
* @param array $module
*/
public function executeModule($module)
{
// Informazioni dal file di configurazione
$info = $module['info'];
// Informazioni aggiuntive per il database
$insert = [
'parent' => module($info['parent']),
'icon' => $info['icon'],
];
$id = $this->executeComponent($module['path'], 'modules', 'zz_modules', $insert, $info, $module['is_installed']);
if (!empty($id)) {
// Fix per i permessi di amministratore
$element = Module::find($id);
$element->groups()->syncWithoutDetaching($this->groups());
}
}
/**
* Instanzia un aggiornamento sulla base di uno zip indicato.
* Controlla inoltre che l'aggiornamento sia fattibile.
*
* @param string $file
*
* @throws DowngradeException
* @throws InvalidArgumentException
*
* @return static
*/
public static function make($file)
{
$extraction_dir = Zip::extract($file);
$update = new static($extraction_dir);
if ($update->isCoreUpdate()) {
$version = Update::getFile($update->getDirectory().'/VERSION');
$current = Update::getVersion();
if (version_compare($current, $version) >= 0) {
$update->delete();
throw new DowngradeException();
}
} else {
$components = $update->componentUpdates();
foreach ((array) $components['modules'] as $module) {
if (version_compare($module['current_version'], $module['info']['version']) >= 0) {
delete($module['path']);
}
}
foreach ((array) $components['plugins'] as $plugin) {
if (version_compare($plugin['current_version'], $plugin['info']['version']) >= 0) {
delete($plugin['path']);
}
}
}
// Instanzia nuovamente l'oggetto
return new static($extraction_dir);
}
/**
* Controlla se è disponibile un aggiornamento nella repository GitHub.
*
* @return string|bool
*/
public static function isAvailable()
{
$release = self::getLastRelease();
$version = ltrim($release['tag_name'], 'v');
$current = Update::getVersion();
$result = false;
if (version_compare($current, $version) < 0) {
$result = $version.($release['prerelease'] ? ' (beta)' : '');
}
// Aggiornamento cache dedicata
Cache::pool('Ultima versione di OpenSTAManager disponibile')->set($result);
return $result;
}
/**
* Scarica la release più recente (se presente).
*
* @return static
*/
public static function download()
{
if (self::isAvailable() === false) {
return null;
}
$directory = Zip::getExtractionDirectory();
$file = $directory.'/release.zip';
directory($directory);
$release = self::getLastRelease();
self::getClient()->request('GET', $release['assets'][0]['browser_download_url'], ['sink' => $file]);
$update = self::make($file);
delete($file);
return $update;
}
/**
* Restituisce i contenuti JSON dell'API del progetto.
*
* @return array
*/
public static function checkFiles()
{
$date = date('Y-m-d', filemtime(DOCROOT.'/core.php'));
// Individuazione dei file tramite data di modifica
$files = Finder::create()
->date('<= '.$date)
->sortByName()
->in(DOCROOT)
->exclude([
'node_modules',
'tests',
'tmp',
'vendor',
])
->name('*.php')
->notPath('*custom*')
->files();
return iterator_to_array($files);
}
/**
* Restituisce il changelog presente nel percorso indicato a partire dalla versione specificata.
*
* @param string $path
* @param string $version
*
* @return string
*/
protected static function readChangelog($path, $version = null)
{
$result = file_get_contents($path.'/CHANGELOG.md');
$start = strpos($result, '## ');
$result = substr($result, $start);
if (!empty($version)) {
$last = strpos($result, '## '.$version.' ');
if ($last !== false) {
$result = substr($result, 0, $last);
}
}
$result = Parsedown::instance()->text($result);
$result = str_replace(['h4>', 'h3>', 'h2>'], ['p>', 'b>', 'h4>'], $result);
return $result;
}
/**
* Completa l'aggiornamento del singolo componente come previsto dai parametri indicati.
*
* @param string $directory Percorso di copia dei contenuti
* @param string $table Tabella interessata dall'aggiornamento
* @param array $insert Informazioni per la registrazione
* @param array $info Contenuti della configurazione
* @param bool $is_installed
*
* @return int|null
*/
protected function executeComponent($path, $directory, $table, $insert, $info, $is_installed = false)
{
// Copia dei file nella cartella relativa
copyr($path, DOCROOT.'/'.$directory.'/'.$info['directory']);
// Eventuale registrazione nel database
if (empty($is_installed)) {
$dbo = database();
$dbo->insert($table, array_merge($insert, [
'name' => $info['name'],
'title' => !empty($info['title']) ? $info['title'] : $info['name'],
'directory' => $info['directory'],
'options' => $info['options'],
'version' => $info['version'],
'compatibility' => $info['compatibility'],
'order' => 100,
'default' => 0,
'enabled' => 1,
]));
return $dbo->lastInsertedID();
}
}
/**
* Resituisce i permessi di default da impostare all'installazione del componente.
*
* @return array
*/
protected function groups()
{
if (!isset($this->groups)) {
$groups = Group::where('nome', 'Amministratori')->get();
$result = [];
foreach ($groups as $group) {
$result[$group->id] = [
'permission_level' => 'rw',
];
}
$this->groups = $result;
}
return $this->groups;
}
/**
* Restituisce l'oggetto per la connessione all'API del progetto.
*
* @return Client
*/
protected static function getClient()
{
if (!isset(self::$client)) {
self::$client = new Client([
'base_uri' => 'https://api.github.com/repos/devcode-it/openstamanager/',
'verify' => false,
]);
}
return self::$client;
}
/**
* Restituisce i contenuti JSON dell'API del progetto.
*
* @return array
* @throws \GuzzleHttp\Exception\GuzzleException
*/
protected static function getLastRelease()
{
$response = self::getClient()->request('GET', 'releases');
$body = $response->getBody();
// Impostazione per l'utilizzo o meno di versioni non stabili
$prerelease = intval(setting('Abilita canale pre-release per aggiornamenti'));
// Ricerca dell'ultima release del tipi specificato
$releases = json_decode($body, true) ?: [];
foreach ($releases as $release) {
if ($release['prerelease'] == $prerelease) {
return $release;
}
}
return null;
}
}

View File

@ -0,0 +1,249 @@
<?php
namespace Modules\Aggiornamenti\Http\Controllers;
use App\Http\Controllers\RequirementsController;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use InvalidArgumentException;
use Modules\Aggiornamenti\Http\Aggiornamento;
use Modules\Aggiornamenti\Http\DowngradeException;
class AggiornamentiController extends Controller
{
/**
* Display a listing of the resource.
* @return Renderable
*/
public function index()
{
try {
$update = new Aggiornamento();
$args = [
'update' => $update,
'update_version' => $update->getVersion(),
'update_requirements' => $update->getRequirements(),
];
return view('aggiornamenti::update', $args);
} catch (InvalidArgumentException $e) {
}
$custom = $this->customComponents();
$tables = $this->customTables();
// Aggiornamenti
$alerts = [];
if (!extension_loaded('zip')) {
$alerts[tr('Estensione ZIP')] = tr('da abilitare');
}
$upload_max_filesize = ini_get('upload_max_filesize');
$upload_max_filesize = str_replace(['k', 'M'], ['000', '000000'], $upload_max_filesize);
// Dimensione minima: 32MB
if ($upload_max_filesize < 32000000) {
$alerts['upload_max_filesize'] = '32MB';
}
$post_max_size = ini_get('post_max_size');
$post_max_size = str_replace(['k', 'M'], ['000', '000000'], $post_max_size);
// Dimensione minima: 32MB
if ($post_max_size < 32000000) {
$alerts['post_max_size'] = '32MB';
}
$args = [
'module' => module('Aggiornamenti'),
'custom' => $custom,
'tables' => $tables,
'alerts' => $alerts,
'enable_updates' => setting('Attiva aggiornamenti'),
'requirements' => RequirementsController::getRequirementsList(),
];
return view('aggiornamenti::index', $args);
}
/**
* Controlla se il database presenta alcune sezioni personalizzate.
*
* @return array
*/
public function customStructure()
{
$results = [];
$dirs = [
'modules',
'templates',
'plugins',
];
// Controlli di personalizzazione fisica
foreach ($dirs as $dir) {
$files = glob(base_dir().'/'.$dir.'/*/custom/*.{php,html}', GLOB_BRACE);
$recursive_files = glob(base_dir().'/'.$dir.'/*/custom/**/*.{php,html}', GLOB_BRACE);
$files = array_merge($files, $recursive_files);
foreach ($files as $file) {
$file = str_replace(base_dir().'/', '', $file);
$result = explode('/custom/', $file)[0];
if (!in_array($result, $results)) {
$results[] = $result;
}
}
}
// Gestione cartella include
$files = glob(base_dir().'/include/custom/*.{php,html}', GLOB_BRACE);
$recursive_files = glob(base_dir().'/include/custom/**/*.{php,html}', GLOB_BRACE);
$files = array_merge($files, $recursive_files);
foreach ($files as $file) {
$file = str_replace(base_dir().'/', '', $file);
$result = explode('/custom/', $file)[0];
if (!in_array($result, $results)) {
$results[] = $result;
}
}
return $results;
}
/**
* Controlla se il database presenta alcune sezioni personalizzate.
*
* @return array
*/
protected function customTables()
{
$tables = include base_dir().'/update/tables.php';
$names = [];
foreach ($tables as $table) {
$names[] = prepare($table);
}
$database = database();
$results = $database->fetchArray('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '.prepare($database->getDatabaseName()).' AND TABLE_NAME NOT IN ('.implode(',', $names).") AND TABLE_NAME != 'updates'");
return array_column($results, 'TABLE_NAME');
}
/**
* Controlla se il database presenta alcune sezioni personalizzate.
*
* @return array
*/
protected function customDatabase()
{
$database = database();
$modules = $database->fetchArray("SELECT name, CONCAT('modules/', directory) AS directory FROM zz_modules WHERE options2 != ''");
$plugins = $database->fetchArray("SELECT name, CONCAT('plugins/', directory) AS directory FROM zz_plugins WHERE options2 != ''");
$results = array_merge($modules, $plugins);
return $results;
}
protected function customComponents()
{
$database_check = $this->customDatabase();
$structure_check = $this->customStructure();
$list = [];
foreach ($database_check as $element) {
$pos = array_search($element['directory'], $structure_check);
$list[] = [
'path' => $element['directory'],
'database' => true,
'directory' => $pos !== false,
];
if ($pos !== false) {
unset($structure_check[$pos]);
}
}
foreach ($structure_check as $element) {
$list[] = [
'path' => $element,
'database' => false,
'directory' => true,
];
}
return $list;
}
public function create(Request $request)
{
if (!setting('Attiva aggiornamenti')) {
die(tr('Accesso negato'));
}
try {
$update = Aggiornamento::make($_FILES['blob']['tmp_name']);
} catch (DowngradeException $e) {
flash()->error(tr('Il pacchetto contiene una versione precedente del gestionale'));
} catch (InvalidArgumentException $e) {
flash()->error(tr('Il pacchetto contiene solo componenti già installate e aggiornate'));
}
}
public function check(Request $request)
{
$result = Aggiornamento::isAvailable();
$result = $result === false ? 'none' : $result;
return $result;
}
public function download(Request $request)
{
Aggiornamento::download();
}
public function execute(Request $request)
{
try {
$update = new Aggiornamento();
$update->execute();
} catch (InvalidArgumentException $e) {
}
$route = $this->router->urlFor('module', [
'module_id' => $args['module_id'],
]);
$response = $response->withRedirect($route);
return $response;
}
public function cancel(Request $request)
{
try {
$update = new Aggiornamento();
$update->delete();
} catch (InvalidArgumentException $e) {
}
$route = $this->router->urlFor('module', [
'module_id' => $args['module_id'],
]);
$response = $response->withRedirect($route);
return $response;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Modules\Aggiornamenti\Http\Controllers;
use App\Http\Controllers\RequirementsController;
use Illuminate\Contracts\Support\Renderable;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use InvalidArgumentException;
use Modules\Aggiornamenti\Http\Aggiornamento;
class ControlliAggiuntiviController extends Controller
{
}

View File

@ -0,0 +1,9 @@
<?php
namespace Modules\Aggiornamenti\Http;
use InvalidArgumentException;
class DowngradeException extends InvalidArgumentException
{
}

View File

@ -0,0 +1,116 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.r.l.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace Modules\Aggiornamenti\Http;
use GuzzleHttp\Client;
use Hooks\CachedManager;
use Update;
/**
* Hook dedicato all'individuazione di nuove versioni del gestionale, pubblicate sulla repository ufficiale di GitHub.
*/
class UpdateHook extends CachedManager
{
protected static $client = null;
public function getCacheName()
{
return 'Ultima versione di OpenSTAManager disponibile';
}
public function cacheData()
{
return self::isAvailable();
}
public function response()
{
$update = $this->getCache()->content;
if ($update == Update::getVersion()) {
$update = null;
}
$module = module('Aggiornamenti');
$link = base_url().'/controller.php?id_module='.$module->id;
$message = tr("E' disponibile la versione _VERSION_ del gestionale", [
'_VERSION_' => $update,
]);
return [
'icon' => 'fa fa-download text-info',
'link' => $link,
'message' => $message,
'show' => !empty($update),
];
}
/**
* Controlla se è disponibile un aggiornamento nella repository GitHub.
*
* @return array|bool
*/
public static function isAvailable()
{
$api = self::getAPI();
if (!$api['prerelease'] or setting('Abilita canale pre-release per aggiornamenti')) {
$version[0] = ltrim($api['tag_name'], 'v');
$version[1] = !empty($api['prerelease']) ? 'beta' : 'stabile';
$current = Update::getVersion();
if (version_compare($current, $version[0]) < 0) {
return $version;
}
}
return false;
}
/**
* Restituisce l'oggetto per la connessione all'API del progetto.
*
* @return Client
*/
protected static function getClient()
{
if (!isset(self::$client)) {
self::$client = new Client([
'base_uri' => 'https://api.github.com/repos/devcode-it/openstamanager/',
'verify' => false,
]);
}
return self::$client;
}
/**
* Restituisce i contenuti JSON dell'API del progetto.
*
* @return array
*/
protected static function getAPI()
{
$response = self::getClient()->request('GET', 'releases');
$body = $response->getBody();
return json_decode($body, true)[0];
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Modules\Aggiornamenti\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;
class AggiornamentiServiceProvider extends ServiceProvider
{
/**
* @var string $moduleName
*/
protected $moduleName = 'Aggiornamenti';
/**
* @var string $moduleNameLower
*/
protected $moduleNameLower = 'aggiornamenti';
/**
* Boot the application events.
*
* @return void
*/
public function boot()
{
$this->registerTranslations();
$this->registerConfig();
$this->registerViews();
$this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->register(RouteServiceProvider::class);
}
/**
* Register config.
*
* @return void
*/
protected function registerConfig()
{
$this->publishes([
module_path($this->moduleName, 'Config/config.php') => config_path($this->moduleNameLower . '.php'),
], 'config');
$this->mergeConfigFrom(
module_path($this->moduleName, 'Config/config.php'), $this->moduleNameLower
);
}
/**
* Register views.
*
* @return void
*/
public function registerViews()
{
$viewPath = resource_path('views/modules/' . $this->moduleNameLower);
$sourcePath = module_path($this->moduleName, 'Resources/views');
$this->publishes([
$sourcePath => $viewPath
], ['views', $this->moduleNameLower . '-module-views']);
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);
}
/**
* Register translations.
*
* @return void
*/
public function registerTranslations()
{
$langPath = resource_path('lang/modules/' . $this->moduleNameLower);
if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, $this->moduleNameLower);
} else {
$this->loadTranslationsFrom(module_path($this->moduleName, 'Resources/lang'), $this->moduleNameLower);
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [];
}
private function getPublishableViewPaths(): array
{
$paths = [];
foreach (\Config::get('view.paths') as $path) {
if (is_dir($path . '/modules/' . $this->moduleNameLower)) {
$paths[] = $path . '/modules/' . $this->moduleNameLower;
}
}
return $paths;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Modules\Aggiornamenti\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
/**
* The module namespace to assume when generating URLs to actions.
*
* @var string
*/
protected $moduleNamespace = 'Modules\Aggiornamenti\Http\Controllers';
/**
* Called before routes are registered.
*
* Register any model bindings or pattern based filters.
*
* @return void
*/
public function boot()
{
parent::boot();
}
/**
* Define the routes for the application.
*
* @return void
*/
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->moduleNamespace)
->group(module_path('Aggiornamenti', '/Routes/web.php'));
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->moduleNamespace)
->group(module_path('Aggiornamenti', '/Routes/api.php'));
}
}

View File

View File

View File

@ -0,0 +1,222 @@
@extends('modules.base')
@section('module_content')
@if(!empty($custom) or !empty($tables))
<!-- Personalizzazioni di codice -->
<div class="box box-outline box-warning">
<div class="box-header with-border">
<h3 class="box-title">
<span class="tip" title="{{ tr('Elenco delle personalizzazioni rilevabili dal gestionale') }}.">
<i class="fa fa-edit"></i> {{ tr('Personalizzazioni') }}
</span>
</h3>
</div>
<div class="box-body">
@if(!empty($custom))
<table class="table table-hover table-striped">
<tr>
<th width="10%">{{ tr('Percorso') }}</th>
<th width="15%">{{ tr('Cartella personalizzata') }}</th>
<th width="15%">{{ tr('Database personalizzato') }}</th>
</tr>
@foreach($custom as $element)
<tr>
<td>{{ $element['path'] }}</td>
<td>{{ $element['directory'] ? tr('Si') : tr('No') }}</td>
<td>{{ $element['database'] ? tr('Si') : tr('No') }}</td>
</tr>
@endforeach
</table>
<p><strong>{{ tr("Si sconsiglia l'aggiornamento senza il supporto dell'assistenza ufficiale") }}.</strong></p>
@else
<p>{{ tr('Non ci sono strutture personalizzate') }}.</p>
@endif
@if(!empty($tables))
<div class="alert alert-warning">
<i class="fa fa-warning"></i>
<b>{{ tr('Attenzione!') }}</b> {{ tr('Ci sono delle tabelle non previste nella versione standard del gestionale: _LIST_', [
'_LIST_' => implode(', ', $tables),
]) }}
</div>
@endif
</div>
</div>
@endif
@if(!empty($alerts))
<div class="alert alert-warning">
<p>{{ tr('Devi modificare il seguenti parametri del file di configurazione PHP (_FILE_) per poter caricare gli aggiornamenti', ['_FILE_' => '<b>php.ini</b>']) }}:</p>
<ul>
@foreach($alerts as $key => $value)
<li><b>{{ $key }}</b> = {{ $value }}</li>
@endforeach
</ul>
</div>
@endif
<script>
function update() {
if ($("#blob").val()) {
swal({
title: "{{ tr('Avviare la procedura?') }}",
type: "warning",
showCancelButton: true,
confirmButtonText: "{{ tr('Sì') }}"
}).then(function (result) {
$("#update").submit();
})
} else {
swal({
title: "{{ tr('Selezionare un file!') }}",
type: "error",
})
}
}
function search(button) {
buttonLoading(button);
$.ajax({
url: "{{ route('controllo-versione') }}",
type: "post",
success: function(data){
$("#update-search").addClass("hidden");
if (data === "none" || !data) {
$("#update-none").removeClass("hidden");
} else {
if (data.includes("beta")) {
$("#beta-warning").removeClass("hidden");
}
$("#update-version").text(data);
$("#update-download").removeClass("hidden");
}
}
});
}
function download(button) {
buttonLoading(button);
$("#main_loading").show();
$.ajax({
url: "{{ route('scarica-aggiornamento') }}",
type: "post",
success: function(){
window.location.reload();
}
});
}
function checksum(button) {
openModal("{{ tr('Controllo dei file') }}", "{{ route('checksum') }}");
}
function database(button) {
openModal("{{ tr('Controllo del database') }}", "{{ route('database') }}");
}
function controlli(button) {
openModal("{{ tr('Controlli del gestionale') }}", "{{ route('controlli') }}");
}
</script>
<div class="row">
<div class="col-md-4">
<div class="box box-outline box-success">
<div class="box-header with-border">
<h3 class="box-title">
{{ tr('Carica un aggiornamento') }} <span class="tip" title="{{ tr('Form di caricamento aggiornamenti del gestionale e innesti di moduli e plugin') }}."><i class="fa fa-question-circle-o"></i></span>
</h3>
</div>
<div class="box-body">
<form action="" method="post" enctype="multipart/form-data" id="update">
<input type="hidden" name="op" value="upload">
<input type="hidden" name="backto" value="record-list">
{[ "type": "file", "name": "blob", "required": 1, "accept": ".zip" ]}
<button type="button" class="btn btn-primary float-right" onclick="update()">
<i class="fa fa-upload"></i> {{ tr('Carica') }}
</button>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title">
{{ tr("Verifica l'integrità dell'intallazione") }} <span class="tip" title="{{ tr("Verifica l'integrità della tua installazione attraverso un controllo sui checksum dei file e sulla struttura del database") }}."><i class="fa fa-question-circle-o"></i></span>
</h3>
</div>
<div class="box-body">
<button type="button" class="btn btn-primary btn-block" onclick="checksum(this)">
<i class="fa fa-list-alt"></i> {{ tr('Controlla file') }}
</button>
<button type="button" class="btn btn-info btn-block" onclick="database(this)">
<i class="fa fa-database"></i> {{ tr('Controlla database') }}
</button>
<button type="button" class="btn btn-block" onclick="controlli(this)">
<i class="fa fa-stethoscope"></i> {{ tr('Controlla gestionale') }}
</button>
</div>
</div>
</div>
<div class="col-md-4">
<div class="box box-outline box-info">
<div class="box-header with-border">
<h3 class="box-title">
{{ tr('Ricerca aggiornamenti') }} <span class="tip" title="{{ tr('Controllo automatico della presenza di aggiornamenti per il gestionale') }}."><i class="fa fa-question-circle-o"></i></span>
</h3>
</div>
<div class="box-body">
@if(extension_loaded('curl'))
<div id="update-search">
<button type="button" class="btn btn-info btn-block" onclick="search(this)">
<i class="fa fa-search"></i> {{ tr('Ricerca') }}
</button>
</div>
@else
<button type="button" class="btn btn-warning btn-block disabled" >
<i class="fa fa-warning"></i> {{ tr('Estensione curl non supportata') }}.
</button>
@endif
<div id="update-download" class="hidden">
<p>{{ tr("E' stato individuato un nuovo aggiornamento") }}: <b id="update-version"></b>.</p>
<p id="beta-warning" class="hidden"><b>{{ tr('Attenzione: la versione individuata è in fase sperimentale, e pertanto può presentare diversi bug e malfunzionamenti') }}.</b></p>
<p>{!! tr('Scaricalo manualmente (_LINK_) oppure in automatico', ['_LINK_' => '<a href="https://github.com/devcode-it/openstamanager/releases">https://github.com/devcode-it/openstamanager/releases</a>']) !!}:</p>
<button type="button" class="btn btn-success btn-block" onclick="download(this)">
<i class="fa fa-download"></i> {{ tr('Scarica') }}
</button>
</div>
<div id="update-none" class="hidden">
<p>{{ tr('Nessun aggiornamento presente') }}.</p>
</div>
</div>
</div>
</div>
</div>
<hr>
<div id="requirements">
<h3>{{ tr('Requisiti') }}</h3>
@include('config.requirements-list')
</div>
@endsection

View File

@ -0,0 +1,137 @@
{% extends "layouts/base.twig" %}
{% block body_class %}hold-transition login-page{% endblock %}
{% block title %}{{ 'Aggiornamento'|trans }}{% endblock %}
{% block body %}
<div class="card card-outline card-center-large card-warning">
<div class="card-header">
<a class="h5" data-toggle="tab" href="#info">{{ "Informazioni sull'aggiornamento"|trans }}</a>
<ul class="nav nav-tabs float-right" id="tabs" role="tablist">
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#changelog">{{ 'Changelog'|trans }}</a>
</li>
</ul>
</div>
<div class="card-body tab-content">
<div id="info" class="tab-pane fade in active">
{% if update.isCoreUpdate() %}
<p>{{ "Il pacchetto selezionato contiene un aggiornamento dell'intero gestionale"|trans }}.</p>
<p>{{ "Si consiglia vivamente di effettuare un backup dell'installazione prima di procedere"|trans }}.</p>
<button type="button" class="btn btn-primary float-right" onclick="backup()">
<i class="fa fa-database"></i> {{ 'Crea backup'|trans }}
</button>
<div class="clearfix"></div>
<hr>
<h3 class="text-center">{{ 'OpenSTAManager versione _VERSION_'|trans({'_VERSION_': update.getVersion()}) }}</h3>
{% include 'config/list.twig' with {requirements: update.getRequirements()} only %}
{% else %}
{% set elements = update.componentUpdates() %}
<div class="alert alert-warning">
<i class="fa fa-warning"></i>
<b>{{ 'Attenzione!'|trans }}</b> {{ 'Verranno aggiornate le sole componenti del pacchetto che non sono già installate e aggiornate'|trans }}.
</div>
{% if elements.modules is not empty %}
<p>{{ 'Il pacchetto selezionato comprende i seguenti moduli'|trans }}:</p>
<ul class="list-group">
{% for element in elements.modules %}
<li class="list-group-item">
<span class="badge">{{ element['info']['version'] }}</span>
{% if element.is_installed %}
<span class="badge">{{ 'Installato'|trans }}</span>';
{% endif %}
{{ element['info']['name'] }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if elements.plugins is not empty %}
<p>{{ 'Il pacchetto selezionato comprende i seguenti plugin'|trans }}:</p>
<ul class="list-group">';
{% for element in elements.plugins %}
<li class="list-group-item">
<span class="badge">{{ element['info']['version'] }}</span>
{% if element.is_installed %}
<span class="badge">{{ 'Installato'|trans }}</span>';
{% endif %}
{{ element['info']['name'] }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
</div>
<div id="changelog" class="tab-pane fade">
{% set changelog = update.getChangelog() %}
{% if changelog is not empty %}
{{ changelog|raw }}
{% else %}
<p>{{ 'Nessuna changelog individuabile'|trans }}.</p>
{% endif %}
</div>
<hr>
<form action="{{ action_link|replace({'|action|': 'cancel'}) }}" method="post" style="display:inline-block">
<button type="submit" class="btn btn-warning">
<i class="fa fa-arrow-left"></i> {{ 'Annulla'|trans }}
</button>
</form>
<form action="{{ action_link|replace({'|action|': 'execute'}) }}" method="post" class="float-right" style="display:inline-block">
<button type="submit" class="btn btn-success">
<i class="fa fa-arrow-right"></i> {{ 'Procedi'|trans }}
</button>
</form>
</div>
</div>
<script>
function backup(){
swal({
title: "{{ 'Nuovo backup'|trans }}",
text: "{{ 'Sei sicuro di voler creare un nuovo backup?'|trans }}",
type: "warning",
showCancelButton: true,
confirmButtonClass: "btn btn-lg btn-success",
confirmButtonText: "{{ 'Crea'|trans }}",
}).then(function(){
$("#main_loading").show();
$.ajax({
url: "{{ url_for('module', {'module_id': module('Backup').id}) }}",
type: "post",
data: {
op: "backup",
},
success: function(data){
$("#main_loading").fadeOut();
}
});
}, function(){});
}
</script>
{% endblock %}

View File

@ -0,0 +1,18 @@
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:api')->get('/aggiornamenti', function (Request $request) {
return $request->user();
});

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Modules\Aggiornamenti\Http\Controllers\AggiornamentiController;
use Modules\Aggiornamenti\Http\Controllers\ControlliAggiuntiviController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::prefix('aggiornamenti')
->group(function () {
Route::get('', [AggiornamentiController::class, 'index']);
Route::post('controllo-versione', [AggiornamentiController::class, 'check'])
->name('controllo-versione');
Route::post('scarica-aggiornamento', [AggiornamentiController::class, 'download'])
->name('scarica-aggiornamento');
Route::get('checksum', [ControlliAggiuntiviController::class, 'checksum'])
->name('checksum');
Route::get('database', [ControlliAggiuntiviController::class, 'database'])
->name('database');
Route::get('controlli', [ControlliAggiuntiviController::class, 'controlli'])
->name('controlli');
});

View File

@ -0,0 +1,24 @@
{
"name": "devcode-it/osm-aggiornamenti-module",
"description": "",
"authors": [{
"name": "DevCode s.n.c.",
"email": "info@openstamanager.com"
}],
"require": {
"erusev/parsedown": "^1.7"
},
"extra": {
"laravel": {
"providers": [],
"aliases": {
}
}
},
"autoload": {
"psr-4": {
"Modules\\Aggiornamenti\\": ""
}
}
}

View File

@ -0,0 +1,13 @@
{
"name": "Aggiornamenti",
"alias": "aggiornamenti",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Aggiornamenti\\Providers\\AggiornamentiServiceProvider"
],
"aliases": {},
"files": [],
"requires": []
}

View File

@ -0,0 +1,17 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^7.0",
"laravel-mix": "^5.0.1",
"laravel-mix-merge-manifest": "^0.1.2"
}
}

14
modules/Aggiornamenti/webpack.mix.js vendored Normal file
View File

@ -0,0 +1,14 @@
const dotenvExpand = require('dotenv-expand');
dotenvExpand(require('dotenv').config({ path: '../../.env'/*, debug: true*/}));
const mix = require('laravel-mix');
require('laravel-mix-merge-manifest');
mix.setPublicPath('../../public').mergeManifest();
mix.js(__dirname + '/Resources/assets/js/app.js', 'js/aggiornamenti.js')
.sass( __dirname + '/Resources/assets/sass/app.scss', 'css/aggiornamenti.css');
if (mix.inProduction()) {
mix.version();
}

View File

@ -1,26 +0,0 @@
<?php
include_once __DIR__.'/../../core.php';
switch (post('op')) {
case 'aggiorna-listino':
$sconto = post('sconto');
foreach ($id_records as $id_record) {
$dbo->query('UPDATE mg_prezzi_articoli SET sconto_percentuale='.prepare($sconto).' WHERE id='.prepare($id_record));
}
flash()->info(tr('Sconti modificati correttamente!'));
break;
}
return [
'aggiorna-listino' => [
'text' => '<span>'.tr('Modifica sconto').'</span>',
'data' => [
'title' => tr('Inserisci lo sconto per questi articoli'),
'msg' => '{[ "type": "text", "label": "<small>'.tr('Nuovo sconto').'</small>","icon-after":"%", "name": "sconto" ]}',
'button' => tr('Modifica'),
'class' => 'btn btn-lg btn-warning',
'blank' => false,
],
],
];

View File

@ -1,85 +0,0 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.n.c.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
include_once __DIR__.'/../../core.php';
use Modules\Ordini\Ordine;
use Modules\Preventivi\Preventivo;
$documento_finale = Ordine::find($id_record);
$dir = $documento_finale->direzione;
$id_documento = get('id_documento');
if (!empty($id_documento)) {
$documento = Preventivo::find($id_documento);
$options = [
'op' => 'add_documento',
'type' => 'preventivo',
'button' => tr('Aggiungi'),
'documento' => $documento,
'documento_finale' => $documento_finale,
'tipo_documento_finale' => Ordine::class,
];
echo App::load('importa.php', [], $options, true);
return;
}
$id_anagrafica = $documento_finale->idanagrafica;
echo '
<div class="row">
<div class="col-md-12">
{[ "type": "select", "label": "'.tr('Preventivo').'", "name": "id_documento", "ajax-source": "preventivi", "select-options": {"idanagrafica": '.$id_anagrafica.', "stato": "is_fatturabile=1 OR is_completato"} ]}
</div>
</div>
<div id="righe_documento">
</div>
<div class="alert alert-info" id="box-loading">
<i class="fa fa-spinner fa-spin"></i> '.tr('Caricamento in corso').'...
</div>';
$file = basename(__FILE__);
echo '
<script>$(document).ready(init)</script>
<script>
var content = $("#righe_documento");
var loader = $("#box-loading");
$(document).ready(function() {
loader.hide();
});
$("#id_documento").on("change", function() {
loader.show();
var id = $(this).selectData() ? $(this).selectData().id : "";
content.html("");
content.load("'.$structure->fileurl($file).'?id_module='.$id_module.'&id_record='.$id_record.'&id_documento=" + id, function() {
loader.hide();
});
});
</script>';

View File

@ -1,133 +0,0 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.n.c.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
include_once __DIR__.'/../../core.php';
use Modules\Ordini\Ordine;
use Modules\Fatture\Fattura;
use Modules\Fatture\Stato;
use Modules\Fatture\Tipo;
$module_fatture = 'Fatture di vendita';
// Segmenti
$id_fatture = Modules::get($module_fatture)['id'];
if (!isset($_SESSION['module_'.$id_fatture]['id_segment'])) {
$segments = Modules::getSegments($id_fatture);
$_SESSION['module_'.$id_fatture]['id_segment'] = isset($segments[0]['id']) ? $segments[0]['id'] : null;
}
$id_segment = $_SESSION['module_'.$id_fatture]['id_segment'];
switch (post('op')) {
case 'crea_fattura':
$documenti = collect();
$numero_totale = 0;
$descrizione_tipo = 'Fattura immediata di vendita';
$tipo_documento = Tipo::where('descrizione', $descrizione_tipo)->first();
$stato_documenti_accodabili = Stato::where('descrizione', 'Bozza')->first();
$accodare = post('accodare');
$data = date('Y-m-d');
$id_segment = post('id_segment');
// Lettura righe selezionate
foreach ($id_records as $id) {
$documento_import = Ordine::find($id);
$anagrafica = $documento_import->anagrafica;
$id_anagrafica = $anagrafica->id;
// Proseguo solo se i documenti scelti sono fatturabili
$ordine = $dbo->fetchOne('SELECT or_statiordine.descrizione AS stato FROM or_ordini LEFT JOIN or_statiordine ON or_ordini.idstatoordine=or_statiordine.id WHERE or_ordini.id='.prepare($id))['stato'];
if (!in_array($ordine, ['Fatturato', 'Evaso', 'Bozza', 'In attesa di conferma', 'Annullato'])) {
$righe = $documento_import->getRighe();
if (!empty($righe)) {
++$numero_totale;
// Ricerca fattura per anagrafica tra le registrate
$fattura = $documenti->first(function ($item, $key) use ($id_anagrafica) {
return $item->anagrafica->id == $id_anagrafica;
});
// Ricerca fattura per anagrafica se l'impostazione di accodamento è selezionata
if (!empty($accodare) && empty($fattura)) {
$fattura = Fattura::where('idanagrafica', $id_anagrafica)
->where('idstatodocumento', $stato_documenti_accodabili->id)
->where('idtipodocumento', $tipo_documento->id)
->first();
if (!empty($fattura)) {
$documenti->push($fattura);
}
}
// Creazione fattura per anagrafica
if (empty($fattura)) {
$fattura = Fattura::build($anagrafica, $tipo_documento, $data, $id_segment);
$documenti->push($fattura);
}
// Inserimento righe
foreach ($righe as $riga) {
$qta = $riga->qta_rimanente;
if ($qta > 0) {
//Fix per idconto righe fattura
$riga->idconto = $fattura->idconto;
$copia = $riga->copiaIn($fattura, $qta);
// Aggiornamento seriali dalla riga dell'ordine
if ($copia->isArticolo()) {
$copia->serials = $riga->serials;
}
}
}
}
}
}
if ($numero_totale > 0) {
flash()->info(tr('_NUM_ ordini fatturati!', [
'_NUM_' => $numero_totale,
]));
} else {
flash()->warning(tr('Nessun ordine fatturato!'));
}
break;
}
if ($module['name'] == 'Ordini cliente') {
$operations['crea_fattura'] = [
'text' => '<span><i class="fa fa-file-code-o"></i> '.tr('Fattura _TYPE_', ['_TYPE_' => strtolower($module['name'])]),
'data' => [
'title' => tr('Fatturare i _TYPE_ selezionati?', ['_TYPE_' => strtolower($module['name'])]),
'msg' => '{[ "type": "checkbox", "label": "<small>'.tr('Aggiungere alle _TYPE_ non ancora emesse?', ['_TYPE_' => strtolower($module_fatture)]).'", "placeholder": "'.tr('Aggiungere alle _TYPE_ nello stato bozza?', ['_TYPE_' => strtolower($module_fatture)]).'</small>", "name": "accodare" ]}
<br>{[ "type": "select", "label": "'.tr('Sezionale').'", "name": "id_segment", "required": 1, "values": "query=SELECT id, name AS descrizione FROM zz_segments WHERE id_module=\''.$id_fatture.'\' AND is_fiscale = 1 ORDER BY name", "value": "'.$id_segment.'" ]}',
'button' => tr('Procedi'),
'class' => 'btn btn-lg btn-warning',
'blank' => false,
],
];
}
if($operations){
return $operations;
} else {
return;
}

View File

@ -1,115 +0,0 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.n.c.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
include_once __DIR__.'/../../core.php';
switch (filter('op')) {
case 'update':
$descrizione = filter('descrizione');
$dir = filter('dir');
$codice_tipo_documento_fe = filter('codice_tipo_documento_fe');
if (isset($descrizione) && isset($dir) && isset($codice_tipo_documento_fe)) {
if ($dbo->fetchNum('SELECT * FROM `co_tipidocumento` WHERE `dir`='.prepare($dir).' AND `codice_tipo_documento_fe`='.prepare($codice_tipo_documento_fe).' AND `id`!='.prepare($id_record)) == 0) {
$predefined = post('predefined');
if (!empty($predefined)) {
$dbo->query('UPDATE co_tipidocumento SET predefined = 0 WHERE dir = '.prepare($dir));
}
$dbo->update('co_tipidocumento', [
'descrizione' => $descrizione,
'dir' => $dir,
'codice_tipo_documento_fe' => $codice_tipo_documento_fe,
'help' => filter('help'),
'predefined' => $predefined,
'enabled' => post('enabled'),
], ['id' => $id_record]);
flash()->info(tr('Salvataggio completato!'));
} else {
flash()->error(tr("E' già presente una tipologia di _TYPE_ con la stessa combinazione di direzione e tipo documento FE", [
'_TYPE_' => 'tipo documento',
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;
case 'add':
$descrizione = filter('descrizione');
$dir = filter('dir');
$codice_tipo_documento_fe = filter('codice_tipo_documento_fe');
if (isset($descrizione) && isset($dir) && isset($codice_tipo_documento_fe)) {
if ($dbo->fetchNum('SELECT * FROM `co_tipidocumento` WHERE `dir`='.prepare($dir).' AND `codice_tipo_documento_fe`='.prepare($codice_tipo_documento_fe)) == 0) {
$dbo->insert('co_tipidocumento', [
'descrizione' => $descrizione,
'dir' => $dir,
'codice_tipo_documento_fe' => $codice_tipo_documento_fe,
]);
$id_record = $dbo->lastInsertedID();
if (isAjaxRequest()) {
echo json_encode(['id' => $id_record, 'text' => $descrizione]);
}
flash()->info(tr('Aggiunta nuova tipologia di _TYPE_', [
'_TYPE_' => 'tipo documento',
]));
} else {
flash()->error(tr("E' già presente una tipologia di _TYPE_ con la stessa combinazione di direzione e tipo documento FE", [
'_TYPE_' => 'tipo documento',
]));
}
} else {
flash()->error(tr('Ci sono stati alcuni errori durante il salvataggio'));
}
break;
case 'delete':
$documenti = $dbo->fetchNum('SELECT id FROM co_documenti WHERE idtipodocumento ='.prepare($id_record));
if (isset($id_record) && empty($documenti)) {
$dbo->query('DELETE FROM `co_tipidocumento` WHERE `id`='.prepare($id_record));
flash()->info(tr('Tipologia di _TYPE_ eliminata con successo.', [
'_TYPE_' => 'tipo documento',
]));
} else {
$dbo->update('co_tipidocumento', [
'deleted_at' => date(),
'predefined' => 0,
'enabled' => 0,
], ['id' => $id_record]);
flash()->info(tr('Tipologia di _TYPE_ eliminata con successo.', [
'_TYPE_' => 'tipo documento',
]));
//flash()->error(tr('Sono presenti dei documenti collegati a questo tipo documento'));
}
break;
}

View File

@ -1,46 +0,0 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.n.c.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
include_once __DIR__.'/../../core.php';
?><form action="" method="post" id="add-form">
<input type="hidden" name="op" value="add">
<input type="hidden" name="backto" value="record-edit">
<div class="row">
<div class="col-md-12">
{[ "type": "text", "label": "<?php echo tr('Descrizione'); ?>", "name": "descrizione", "required": 1 ]}
</div>
<div class="col-md-6">
{[ "type": "select", "label": "<?php echo tr('Direzione'); ?>", "name": "dir", "values": "list=\"\": \"Non specificato\", \"entrata\": \"<?php echo tr('Entrata'); ?>\", \"uscita\": \"<?php echo tr('Uscita'); ?>\"", "required": 1 ]}
</div>
<div class="col-md-6">
{[ "type": "select", "label": "<?php echo tr('Codice tipo documento FE'); ?>", "name": "codice_tipo_documento_fe", "values": "query=SELECT codice AS id, CONCAT_WS(' - ', codice, descrizione) AS descrizione FROM fe_tipi_documento", "required": 1 ]}
</div>
</div>
<!-- PULSANTI -->
<div class="row">
<div class="col-md-12 text-right">
<button type="submit" class="btn btn-primary"><i class="fa fa-plus"></i> <?php echo tr('Aggiungi'); ?></button>
</div>
</div>
</form>

View File

@ -1,37 +0,0 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.n.c.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
include_once __DIR__.'/../../../core.php';
switch ($resource) {
case 'tipi_documento':
$query = 'SELECT id, descrizione FROM co_tipidocumento |where| ORDER BY descrizione ASC';
$where[] = 'co_tipidocumento.enabled = 1';
$where[] = 'dir='.$superselect['dir'];
foreach ($elements as $element) {
$filter[] = 'id='.prepare($element);
}
if (!empty($search)) {
$search_fields[] = 'descrizione LIKE '.prepare('%'.$search.'%');
}
break;
}

View File

@ -1,78 +0,0 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.n.c.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
include_once __DIR__.'/../../core.php';
?><form action="" method="post" id="edit-form">
<input type="hidden" name="backto" value="record-edit">
<input type="hidden" name="op" value="update">
<div class="row">
<div class="col-md-6">
{[ "type": "text", "label": "<?php echo tr('Descrizione'); ?>", "name": "descrizione", "required": 1, "value": "$descrizione$" ]}
</div>
<div class="col-md-3">
{[ "type": "select", "label": "<?php echo tr('Direzione'); ?>", "name": "dir", "value": "$dir$", "values": "list=\"\": \"Non specificato\", \"entrata\": \"<?php echo tr('Entrata'); ?>\", \"uscita\": \"<?php echo tr('Uscita'); ?>\"", "required": 1 ]}
</div>
<div class="col-md-3">
{[ "type": "select", "label": "<?php echo tr('Codice tipo documento FE'); ?>", "name": "codice_tipo_documento_fe", "value": "$codice_tipo_documento_fe$", "values": "query=SELECT codice AS id, CONCAT_WS(' - ', codice, descrizione) AS descrizione FROM fe_tipi_documento", "required": 1 ]}
</div>
<div class="col-md-4">
{[ "type": "checkbox", "label": "<?php echo tr('Tipo documento predefinito'); ?>", "name": "predefined", "value": "<?php echo intval($record['predefined']); ?>", "help":"<?php echo tr('Impostare questo tipo di documento predefinto per le fatture'); ?>." ]}
</div>
<div class="col-md-4">
{[ "type": "checkbox", "label": "<?php echo tr('Attivo'); ?>", "name": "enabled", "value": "<?php echo intval($record['enabled']); ?>" ]}
</div>
<div class="col-md-4">
{[ "type": "checkbox", "label": "<?php echo tr('Reversed'); ?>", "name": "reversed", "value": "<?php echo intval($record['reversed']); ?>", "readonly": 1 ]}
</div>
<div class="col-md-12">
{[ "type": "text", "label": "<?php echo tr('Help'); ?>", "name": "help", "value": "$help$" ]}
</div>
</div>
</form>
<?php
// Collegamenti diretti (numerici)
$numero_documenti = $dbo->fetchNum('SELECT id FROM co_documenti WHERE idtipodocumento='.prepare($id_record));
if (!empty($numero_documenti)) {
echo '
<div class="alert alert-danger">
'.tr('Ci sono _NUM_ documenti collegati', [
'_NUM_' => $numero_documenti,
]).'.
</div>';
}
?>
<a class="btn btn-danger ask" data-backto="record-list">
<i class="fa fa-trash"></i> <?php echo tr('Elimina'); ?>
</a>

View File

@ -1,24 +0,0 @@
<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.n.c.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
include_once __DIR__.'/../../core.php';
if (isset($id_record)) {
$record = $dbo->fetchOne('SELECT * FROM `co_tipidocumento` WHERE id='.prepare($id_record));
}

3
modules_statuses.json Normal file
View File

@ -0,0 +1,3 @@
{
"Aggiornamenti": true
}

View File

@ -8,7 +8,7 @@
'value' => $value, 'value' => $value,
'required' => $required, 'required' => $required,
'data-parsley-errors-container' => '#'.$unique_id.'-errors' 'data-parsley-errors-container' => '#'.$unique_id.'-errors'
]) }} onchange="$(this).parent().find(\'[type = hidden]\').val(+this.checked).trigger(\'change\')"/> ]) }} onchange="$(this).parent().find('[type = hidden]').val(+this.checked).trigger('change')"/>
<div class="btn-group checkbox-buttons"> <div class="btn-group checkbox-buttons">
<label for="{{ $id }}" class="btn btn-default{{ $class }}"> <label for="{{ $id }}" class="btn btn-default{{ $class }}">
<span class="fa fa-check text-success"></span> <span class="fa fa-check text-success"></span>

View File

@ -0,0 +1,35 @@
@foreach($requirements as $group => $elements)
@php
$general_status = true;
foreach ($elements as $element){
$general_status &= $element['status'];
}
@endphp
<div class="box box-outline box-{{ $general_status ? 'success collapsed-box' : 'danger' }}">
<div class="box-header with-border">
<h3 class="box-title">{{ $group }}</h3>
@if($general_status)
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse">
<i class="fa fa-plus"></i>
</button>
</div>
@endif
</div>
<div class="box-body no-padding">
<table class="table">
@foreach($elements as $name => $element)
<tr class="{{ $element['status'] ? 'success' : 'danger' }}">
<td style="width: 10px"><i class="fa fa-{{ $element['status'] ? 'check' : 'times' }}"></i></td>
<td>{{ $name }}</td>
<td>{!! $element['description'] !!}</td>
</tr>
@endforeach
</table>
</div>
</div>
@endforeach

View File

@ -0,0 +1,22 @@
@extends('layouts.guest')
@section('title', tr("Requisiti"))
@section('box_class', 'box-info')
@section('box_header')
<h3 class="box-title">{{ tr('Requisiti del software OpenSTAManager') }}</h3>
@endsection
@section('content')
<p>{!! tr('Benvenuto in _NAME_!', ['_NAME_' => '<strong>'.tr('OpenSTAManager').'</strong>']) !!}</p>
<p>{{ tr("Prima di procedere alla configurazione e all'installazione del software, sono necessari alcuni accorgimenti per garantire il corretto funzionamento del gestionale") }}.</p>
<br>
<p>{!! tr('Le estensioni e impostazioni PHP possono essere personalizzate nel file di configurazione _FILE_', ['_FILE_' => '<b>php.ini</b>']) !!}.</p>
<hr>
@include('config.requirements-list')
<button type="button" class="btn btn-info center-block" onclick="window.location.reload()">
<i class="fa fa-refresh"></i> {{ tr("Ricarica") }}
</button>
@endsection

View File

@ -0,0 +1,10 @@
@extends('errors.base')
@section('title', tr("Servizio non disponibile"))
@section('error_color', 'info')
@section('error_header', '503')
@section('error_message', tr('Servizio non disponibile!'))
@section('error_info', tr("E' in corso un processo di manutenzione dell'applicazione"))
@section('error_return', tr("Torneremo attivi il più presto possibile"))

View File

@ -276,6 +276,23 @@
<!-- /.sidebar --> <!-- /.sidebar -->
</aside> </aside>
{{-- Menu laterale per la visualizzazione dei plugin --}}
@if($__env->yieldContent('sidebar'))
<aside class="control-sidebar control-sidebar-light control-sidebar-shown">
<h4 class="text-center">@yield('sidebar_title', tr('Plugin disponibili'))</h4>
<ul class="nav nav-tabs nav-pills nav-stacked">
<li data-toggle="control-sidebar" class="active">
<a data-toggle="tab" href="#tab_0">
<i class="fa fa-home"></i> {{ tr('Pagina principale') }}
</a>
</li>
@yield('sidebar')
</ul>
</aside>
<div class="control-sidebar-bg"></div>
@endif
<!-- Right side column. Contains the navbar and content of the page --> <!-- Right side column. Contains the navbar and content of the page -->
<div class="content-wrapper"> <div class="content-wrapper">
<!-- Main content --> <!-- Main content -->

View File

@ -1,6 +1,6 @@
@extends('layouts.base') @extends('layouts.base')
@section('body_class')bg-light@endsection @section('body_class', 'bg-light')
@section('body') @section('body')
@yield('before_content') @yield('before_content')

View File

@ -3,8 +3,5 @@
use App\Http\Controllers\LegacyController; use App\Http\Controllers\LegacyController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
//Route::any('/', [LegacyController::class, 'index']); // Route di fallback generale
Route::fallback([LegacyController::class, 'index']);
Route::any('/{path}', [LegacyController::class, 'index'])
->name('legacy')
->where('path', '.*');

View File

@ -4,7 +4,9 @@ use App\Http\Controllers\ConfigurationController;
use App\Http\Controllers\HookController; use App\Http\Controllers\HookController;
use App\Http\Controllers\InfoController; use App\Http\Controllers\InfoController;
use App\Http\Controllers\InitializationController; use App\Http\Controllers\InitializationController;
use App\Http\Controllers\LegacyController;
use App\Http\Controllers\MessageController; use App\Http\Controllers\MessageController;
use App\Http\Controllers\RequirementsController;
use App\Http\Controllers\Test; use App\Http\Controllers\Test;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -31,6 +33,10 @@ Route::get('/', function () {
}) })
->middleware(['auth']); ->middleware(['auth']);
// Schermata dei requisiti
Route::get('/requirements', [RequirementsController::class, 'index'])
->name('requirements');
// Sezione di configurazione // Sezione di configurazione
Route::get('/config', [ConfigurationController::class, 'index']) Route::get('/config', [ConfigurationController::class, 'index'])
->name('configuration'); ->name('configuration');