<?php
// +-------------------------------------------------+
// © 2002-2004 PMB Services / www.sigb.net pmb@sigb.net et contributeurs (voir www.sigb.net)
// +-------------------------------------------------+
// $Id: demandes_ajax.inc.php,v 1.11.10.1.2.3 2026/08/14 08:40:20 dbellamy Exp $

if (stristr($_SERVER['REQUEST_URI'], ".inc.php")) die("no access");

global $class_path, $include_path, $sub, $action, $quoifaire;
global $id_demande, $idnotice, $object_type;

require_once("$class_path/mono_display.class.php");
require_once("$class_path/demandes.class.php");
require_once("$include_path/templates/demandes_actions.tpl.php");

switch($quoifaire){

	case 'show_list_action':
		list_demandes_actions_demande_ui::set_allow_expand(false);
		$list_demandes_actions_demande_ui = list_demandes_actions_demande_ui::get_instance(array('id_demande' => $id_demande));
		ajax_http_send_response($list_demandes_actions_demande_ui->get_display_list());
		break;
	case 'show_notice':
		show_notice($idnotice);
		break;
	case 'change_read_dmde':
		$demande=new demandes($id_demande,false);
		demandes::change_read($demande);
		ajax_http_send_response(demandes::dmde_propageLu($id_demande));
		break;
	case "get_pperso_form":
		global $type_demande;
		$type_demande = intval($type_demande);
		$demande = new demandes();
		ajax_http_send_response($demande->get_pperso_form_content($id_demande, $type_demande));
		break;
	case "get_allowed_types":
	    global $id_theme;
	    $id_theme = intval($id_theme);
	    $theme = new demandes_theme($id_theme);
	    $allowed_types = $theme->getAllowedTypes();
	    ajax_http_send_response($allowed_types);
	    break;
	case 'get_timeline':
		//Fil de discussion d'une demande, pour l'affichage de la liste en
		//accordeon (parametre demandes_display_mode a 2).
		demandes_ajax_send_json(demandes_ajax_get_timeline($id_demande));
		break;
	case 'add_note':
		//Ajout d'une note depuis l'accordeon, sans quitter la liste.
		demandes_ajax_send_json(demandes_ajax_add_note($id_demande));
		break;
	case 'edit_note':
		//Modification du contenu d'une note depuis l'accordeon.
		demandes_ajax_send_json(demandes_ajax_edit_note($id_demande));
		break;
	case 'toggle_note_flag':
		//Bascule d'un indicateur de note depuis l'accordeon.
		demandes_ajax_send_json(demandes_ajax_toggle_note_flag($id_demande));
		break;
	case 'delete_note':
		//Suppression d'une note depuis l'accordeon.
		demandes_ajax_send_json(demandes_ajax_delete_note($id_demande));
		break;
	case 'delete_file':
		//Suppression d'une piece jointe depuis l'accordeon.
		demandes_ajax_send_json(demandes_ajax_delete_file($id_demande));
		break;
	case 'change_state':
		//Changement d'etat d'une demande depuis l'accordeon, dans les limites du
		//workflow.
		demandes_ajax_send_json(demandes_ajax_change_state($id_demande));
		break;
	case 'change_users':
		//Modification de l'attribution d'une demande depuis l'accordeon.
		demandes_ajax_send_json(demandes_ajax_change_users($id_demande));
		break;

}

switch ($sub) {
	case 'faq_questions':
		switch($action) {
			case "list":
				require_once($class_path.'/demandes/faq_questions_controller.class.php');
				faq_questions_controller::proceed_ajax($object_type, 'demandes');
				break;
		}
		break;
	default:
		switch($action) {
			case "list":
				require_once($class_path.'/demandes/demandes_controller.class.php');
				demandes_controller::proceed_ajax($object_type, 'demandes');
				break;
		}
		break;
}
/*
 * Affichage de la notice
 */
function show_notice($idnotice){


	$isbd = new mono_display($idnotice, 6, '', 1, '', '', '',1);
	$html = "<div class='row' style='padding-top: 8px;'>".$isbd->aff_statut."<h1 style='display: inline;'>".$isbd->header."</h1></div>";
	$html .= "<div class='row'>".$isbd->isbd."</div>";


	print ajax_http_send_response($html);
}

/**
 * Envoie une reponse JSON normalisee en UTF-8.
 *
 * ajax_http_send_response() serialise les tableaux avec json_encode() sans
 * normalisation prealable : on passe donc par encoding_normalize, comme le fait
 * VueJsView pour les donnees initiales de la vue, afin qu'un contenu dans un
 * encodage inattendu ne fasse pas echouer silencieusement la serialisation.
 *
 * @param array $payload
 * @return void
 */
function demandes_ajax_send_json($payload)
{
	header("Content-Type: application/json; charset=utf-8");
	print encoding_normalize::json_encode($payload);
}

/**
 * Construit le fil de discussion d'une demande pour la liste en accordeon.
 *
 * Le fil agrege les chronologies de toutes les actions de la demande
 * (cf demandes_actions::getTimeline()). La demande est instanciee sans
 * chargement paresseux pour que notes et pieces jointes soient disponibles.
 *
 * @param int $id_demande
 * @return array Reponse JSON : ok, groups[]
 */
function demandes_ajax_get_timeline($id_demande)
{
	$id_demande = intval($id_demande);
	if (!$id_demande) {
		return array('ok' => 0);
	}
	$demande = new demandes($id_demande, false);
	if (!$demande->id_demande) {
		return array('ok' => 0);
	}

	$groups = array();
	//Action a laquelle rattacher une nouvelle note : la derniere de la demande,
	//pour prolonger la conversation en cours. Une note est toujours portee par
	//une action : sans action, la saisie est impossible et l'identifiant reste a
	//zero, ce dont la vue se sert pour desactiver le champ de saisie.
	$default_action_id = 0;
	foreach ($demande->actions as $action) {
		if ($action->id_action > $default_action_id) {
			$default_action_id = intval($action->id_action);
		}
		foreach ($action->getTimeline() as $group) {
			$formatted = demandes_ajax_format_timeline_group($group, $action);
			//Un groupe dont il ne reste ni note ni piece jointe (notes vides
			//ecartees) n'afficherait qu'une ligne createur/date isolee.
			if (empty($formatted['notes']) && empty($formatted['files'])) {
				continue;
			}
			$groups[] = $formatted;
		}
	}
	//Les actions sont parcourues dans l'ordre de leurs identifiants : on retrie
	//l'ensemble des groupes sur la date pour presenter une conversation continue.
	usort($groups, function ($a, $b) {
		return strcmp((string) $a['date_raw'], (string) $b['date_raw']);
	});

	return array(
		'ok' => 1,
		'groups' => $groups,
		'default_action_id' => $default_action_id,
	);
}

/**
 * Met en forme un groupe de chronologie pour la reponse JSON.
 *
 * Seules les donnees utiles a l'affichage sont exposees : les objets complets
 * demandes_note et explnum_doc contiennent notamment le binaire des pieces
 * jointes, qui n'a pas a transiter dans la reponse.
 *
 * @param array $group Groupe issu de demandes_actions::getTimeline()
 * @param demandes_actions $action Action porteuse du groupe
 * @return array
 */
function demandes_ajax_format_timeline_group($group, $action)
{
	global $base_path;

	$notes = array();
	foreach ($group['notes'] as $note) {
		//Une note sans contenu n'apporte rien au fil et s'y afficherait comme
		//une bulle vide : on l'ecarte.
		if (trim(strval($note->contenu)) === '') {
			continue;
		}
		$notes[] = array(
			'id' => intval($note->id_note),
			'contenu' => $note->contenu,
			'prive' => intval($note->prive),
			'rapport' => intval($note->rapport),
			//Indicateur brut de la base plutot que son contraire : notes_read_gestion
			//a 1 signifie "a signaler comme nouvelle". L'exposer tel quel evite une
			//double negation entre le stockage, la vue et la bascule.
			'unread' => intval($note->notes_read_gestion),
			//Une note du demandeur ne peut etre ni rendue privee ni versee au
			//rapport documentaire : ces deux notions sont propres a la gestion.
			'is_gestion' => (intval($note->notes_type_user) === 0),
		);
	}

	$files = array();
	foreach ($group['files'] as $file) {
		//Une piece jointe est soit un lien externe (mime a URL), soit un
		//fichier stocke en base et servi par explnum_doc.php.
		if ($file->explnum_doc_mime == 'URL') {
			$url = $file->explnum_doc_url;
		} else {
			$url = $base_path.'/explnum_doc.php?explnumdoc_id='.intval($file->explnum_doc_id);
		}
		$files[] = array(
			'id' => intval($file->explnum_doc_id),
			'nom' => ($file->explnum_doc_nomfichier ? $file->explnum_doc_nomfichier : $file->explnum_doc_url),
			'url' => $url,
		);
	}

	return array(
		'id_action' => intval($action->id_action),
		'sujet_action' => $action->sujet_action,
		'createur' => $group['createur'],
		'date' => ($group['date'] ? formatdate($group['date'], 1) : ''),
		//Date brute conservee pour le tri, l'affichage utilisant 'date'.
		'date_raw' => $group['date'],
		'side' => $group['side'],
		'notes' => $notes,
		'files' => $files,
	);
}

/**
 * Ajoute une note et/ou une piece jointe a une action de la demande, puis
 * renvoie le fil mis a jour.
 *
 * L'un des deux suffit : le formulaire de l'accordeon permet d'envoyer une note
 * seule, un fichier seul, ou les deux d'un meme geste. La note passe par
 * demandes_note::set_properties_from_form() et save(), qui appliquent le meme
 * traitement (dont l'echappement SQL du contenu) que la saisie en pleine page.
 *
 * @param int $id_demande
 * @return array Reponse JSON : ok, et le fil complet en cas de succes
 */
function demandes_ajax_add_note($id_demande)
{
	global $idaction, $contenu_note, $idnote, $iddemande, $date_note;

	$id_demande = intval($id_demande);
	$idaction = intval($idaction);
	$contenu_note = strval($contenu_note ?? '');
	$has_file = !empty($_FILES['f_fichier']) && $_FILES['f_fichier']['error'] != UPLOAD_ERR_NO_FILE;
	if (!$id_demande || !$idaction || (trim($contenu_note) === '' && !$has_file)) {
		return array('ok' => 0);
	}

	$demande = new demandes($id_demande, false);
	//L'action doit appartenir a la demande visee : sans ce controle, un
	//identifiant d'action arbitraire permettrait d'ecrire dans le fil d'une
	//autre demande.
	if (!$demande->id_demande || !isset($demande->actions[$idaction])) {
		return array('ok' => 0);
	}

	$id_note = 0;
	if (trim($contenu_note) !== '') {
		//Cet endpoint ne cree que des notes : on neutralise l'identifiant de
		//note eventuellement transmis, qui permettrait sinon de reecrire
		//n'importe quelle note existante. La date est laissee vide pour que la
		//classe horodate elle-meme la note (la valeur n'est pas echappee a
		//l'insertion).
		$idnote = 0;
		$date_note = '';
		//set_properties_from_form() lit l'identifiant de demande dans $iddemande.
		$iddemande = $id_demande;

		$note = new demandes_note(0, $idaction);
		$note->set_properties_from_form();
		$note->save();
		if (!$note->id_note) {
			return array('ok' => 0);
		}
		$note->fetch_data();
		$id_note = intval($note->id_note);
		//Signale la nouveaute de part et d'autre, comme la saisie en pleine
		//page. Une piece jointe seule ne touche pas ces indicateurs, ni ici ni
		//dans les parcours existants (save_docnum en pleine page, endpoint
		//equivalent de l'Opac).
		$note->note_majParent($id_demande, "_gestion");
		$note->note_majParent($id_demande, "_opac");
	}

	$file_error = 0;
	if ($has_file && !demandes_ajax_save_attachment($idaction)) {
		//Un fichier refuse ne doit pas passer inapercu, meme quand la note a
		//bien ete enregistree : le client distingue les deux cas par cette cle.
		$file_error = 1;
	}

	$timeline = demandes_ajax_get_timeline($id_demande);
	$timeline['file_error'] = $file_error;
	$timeline['id_note'] = $id_note;
	return $timeline;
}

/**
 * Enregistre la piece jointe transmise dans $_FILES['f_fichier'] et la rattache
 * a une action de demande.
 *
 * L'appartenance de l'action a la demande est verifiee par l'appelant. Le
 * fichier est valide avant toute ecriture : extension declaree ET extension
 * reelle deduite du contenu binaire, pour ecarter un fichier renomme. C'est le
 * meme controle que l'endpoint equivalent de l'Opac (ajax_upload_attachment).
 *
 * Le rattachement est ecrit ici plutot que par demandes_actions::save_docnum(),
 * qui lit sur l'objet explnum_doc des proprietes prive et rapport que la classe
 * ne declare pas : les poser depuis ce contexte creerait des proprietes
 * dynamiques, obsoletes depuis PHP 8.2.
 *
 * @param int $idaction
 * @return bool Vrai si la piece jointe a ete enregistree
 */
function demandes_ajax_save_attachment($idaction)
{
	global $PMBuserid;

	$idaction = intval($idaction);
	$file = $_FILES['f_fichier'];
	if ($file['error'] != UPLOAD_ERR_OK || !is_uploaded_file($file['tmp_name'])) {
		return false;
	}

	$content = file_get_contents($file['tmp_name']);
	if ($content === false) {
		return false;
	}
	$declared_extension = \Pmb\Common\Library\Upload\UploadFileValidator::findExtensionWithFile($file['name']);
	$real_extension = \Pmb\Common\Library\Upload\UploadFileValidator::findExtensionWithContent($content);
	if (!\Pmb\Common\Library\Upload\UploadFileValidator::isExtensionAllowed($declared_extension)
		|| !\Pmb\Common\Library\Upload\UploadFileValidator::isExtensionAllowed($real_extension)) {
		return false;
	}

	$explnum_doc = new explnum_doc();
	$explnum_doc->load_file($file);
	$explnum_doc->analyse_file();
	//Depot par un utilisateur de gestion : le fil de discussion place ainsi la
	//piece jointe du cote gestionnaire, et sous son nom.
	$explnum_doc->explnum_doc_num_user = intval($PMBuserid);
	$explnum_doc->explnum_doc_type_user = 0;
	$explnum_doc->save();
	if (!$explnum_doc->explnum_doc_id) {
		return false;
	}

	//Piece jointe visible du demandeur et non rattachable a la notice : ce sont
	//les valeurs du formulaire de saisie de note, ou ces deux cases ne sont pas
	//proposees.
	$query = "REPLACE INTO explnum_doc_actions SET
		num_explnum_doc = '".$explnum_doc->explnum_doc_id."',
		num_action = ".$idaction.",
		prive = 0,
		rapport = 0";
	pmb_mysql_query($query);
	return true;
}

/**
 * Modifie le contenu d'une note, puis renvoie le fil mis a jour.
 *
 * Comme en pleine page (demandes_notes_controller, action save_note), aucune
 * restriction d'auteur : la gestion corrige aussi bien la note d'un collegue
 * que celle du demandeur. Seule l'appartenance a la demande visee est verifiee,
 * l'identifiant etant fourni par le client.
 *
 * @param int $id_demande
 * @return array Reponse JSON : ok, et le fil complet en cas de succes
 */
function demandes_ajax_edit_note($id_demande)
{
	global $idnote, $contenu_note;

	$id_demande = intval($id_demande);
	$idnote = intval($idnote);
	$contenu_note = strval($contenu_note ?? '');
	//Une note sans contenu s'afficherait comme une bulle vide, et le fil
	//l'ecarterait : la saisie en pleine page l'interdit deja
	//(demandes_note_create_ko).
	if (!$id_demande || !$idnote || trim($contenu_note) === '') {
		return array('ok' => 0);
	}

	$demande = new demandes($id_demande, false);
	if (!$demande->id_demande) {
		return array('ok' => 0);
	}

	//La note doit appartenir a une action de cette demande.
	$id_action = 0;
	foreach ($demande->actions as $action) {
		if (isset($action->notes[$idnote])) {
			$id_action = intval($action->id_action);
			break;
		}
	}
	if (!$id_action) {
		return array('ok' => 0);
	}

	//Seul le contenu est reecrit. demandes_note::save() reprendrait toute la
	//note depuis des variables de formulaire absentes - confidentialite,
	//rapport, horodatage, auteur - et forcerait notes_read_opac a 1, signalant
	//de nouveau la note au demandeur pour une simple correction de texte.
	//stripslashes puis addslashes : meme traitement que
	//set_properties_from_form() suivi de save().
	$query = "UPDATE demandes_notes SET contenu = '".addslashes(stripslashes($contenu_note))."'
		WHERE id_note = ".$idnote;
	pmb_mysql_query($query);

	return demandes_ajax_get_timeline($id_demande);
}

/**
 * Bascule un indicateur d'une note, puis renvoie le fil mis a jour.
 *
 * Trois indicateurs sont exposes : confidentialite (prive), rapport
 * documentaire (rapport) et etat lu/non lu du cote gestion
 * (notes_read_gestion). Les deux premiers sont propres a la gestion et sont
 * refuses sur une note du demandeur.
 *
 * La mise a jour est ciblee sur la seule colonne demandee. Passer par
 * demandes_note::save() reecrirait toute la note depuis des variables de
 * formulaire absentes, et forcerait au passage notes_read_opac a 1, ce qui
 * signalerait de nouveau la note au demandeur. Le nom de colonne vient du
 * client : il est resolu par une liste blanche, jamais interpole.
 *
 * @param int $id_demande
 * @return array Reponse JSON : ok, et le fil complet en cas de succes
 */
function demandes_ajax_toggle_note_flag($id_demande)
{
	global $idnote, $note_flag, $note_flag_value;

	$columns = array(
		'prive' => 'prive',
		'rapport' => 'rapport',
		'unread' => 'notes_read_gestion',
	);
	$gestion_only = array('prive', 'rapport');

	$id_demande = intval($id_demande);
	$idnote = intval($idnote);
	$note_flag = strval($note_flag ?? '');
	$note_flag_value = (!empty($note_flag_value) ? 1 : 0);
	if (!$id_demande || !$idnote || !isset($columns[$note_flag])) {
		return array('ok' => 0);
	}

	$demande = new demandes($id_demande, false);
	if (!$demande->id_demande) {
		return array('ok' => 0);
	}

	//La note doit appartenir a une action de cette demande.
	$id_action = 0;
	foreach ($demande->actions as $action) {
		if (isset($action->notes[$idnote])) {
			$id_action = intval($action->id_action);
			break;
		}
	}
	if (!$id_action) {
		return array('ok' => 0);
	}

	$note = new demandes_note($idnote, $id_action);
	if (!$note->id_note) {
		return array('ok' => 0);
	}
	if (in_array($note_flag, $gestion_only) && intval($note->notes_type_user) !== 0) {
		return array('ok' => 0);
	}

	$query = "UPDATE demandes_notes SET ".$columns[$note_flag]." = ".$note_flag_value."
		WHERE id_note = ".$idnote;
	pmb_mysql_query($query);

	if ($note_flag == 'unread') {
		//L'etat lu/non lu se propage a l'action puis a la demande : c'est ce qui
		//commande la mise en evidence dans la liste.
		$note->fetch_data();
		$note->note_majParent($id_demande, "_gestion");
	}

	return demandes_ajax_get_timeline($id_demande);
}

/**
 * Supprime une note de la demande, puis renvoie le fil mis a jour.
 *
 * En gestion, une note du demandeur comme une note d'un collegue peut etre
 * supprimee : c'est le comportement de la saisie en pleine page
 * (demandes_notes_controller, action suppr_note), qui n'applique aucune
 * restriction d'auteur. Seule l'appartenance a la demande visee est verifiee,
 * l'identifiant etant fourni par le client.
 *
 * @param int $id_demande
 * @return array Reponse JSON : ok, et le fil complet en cas de succes
 */
function demandes_ajax_delete_note($id_demande)
{
	global $idnote;

	$id_demande = intval($id_demande);
	$idnote = intval($idnote);
	if (!$id_demande || !$idnote) {
		return array('ok' => 0);
	}

	$demande = new demandes($id_demande, false);
	if (!$demande->id_demande) {
		return array('ok' => 0);
	}

	//La note doit appartenir a une action de cette demande : sans ce controle,
	//un identifiant arbitraire permettrait de supprimer la note d'une autre
	//demande.
	$id_action = 0;
	foreach ($demande->actions as $action) {
		if (isset($action->notes[$idnote])) {
			$id_action = intval($action->id_action);
			break;
		}
	}
	if (!$id_action) {
		return array('ok' => 0);
	}

	$note = new demandes_note($idnote, $id_action);
	demandes_note::delete($note);
	//Pas de recalcul des indicateurs de nouveaute ici : la suppression en pleine
	//page n'en fait pas davantage, et l'etat lu/non lu de chaque demande est de
	//toute facon recalcule a chaque rendu de la liste (dmde_majRead).

	return demandes_ajax_get_timeline($id_demande);
}

/**
 * Supprime une piece jointe de la demande, puis renvoie le fil mis a jour.
 *
 * Le document numerique et son rattachement a l'action sont supprimes par
 * demandes_actions::delete_docnum(), comme en pleine page (action
 * suppr_docnum). L'appartenance a la demande visee est verifiee en base plutot
 * que sur l'objet charge : la jointure est plus directe que le parcours des
 * actions et de leurs pieces jointes.
 *
 * @param int $id_demande
 * @return array Reponse JSON : ok, et le fil complet en cas de succes
 */
function demandes_ajax_delete_file($id_demande)
{
	global $id_explnum_doc;

	$id_demande = intval($id_demande);
	$id_explnum_doc = intval($id_explnum_doc);
	if (!$id_demande || !$id_explnum_doc) {
		return array('ok' => 0);
	}

	$query = "SELECT eda.num_explnum_doc FROM explnum_doc_actions eda
		JOIN demandes_actions da ON da.id_action = eda.num_action
		WHERE eda.num_explnum_doc = ".$id_explnum_doc." AND da.num_demande = ".$id_demande;
	$result = pmb_mysql_query($query);
	if (!pmb_mysql_num_rows($result)) {
		return array('ok' => 0);
	}

	$explnum_doc = new explnum_doc($id_explnum_doc);
	if (!$explnum_doc->explnum_doc_id) {
		return array('ok' => 0);
	}
	demandes_actions::delete_docnum($explnum_doc);

	return demandes_ajax_get_timeline($id_demande);
}

/**
 * Change l'etat d'une demande depuis l'accordeon, dans les limites du workflow.
 *
 * L'etat cible doit figurer parmi les transitions declarees pour l'etat courant
 * (cf includes/demandes/workflow.xml). Cette verification appartient a
 * l'appelant : demandes::change_state() applique l'etat demande sans examiner
 * s'il est atteignable. Elle est ici indispensable, l'endpoint etant sollicite
 * par le client.
 *
 * Le changement d'etat n'est pas une simple ecriture de colonne : il peut
 * attribuer la demande a l'utilisateur courant et creer une action par defaut.
 * Dans ces deux cas, le panneau ne se limite plus a son etat et la reponse
 * demande le rechargement de la page : l'attribution et le fil de discussion ne
 * sont pas rafraichissables seuls, et un fil renvoye ici entrerait en
 * concurrence avec le chargement differe du panneau.
 *
 * @param mixed $id_demande Identifiant transmis par la requete
 * @return array Reponse JSON : ok, reload, etat, etat_name, etat_id, states[]
 */
function demandes_ajax_change_state($id_demande)
{
	global $state;

	$state = intval($state);
	$demande = demandes_ajax_get_demande($id_demande);
	if (!isset($demande) || !$state) {
		return array('ok' => 0);
	}
	$id_demande = $demande->id_demande;
	//Meme condition que la fiche de gestion et que l'affichage du selecteur :
	//sans etat de depart connu, ou sans prise en charge lorsque le parametre
	//demandes_init_workflow l'exige, la demande n'offre aucune action.
	if (empty($demande->etat_demande) || !list_demandes_ui::is_allowed_actions_accordion($demande)) {
		return array('ok' => 0);
	}

	$allowed = false;
	foreach ($demande->workflow->getStateList(intval($demande->etat_demande)) as $allowed_state) {
		if (intval($allowed_state['id']) === $state) {
			$allowed = true;
			break;
		}
	}
	if (!$allowed) {
		return array('ok' => 0);
	}

	//Etat des lieux avant l'operation : change_state() attribue la demande a
	//l'utilisateur courant lorsque le parametre demandes_init_workflow vaut 1 et
	//que personne ne s'en occupe encore, et cree une action reprenant la demande
	//au passage a l'etat valide lorsque demandes_default_action vaut 1.
	$users_before = count($demande->users);
	$actions_before = count($demande->actions);
	$demande->change_state($state);
	$demande->fetch_data($id_demande, false);

	return array(
		'ok' => 1,
		'reload' => (count($demande->users) != $users_before
			|| count($demande->actions) != $actions_before ? 1 : 0),
		'etat_id' => intval($demande->etat_demande),
		'etat' => $demande->workflow->getStateCommentById($demande->etat_demande),
		//Nom technique de l'etat, suffixe de la classe CSS du badge : compose
		//comme le fait l'affichage initial de la liste.
		'etat_name' => strtolower($demande->workflow->getStatesById(intval($demande->etat_demande))),
		'states' => list_demandes_ui::get_allowed_states_accordion($demande),
	);
}

/**
 * Charge une demande designee par la requete, ou rien si elle n'existe pas.
 *
 * Deux precautions que l'instanciation ne prend pas :
 *
 * - un identifiant qui n'est pas une valeur simple est refuse. Un tableau, que
 *   rien n'empeche d'envoyer, vaut 1 apres conversion en entier : la demande
 *   traitee ne serait pas celle que le client croit designer.
 * - l'existence est verifiee en base. demandes::fetch_data() conserve
 *   l'identifiant qu'on lui donne, meme sans ligne correspondante : l'objet
 *   obtenu porte alors un identifiant utilisable en ecriture, sans demande
 *   derriere.
 *
 * @param mixed $id_demande Identifiant transmis par la requete
 * @return demandes|null La demande chargee sans chargement paresseux, ou null
 */
function demandes_ajax_get_demande($id_demande)
{
	if (!is_scalar($id_demande)) {
		return null;
	}
	$id_demande = intval($id_demande);
	if (!$id_demande) {
		return null;
	}
	$query = "SELECT 1 FROM demandes WHERE id_demande = ".$id_demande;
	$result = pmb_mysql_query($query);
	if (!pmb_mysql_num_rows($result)) {
		return null;
	}
	return new demandes($id_demande, false);
}

/**
 * Modifie l'attribution d'une demande depuis l'accordeon.
 *
 * L'enregistrement est celui du formulaire de la demande
 * (cf demandes::save_attribution(), qui appelle save_demandes_users()) : les
 * gestionnaires retires conservent leur ligne avec users_statut a 0, et une
 * selection vide retire tout le monde.
 *
 * Les identifiants recus sont confrontes aux utilisateurs habilites : le
 * selecteur ne propose que ceux-la, et l'endpoint etant sollicite par le client,
 * la verification ne peut pas lui etre laissee. Un identifiant hors liste fait
 * echouer l'operation entiere plutot que d'etre ecarte en silence.
 *
 * A la difference du changement d'etat, l'operation n'est pas soumise a la
 * condition du bloc d'actions : avec le parametre demandes_init_workflow a 2,
 * c'est l'attribution qui ouvre le traitement d'une demande.
 *
 * @param mixed $id_demande Identifiant transmis par la requete
 * @return array Reponse JSON : ok, reload, attribution, users[]
 */
function demandes_ajax_change_users($id_demande)
{
	global $dmde_users;

	$demande = demandes_ajax_get_demande($id_demande);
	if (!isset($demande)) {
		return array('ok' => 0);
	}
	$id_demande = $demande->id_demande;

	//Aucun gestionnaire transmis : l'attribution est retiree. Le champ est absent
	//de la requete dans ce cas, le navigateur n'envoyant pas un selecteur vide.
	//Une valeur inexploitable fait echouer l'operation plutot que d'etre ecartee :
	//le selecteur n'envoie que des identifiants, et retirer silencieusement toute
	//attribution parce que la requete est malformee serait pire que ne rien faire.
	$users = array();
	foreach ((is_array($dmde_users) ? $dmde_users : array()) as $user) {
		$id_user = intval($user);
		if ($id_user <= 0) {
			return array('ok' => 0);
		}
		if (!in_array($id_user, $users)) {
			$users[] = $id_user;
		}
	}
	$assignable = array();
	foreach (list_demandes_ui::get_assignable_users_accordion() as $assignable_user) {
		$assignable[] = $assignable_user['id'];
	}
	foreach ($users as $id_user) {
		if (!in_array($id_user, $assignable)) {
			return array('ok' => 0);
		}
	}

	//Etat des lieux avant l'operation : avec le parametre demandes_init_workflow a
	//2, prendre en charge une demande ouvre son bloc d'actions et son selecteur
	//d'etat, et les rendre ne peut pas se faire sans recharger la page ; retirer
	//toute attribution les referme de meme.
	$actions_allowed_before = list_demandes_ui::is_allowed_actions_accordion($demande);
	$demande->save_attribution($users);
	$demande->fetch_data($id_demande, false);

	return array(
		'ok' => 1,
		'reload' => (list_demandes_ui::is_allowed_actions_accordion($demande) != $actions_allowed_before ? 1 : 0),
		//Libelle compose comme la colonne Attribution du tableau : la vue le
		//reprend tel quel dans les donnees de la demande.
		'attribution' => list_demandes_ui::get_attribution_label($demande),
		'users' => list_demandes_ui::get_object_users_accordion($demande),
	);
}
?>