diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..b4eee3758
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+.git
+.claude
diff --git a/.env.dist b/.env.dist
index d7426001e..ab52fe734 100644
--- a/.env.dist
+++ b/.env.dist
@@ -16,4 +16,5 @@ BASE_URL=http://dev-datan.fr
API_KEY_NOBG=
API_KEY_MAILJET=
API_KEY_SECRETE_MAILJET=
-COMPOSER_AUTOLOAD=application/config/autoload.php
\ No newline at end of file
+COMPOSER_AUTOLOAD=application/config/autoload.php
+POLITIC_ANALYSIS_PUBLIC_URL=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 27bc30d53..bb01c7e73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,9 @@ application/logs/*
#custom config
application/config/custom_config.php
+#claude code
+.claude/
+
#root folder
node_modules
/test
diff --git a/application/config/config.php b/application/config/config.php
index 34e7f3dc2..eb54b9763 100644
--- a/application/config/config.php
+++ b/application/config/config.php
@@ -455,7 +455,7 @@
$config['csrf_cookie_name'] = 'csrf_cookie_name';
$config['csrf_expire'] = 7200;
$config['csrf_regenerate'] = TRUE;
-$config['csrf_exclude_uris'] = array('upload/image', 'api/.*');
+$config['csrf_exclude_uris'] = array('upload/image', 'api/.*', 'admin/amendements/review');
/*
|--------------------------------------------------------------------------
diff --git a/application/config/routes.php b/application/config/routes.php
index e044a469e..da43ab21f 100644
--- a/application/config/routes.php
+++ b/application/config/routes.php
@@ -52,6 +52,8 @@
// ADMIN
$route['admin'] = 'admin/index';
+$route['admin/amendements'] = 'admin/amendements';
+$route['admin/amendements/review'] = 'admin/amendements_review';
$route['admin/votes'] = 'admin/votes';
$route['admin/elections/modifications-mps'] = 'admin/election_modifications_mps';
$route['admin/elections/(:any)'] = 'admin/election_candidates/$1';
@@ -211,6 +213,8 @@
$route['api/non_decrypted_votes'] = 'api/non_decrypted_votes/index';
$route['api/non_decrypted_votes/meta'] = 'api/non_decrypted_votes/meta';
$route['api/non_decrypted_votes/(:any)'] = 'api/non_decrypted_votes/index/$1';
+// Résumés IA des amendements (écriture depuis PoliticAnalysis)
+$route['api/amendements_ia'] = 'api/amendements_ia/index';
// Exposés des motifs
$route['api/exposes'] = 'api/exposes/index';
$route['api/exposes/meta'] = 'api/exposes/meta';
diff --git a/application/controllers/Admin.php b/application/controllers/Admin.php
index bdd488315..c25e01ae8 100644
--- a/application/controllers/Admin.php
+++ b/application/controllers/Admin.php
@@ -1,5 +1,5 @@
session->set_flashdata('success', 'Clé API révoquée avec succès');
redirect('admin/api-keys');
}
+
+ public function amendements()
+ {
+ $data = $this->data;
+
+ $sort = $this->input->get('sort') ?: 'date';
+ $direction = $this->input->get('direction') ?: 'DESC';
+
+ $period = $this->input->get('period');
+ $date_start = $this->input->get('date_start');
+ $date_end = $this->input->get('date_end');
+ $hide_reviewed = filter_var($this->input->get('hide_reviewed'), FILTER_VALIDATE_BOOLEAN);
+
+ $allowed_periods = array('all', '7', '30', '90', '180', '365');
+ if (!in_array($period, $allowed_periods, true)) {
+ $period = 'all';
+ }
+
+ $valid_date = function ($d) {
+ return $d && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
+ };
+ $date_start = $valid_date($date_start) ? $date_start : '';
+ $date_end = $valid_date($date_end) ? $date_end : '';
+
+ $filters = array(
+ 'period' => $period,
+ 'date_start' => $date_start,
+ 'date_end' => $date_end,
+ 'hide_reviewed' => $hide_reviewed,
+ );
+
+ $data['amendements'] = $this->admin_model->get_amendements_list($sort, $direction, $filters);
+ $data['sort'] = $sort;
+ $data['direction'] = $direction;
+ $data['period'] = $period;
+ $data['date_start'] = $date_start;
+ $data['date_end'] = $date_end;
+ $data['hide_reviewed'] = $hide_reviewed;
+ $data['title'] = 'Liste des amendements';
+ $data['pa_public_url'] = rtrim($_SERVER['POLITIC_ANALYSIS_PUBLIC_URL'] ?? '', '/');
+
+ $data['title_meta'] = 'Amendements - Dashboard | Datan';
+ $data['breadcrumb'] = array(
+ array('name' => 'Dashboard', 'url' => base_url() . 'admin', 'active' => FALSE),
+ array('name' => 'Amendements', 'url' => base_url() . 'admin/amendements', 'active' => TRUE),
+ );
+
+ $this->load->view('dashboard/header', $data);
+ $this->load->view('dashboard-mp/amendements/index', $data);
+ $this->load->view('dashboard/footer');
+ }
+
+ public function amendements_review()
+ {
+ if ($this->input->method(TRUE) !== 'POST') {
+ show_404();
+ }
+
+ $legislature = (int) $this->input->post('legislature');
+ $voteNumero = (string)$this->input->post('voteNumero');
+ $reviewed = filter_var($this->input->post('reviewed'), FILTER_VALIDATE_BOOLEAN);
+
+ if (!$legislature || !$voteNumero) {
+ $this->output->set_status_header(400)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => 'legislature et voteNumero sont requis']));
+ return;
+ }
+
+ $ok = $this->admin_model->set_amendement_reviewed($legislature, $voteNumero, $reviewed);
+
+ if (!$ok) {
+ $this->output->set_status_header(500)->set_content_type('application/json')
+ ->set_output(json_encode(['error' => "Échec de l'enregistrement (vérifier que la table amendements_ia et sa clé unique uk_leg_vote existent)"]));
+ return;
+ }
+
+ $this->output->set_status_header(200)->set_content_type('application/json')
+ ->set_output(json_encode(['success' => true, 'reviewed' => $reviewed ? 1 : 0]));
+ }
}
?>
diff --git a/application/models/Admin_model.php b/application/models/Admin_model.php
index 52c3bcedf..56ebc1dbe 100644
--- a/application/models/Admin_model.php
+++ b/application/models/Admin_model.php
@@ -242,4 +242,130 @@ public function table_changes($table, $toInsert){
);
$this->db->insert('table_changes', $data);
}
+
+ /**
+ * Liste les votes de type amendement de la dernière législature,
+ * avec résumé IA, score de simplicité et statut de décryptage.
+ *
+ * @param string $sort Colonne de tri : 'date'|'votants'|'disparite'|'simplicite'|'decrypte'
+ * @param string $direction 'ASC'|'DESC'
+ * @param array $filters ['period' => '7'|'30'|'90'|'180'|'365'|'all',
+ * 'date_start' => 'YYYY-MM-DD',
+ * 'date_end' => 'YYYY-MM-DD']
+ */
+ public function get_amendements_list($sort = 'date', $direction = 'DESC', $filters = array())
+ {
+ $allowed_sorts = array(
+ 'date' => 'vi.dateScrutin',
+ 'votants' => 'vi.nombreVotants',
+ 'disparite' => 'disparite',
+ 'interet' => 'interet',
+ 'simplicite' => 'aia.simplicite_ia',
+ 'decrypte' => 'decrypte',
+ );
+
+ $order_col = isset($allowed_sorts[$sort]) ? $allowed_sorts[$sort] : 'vi.dateScrutin';
+ $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC';
+
+ // Dernière législature disponible
+ $last_leg = $this->db->query('SELECT MAX(legislature) AS leg FROM votes_info')->row_array();
+ $legislature = $last_leg['leg'] ?? 17;
+
+ // Filtres date
+ $where_date = '';
+ $params = array($legislature);
+
+ $period = isset($filters['period']) ? (string)$filters['period'] : 'all';
+ $date_start = isset($filters['date_start']) ? (string)$filters['date_start'] : '';
+ $date_end = isset($filters['date_end']) ? (string)$filters['date_end'] : '';
+
+ $is_valid_date = function ($d) {
+ return $d && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d);
+ };
+
+ if ($is_valid_date($date_start) || $is_valid_date($date_end)) {
+ if ($is_valid_date($date_start)) {
+ $where_date .= ' AND vi.dateScrutin >= ?';
+ $params[] = $date_start;
+ }
+ if ($is_valid_date($date_end)) {
+ $where_date .= ' AND vi.dateScrutin <= ?';
+ $params[] = $date_end;
+ }
+ } elseif (in_array($period, array('7', '30', '90', '180', '365'), true)) {
+ $where_date = ' AND vi.dateScrutin >= DATE_SUB(CURDATE(), INTERVAL ? DAY)';
+ $params[] = (int)$period;
+ }
+
+ // Filtre "cacher les reviewed"
+ $where_reviewed = '';
+ if (!empty($filters['hide_reviewed'])) {
+ $where_reviewed = ' AND COALESCE(aia.reviewed, 0) = 0';
+ }
+
+ $sql = "
+ SELECT
+ vi.voteNumero,
+ vi.legislature,
+ vi.dateScrutin,
+ date_format(vi.dateScrutin, '%d/%m/%Y') AS dateScrutinFR,
+ vi.nombreVotants,
+ vi.decomptePour AS pour,
+ vi.decompteContre AS contre,
+ vi.decompteAbs AS abstention,
+ CASE
+ WHEN vi.nombreVotants > 0
+ THEN ROUND(ABS(vi.decomptePour - vi.decompteContre) * 100 / vi.nombreVotants, 1)
+ ELSE 0
+ END AS disparite,
+ CASE
+ WHEN vi.nombreVotants > 0
+ THEN ROUND(
+ LEAST(vi.nombreVotants / 250, 1)
+ * (1 - ABS(vi.decomptePour - vi.decompteContre) / vi.nombreVotants)
+ * 100, 1)
+ ELSE 0
+ END AS interet,
+ aia.titre_ia,
+ aia.resume_ia,
+ aia.simplicite_ia,
+ COALESCE(aia.reviewed, 0) AS reviewed,
+ COALESCE(vd.title, vi.titre, vi.seanceRef) AS titre
+ FROM votes_info vi
+ LEFT JOIN amendements_ia aia
+ ON aia.voteNumero = vi.voteNumero AND aia.legislature = vi.legislature
+ LEFT JOIN votes_datan vd
+ ON vd.voteNumero = vi.voteNumero AND vd.legislature = vi.legislature
+ WHERE vi.voteType IN ('amendement', 'les amen')
+ AND vi.legislature = ?
+ AND vd.id IS NULL
+ $where_date
+ $where_reviewed
+ ORDER BY $order_col $direction
+ ";
+
+ return $this->db->query($sql, $params)->result_array();
+ }
+
+ /**
+ * Marque un amendement comme reviewed (ou non).
+ * Crée la ligne dans amendements_ia si elle n'existe pas encore.
+ *
+ * @return bool true si la requête a réussi, false en cas d'erreur SQL
+ */
+ public function set_amendement_reviewed($legislature, $voteNumero, $reviewed)
+ {
+ $legislature = (int) $legislature;
+ $voteNumero = (string) $voteNumero;
+ $reviewed = $reviewed ? 1 : 0;
+
+ $ok = $this->db->query(
+ "INSERT INTO amendements_ia (legislature, voteNumero, reviewed)
+ VALUES (?, ?, ?)
+ ON DUPLICATE KEY UPDATE reviewed = VALUES(reviewed), updated_at = NOW()",
+ array($legislature, $voteNumero, $reviewed)
+ );
+
+ return $ok !== FALSE;
+ }
}
diff --git a/application/models/Api_key_model.php b/application/models/Api_key_model.php
index 1512c588f..abe806c68 100644
--- a/application/models/Api_key_model.php
+++ b/application/models/Api_key_model.php
@@ -42,6 +42,10 @@ public function get_available_endpoints()
'/api/non_decrypted_votes' => array(
'GET' => 'Lister les votes non décryptés'
),
+ // Résumés IA des amendements (écriture depuis PoliticAnalysis)
+ '/api/amendements_ia' => array(
+ 'POST' => "Créer/mettre à jour un résumé IA d'amendement"
+ ),
// Exposés des motifs
'/api/exposes' => array(
'GET' => 'Lister les exposés',
diff --git a/application/models/DashboardMP_model.php b/application/models/DashboardMP_model.php
index 3d702ea53..03b092c01 100644
--- a/application/models/DashboardMP_model.php
+++ b/application/models/DashboardMP_model.php
@@ -84,7 +84,6 @@ public function get_votes_explained($mpId, $published = NULL){
}
public function get_vote_explained($mpId, $legislature, $voteNumero){
- echo $voteNumero;
$sql = 'SELECT e.id, e.voteNumero, e.legislature, e.text AS explication, vd.title AS vote_titre,
CASE WHEN e.state = 1 THEN "publié" ELSE "brouillon" END AS state,
CASE
diff --git a/application/views/dashboard-mp/amendements/index.php b/application/views/dashboard-mp/amendements/index.php
new file mode 100644
index 000000000..9d60d4401
--- /dev/null
+++ b/application/views/dashboard-mp/amendements/index.php
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
+
+
+
= $title ?>
+
Dernière législature · = count($amendements) ?> amendements
+
+
+
+
+ session->flashdata('flash')) : ?>
+
= $this->session->flashdata('flash') ?>
+
+
+
+
+
+
+
+ Les dates personnalisées (Du / Au) ont priorité sur la période sélectionnée.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/views/dashboard/footer.php b/application/views/dashboard/footer.php
index b96e7b463..6fded5006 100644
--- a/application/views/dashboard/footer.php
+++ b/application/views/dashboard/footer.php
@@ -68,12 +68,24 @@
$(document).ready(function() {
- $('#table_votes_datan').dataTable({
- "order": [[0, "desc"]],
- language: french
- });
-
- $('#table_votes_an').DataTable( {
+ if ($('#table_votes_datan').length && !$.fn.DataTable.isDataTable('#table_votes_datan')) {
+ $('#table_votes_datan').dataTable({
+ "order": [[0, "desc"]],
+ language: french
+ });
+ }
+
+ if ($('#table-amendements').length && !$.fn.DataTable.isDataTable('#table-amendements')) {
+ $('#table-amendements').DataTable({
+ fixedHeader: true,
+ paging: true,
+ order: [[ 5, "desc" ]],
+ language: french,
+ });
+ }
+
+ if ($('#table_votes_an').length && !$.fn.DataTable.isDataTable('#table_votes_an')) {
+ $('#table_votes_an').DataTable( {
dom: 'Bfrtip',
//"lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]],
fixedHeader: true,
@@ -98,11 +110,14 @@
show: ':hidden'
}
]
- } );
-
- $('#table_analyses').DataTable({
- "order": [[0, "desc"]]
- });
+ } );
+ }
+
+ if ($('#table_analyses').length && !$.fn.DataTable.isDataTable('#table_analyses')) {
+ $('#table_analyses').DataTable({
+ "order": [[0, "desc"]]
+ });
+ }
for(let link of $('.nav-treeview .nav-link')){
let searchParams = new URLSearchParams(window.location.search);
diff --git a/application/views/dashboard/header.php b/application/views/dashboard/header.php
index 1a4328691..fb4f29f63 100644
--- a/application/views/dashboard/header.php
+++ b/application/views/dashboard/header.php
@@ -152,6 +152,12 @@
Créer un vote
+
+
+
+ Amendements à décrypter
+
+
diff --git a/conf/000-default.conf b/conf/000-default.conf
index c3675d663..a1cc86dfa 100644
--- a/conf/000-default.conf
+++ b/conf/000-default.conf
@@ -47,6 +47,7 @@
SetEnv API_KEY_MAILJET ${API_KEY_MAILJET}
SetEnv API_KEY_SECRETE_MAILJET ${API_KEY_SECRETE_MAILJET}
SetEnv COMPOSER_AUTOLOAD ${COMPOSER_AUTOLOAD}
+ SetEnv POLITIC_ANALYSIS_PUBLIC_URL ${POLITIC_ANALYSIS_PUBLIC_URL}
diff --git a/package.json b/package.json
index ad4f63338..ff68bb5a3 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"docker-download": "docker exec -it datan php scripts/download.php",
"docker-daily": "docker exec -it datan php scripts/daily.php",
"docker-weekly": "docker exec -it datan php scripts/weekly.php",
+ "docker-ia": "docker exec -it datan php scripts/generate_amendements_ia.php",
"docker-municipales": "docker exec -it datan php scripts/get_electoral_results.php",
"docker-municipales-candidate": "docker exec -it datan php scripts/get_electoral_candidates.php",
"docker-profession": "docker exec -it datan php scripts/profession.php",
diff --git a/sql/amendements_ia.sql b/sql/amendements_ia.sql
new file mode 100644
index 000000000..28e0d47f2
--- /dev/null
+++ b/sql/amendements_ia.sql
@@ -0,0 +1,11 @@
+CREATE TABLE IF NOT EXISTS `amendements_ia` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `legislature` INT NOT NULL,
+ `voteNumero` VARCHAR(20) NOT NULL,
+ `titre_ia` VARCHAR(255) NULL,
+ `resume_ia` TEXT NULL,
+ `simplicite_ia` TINYINT UNSIGNED NULL,
+ `reviewed` TINYINT(1) NOT NULL DEFAULT 0,
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ UNIQUE KEY `uk_leg_vote` (`legislature`, `voteNumero`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/tests/urls.txt b/tests/urls.txt
index ce7376f70..e584f77c2 100644
--- a/tests/urls.txt
+++ b/tests/urls.txt
@@ -73,5 +73,4 @@ http://dev-datan.fr/elections/regionales-2021
http://dev-datan.fr/elections/departementales-2021
http://dev-datan.fr/parrainages-2022
http://dev-datan.fr/outils/coalition-simulateur
-http://dev-datan.fr/blog/categorie/actualite-politique
-http://dev-datan.fr/blog/datan/interview-la-division-du-travail-parlementaire-repose-sur-une-vision-stereotypee-de-la-femme
\ No newline at end of file
+http://dev-datan.fr/blog/categorie/actualite-politique
\ No newline at end of file