Разработка системы обновления ПО холдинга часть 2. Web-интерфейс.

Итак, бэкэнд был разработан. Пришло время придать форму приложению. Учитывая не обычную структуру, было решено не использовать checkbox-элементы, т.к. они не могут в полной мере описать структуру дерева. Решил запилить свою собственную форму.

В итоге вышло что-то вроде:

Сам интерфейс вытягивается ajax-oм в окошко. В верхней части интерфеса есть input с наименованием файла(только чтение), в него при клике на название софта в таблице(внизу) или при выборе файла(вверху) пишется имя файла. Второй input(доступен для записи) — папка в которую нужно будет положить файл на клиенте. Выбор файла, при выборе которого собственно обновляется верхняя форма и там появляется кнопка загрузить(с подключенной функцией в javascript):

Снизу таблица, которая полностью соответствует данным в MySQL(из них и вытягивается). Над этой таблицей кнопка для ручной выгрузки в MongoDB. При клике на название программы в таблице в первый(заблокированный) input вытягивается имя программы и верхняя форма обновляется: появляется две кнопки «удалить» и «применить» с соответствующими подключенными функциями:

Посередине дерево, у каждого элемента есть псевдо-checkbox. При этом он у верхних двух уровней имеет три положения:
1)пустое окно — элемент не выбран(нижестоящие уровни пусты).
2)окно с плюсом — раскрыт список дерева, сам элемент не выбран(нижестоящие уровни доступны к редактированию).
3)окно с флагом — выбран элемент и все нижестоящие уровни(список скрыт).
Сами значки нарисовал в Adobe Photoshop, т.к. в интернете ничего подходящего не нашел.
Выбор элементов асинхронным XmlHttpRequest пишет в базу при клике значение, по факту записи вторым асинхронным запросом уже вытягивается информация в нижнюю таблицу.
При этом, если первый input(с именем файла) пуст — псевдо-checkbox возвращает сообщение, что не выбран софт:

При попытке загрузить файл, который уже существует — обновляется только файл и содержимое ячейки hash в MySQL. Соответствующее сообщение также выводится:

Аналогичным сообщением подтверждается обновление и удаление файла(ответ идет не от javascript, php, а напрямую из MySQL, исключая обновление «только файла»), а также весь функционал по добавлению и редактированию объектов(функционал легко расширяем):

При этом, при выборе элемента списка, обновляется форма ввода с примером:

При выгрузке в MongoDB — из NoSQL базы возвращается ответ о количестве результате операций:

Ну и касательно собственно кода, вытащим параметры пользователя из БД соответственно поднятой сессии и тут же закроем соединение, оставив нужные данные в переменных. Далее подключимся к MySQL еще раз, уже для работы приложения:

$mysqli = new mysqli("*******", "*****", "********", "update_soft");
		if ($mysqli->connect_errno) {
			echo "Не удалось подключиться к MySQL:".$mysqli->connect_error; 
			exit;
			}			
$mysqli->query("SET NAMES utf8");
 
//для отправки в iRetail
if($_GET['obj'] == '**********'){
     //об этом читайте в другой части 4
}

Ту часть, что отправляет в cloud умышленно сделано GET-запросом, чтобы можно было отправить по клику, из crona, curlom и т.д. Далее идет проверка прав пользователя на выполнение обновления. Она должна быть после отправки в облако, иначе это будет невозможно сделать из консоли.

if($status_user[2] < 5) die ('Недостаточно прав');

Далее создадим основной текст документа и чуть ниже его разберем. Основной документ выводится, если $_GET[‘obj’] не существует:

Показать

//основной документ
if(!isset($_GET['obj'])){
	echo '
	<style type="text/css">
	.jurlics{
		padding: 0px 0px 0px 12px;
	}
	.objects{
		padding: 0px 0px 0px 20px;
	}
	#inputfile{
		padding: 0px 0px 12px 0px;
	}
	#softwares{
		padding: 12px 0px 0px 0px;
	}
	table {border: 1px solid grey;} 
	th {border: 1px solid grey;}
	td {border: 1px solid grey;}
	#objecteditor{
		padding: 8px 0px 0px 0px;
	}
	#process_procedure{
		padding: 5px 0px 0px 0px;
	}
	#process_primer{
		padding: 3px 0px 0px 0px;
		font-size: 8pt;
		color: gray;
	}
	#O_object, #O_jurlico, #H_holding, #H_jurlico, #G_selector, #G_toval{
		width: 150px;
	}
	#O_guid, #G_guid{
		width: 500px;
	}
	#softpath{
		width: 300px;
	}
	#softname{
		width: 300px;
	}
	ins, button, select, .holdings img, .jurlics img, .objects img{
		cursor: pointer;
	}
	#for_script_object, #for_script_jurlico, #for_script_holding, #for_script_noholdjurlico{
		visibility: hidden;
		width: 0px;
		height: 0px;
	}
	</style>
 
	<script> 
	function MyListSelect(op, obj, objval) {
		softname = document.getElementById("softname").value;
		var object_guid, jurlico_guid, holding_guid;
		if(document.getElementById("for_script_holding")){
			holding_guid = JSON.parse(document.getElementById("for_script_holding").innerHTML.toString())[objval];
		}
		if(document.getElementById("for_script_jurlico")){
			jurlico_guid = JSON.parse(document.getElementById("for_script_jurlico").innerHTML.toString())[objval];
		}
		if((jurlico_guid == undefined) || (jurlico_guid == null) || (jurlico_guid == "")){ 
			if(document.getElementById("for_script_noholdjurlico")){
				jurlico_guid = JSON.parse(document.getElementById("for_script_noholdjurlico").innerHTML.toString())[objval]; 
			}
		}
		if(document.getElementById("for_script_object")){
			object_guid = JSON.parse(document.getElementById("for_script_object").innerHTML.toString())[objval];
		}
 
		if((softname != null) & (softname != "")) {
			if (op == 0){ //флаг не стоит
				xmlhttp=new XMLHttpRequest();
				xmlhttp.onreadystatechange=function() {
					if( obj == "holding") {
						if (this.readyState==4 && this.status==200) {
							document.getElementById("H." + objval).innerHTML = "";
							document.getElementById("HC." + objval).innerHTML = "<img src=\"/update_apt/unselect.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(1, \"holding\", \\"" + objval + "\\");\'>" + objval + " - {" + holding_guid +"}";
							LoadSoftTable();
						}
					}
					if( obj == "jurlico") {
						if (this.readyState==4 && this.status==200) {
							document.getElementById("J." + objval).innerHTML = "";
							document.getElementById("JC." + objval).innerHTML = "<img src=\"/update_apt/unselect.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(1, \"jurlico\", \\"" + objval + "\\");\'>" + objval + " - {" + jurlico_guid +"}";
							LoadSoftTable();
						}
					}
					if( obj == "object") {
						if (this.readyState==4 && this.status==200) {
							document.getElementById("OC." + objval).innerHTML = "<img src=\"/update_apt/unselect.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(2, \"object\", \\"" + objval + "\\");\'>" + objval + " - {" + object_guid +"}";
							LoadSoftTable();
						}
					}
				}
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_soft_to&param1=remove&param2=" + obj + "&param3=" + objval + "&param4=" + softname, true);
				xmlhttp.send(); 
			}
			if (op == 1){ //раскрытый список
				xmlhttp=new XMLHttpRequest();
				xmlhttp.onreadystatechange=function() {
					if (this.readyState==4 && this.status==200) {
						if( obj == "holding") {
							document.getElementById("H." + objval).innerHTML = xmlhttp.responseText;
							document.getElementById("HC." + objval).innerHTML = "<img src=\"/update_apt/plus.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(2, \"holding\", \\"" + objval + "\\");\'>" + objval + " - {" + holding_guid +"}";
						}
						if( obj == "jurlico") {
							document.getElementById("J." + objval).innerHTML = xmlhttp.responseText;
							document.getElementById("JC." + objval).innerHTML = "<img src=\"/update_apt/plus.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(2, \"jurlico\", \\"" + objval + "\\");\'>" + objval + " - {" + jurlico_guid +"}";
						}
					}
				}
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=" + obj + "&objval=" + objval + "&soft_in_table=" + document.getElementById("softname").value, true);
				xmlhttp.send(); 
			}
			if (op == 2){ //флаг стоит
				xmlhttp=new XMLHttpRequest();
				xmlhttp.onreadystatechange=function() {
					if( obj == "holding") {
						if (this.readyState==4 && this.status==200) {
							document.getElementById("H." + objval).innerHTML = "";
							document.getElementById("HC." + objval).innerHTML = "<img src=\"/update_apt/select.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(0, \"holding\", \\"" + objval + "\\");\'>" + objval + " - {" + holding_guid +"}";
							LoadSoftTable();
						}
					}
					if( obj == "jurlico") {
						if (this.readyState==4 && this.status==200) {
							document.getElementById("J." + objval).innerHTML = "";
							document.getElementById("JC." + objval).innerHTML = "<img src=\"/update_apt/select.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(0, \"jurlico\", \\"" + objval + "\\");\'>" + objval + " - {" + jurlico_guid +"}";
							LoadSoftTable();
						}
					}
					if( obj == "object") {
						if (this.readyState==4 && this.status==200) {
							document.getElementById("OC." + objval).innerHTML = "<img src=\"/update_apt/select.png\" width=\"12\" height=\"12\" onClick=\'MyListSelect(0, \"object\", \\"" + objval + "\\");\'>" + objval + " - {" + object_guid +"}";
							LoadSoftTable();
						}
					}
				}
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_soft_to&param1=add&param2=" + obj + "&param3=" + objval + "&param4=" + softname, true);
				xmlhttp.send(); 
			}
		} else {
			alert("Не выбрано ПО!");
		}
	}
 
	function LoadSoftWare(name, path) { 
		document.getElementById("softname").value = name;
		document.getElementById("softpath").value = path;
		document.getElementById("buttons").innerHTML = "<button onClick=\"EditSoft(\'update\');\">Применить</button> <button onClick=\"EditSoft(\'remove\');\">Удалить</button>";
		LoadObjectTable(name);
	}
 
	function LoadSoftTable(){ 
		xmlhttp=new XMLHttpRequest();
		xmlhttp.onreadystatechange=function() {
			if (this.readyState==4 && this.status==200) {
				document.getElementById("softwares").innerHTML = xmlhttp.responseText;
			}
		}
		xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=loadsofttable", false);
		xmlhttp.send();
	}
 
	function LoadSoftFile(){
		arr = String(document.getElementById("softfile").value).split("\\\");
		document.getElementById(\'softname\').value = arr[arr.length - 1]; 
		document.getElementById("softpath").value = "";
		document.getElementById("buttons").innerHTML = "<button onClick=\"EditSoft(\'add\');\">Загрузить</button>";
	}
 
	function EditSoft(param){
		xmlhttp=new XMLHttpRequest();
		xmlhttp.onreadystatechange=function() {
			if (this.readyState==4 && this.status==200) {
				alert(xmlhttp.responseText);
				LoadSoftTable();
				if((param == "add") || (param == "remove")){
					LoadObjectTable("all");
					document.getElementById("softname").value = "";
					document.getElementById("softpath").value = "";
					document.getElementById("softfile").value = "";
				}
			}
		}
		if(param == "add"){
			xmlhttp.open("POST","/update_apt/update_soft.php?obj=command&command=proc_software&param1=" + param + "&param2=" + document.getElementById("softname").value + "&param3=" + document.getElementById("softpath").value + "&param4=/home/new_update_soft/", true);
			form=new FormData();
			upload_file=document.getElementById("softfile").files[0];
			form.append("softfile", upload_file);
			xmlhttp.send(form);
		}
		else {
			xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_software&param1=" + param + "&param2=" + document.getElementById("softname").value + "&param3=" + document.getElementById("softpath").value + "&param4=/home/new_update_soft/", true);
			xmlhttp.send();
		} 
	}
 
	function SelectProc(){
		var selected = document.getElementById("process_selector").value;
		switch(selected){
			case "proc_holding_add":
				document.getElementById("process_procedure").innerHTML = "<input id=\"H_holding\" type=\"text\" value=\"Холдинг\"> <input id=\"H_jurlico\" type=\"text\" value=\"Юрлицо\">";
				document.getElementById("process_primer").innerHTML = "ПРИМЕР: Холдинг: farmin, Юрлицо: farmin";
			break;
			case "proc_holding_remove":
				document.getElementById("process_procedure").innerHTML = "<input id=\"H_holding\" type=\"text\" value=\"Холдинг\">";
				document.getElementById("process_primer").innerHTML = "ПРИМЕР: Холдинг: farmin";
			break;
			case "proc_object_add":
				document.getElementById("process_procedure").innerHTML = "<input id=\"O_object\" type=\"text\" value=\"Объект\"> <input id=\"O_jurlico\" type=\"text\" value=\"Юрлицо\"> <input id=\"O_guid\" type=\"text\" value=\"GUID объекта\">";
				document.getElementById("process_primer").innerHTML = "ПРИМЕР: Объект: farmin.apt01, Юрлицо: farmin, GUID: 960E4C49-EF07-11E2-AF2D-782BCB1FF45D";
			break;
			case "proc_object_remove":
				document.getElementById("process_procedure").innerHTML = "<input id=\"O_object\" type=\"text\" value=\"Объект\">";
				document.getElementById("process_primer").innerHTML = "ПРИМЕР: Объект: farmin.apt01";
			break;
			case "proc_object_update":
				document.getElementById("process_procedure").innerHTML = "<input id=\"O_object\" type=\"text\" value=\"Объект\"> <input id=\"O_jurlico\" type=\"text\" value=\"Юрлицо\"> <input id=\"O_guid\" type=\"text\" value=\"GUID объекта\">";
				document.getElementById("process_primer").innerHTML = "ПРИМЕР: Объект: farmin.apt01, Юрлицо: farmin, GUID: 960E4C49-EF07-11E2-AF2D-782BCB1FF45D";
			break;
			case "proc_guid_to":
				document.getElementById("process_procedure").innerHTML = "<select id=\"G_selector\" size=\"1\"><option value=\"holding\">Холдинг</option><option value=\"jurlico\">Юрлицо</option></select> <input id=\"G_toval\" type=\"text\" value=\"Холдинг или юрлицо\"> <input id=\"G_guid\" type=\"text\" value=\"GUID организации\">";
				document.getElementById("process_primer").innerHTML = "ПРИМЕР: Холдинг: farmin, GUID: 960E4C49-EF07-11E2-AF2D-782BCB1FF45D";
			break;
			default:
				alert( "Произошла исключительная ситуация!" );
		}
	}
 
	function EditObject(){
		var selected = document.getElementById("process_selector").value;
		xmlhttp=new XMLHttpRequest();
		xmlhttp.onreadystatechange=function() {
			if (this.readyState==4 && this.status==200) {
				alert(xmlhttp.responseText);
				LoadSoftTable();
				LoadObjectTable("all");
				document.getElementById("softname").value = "";
				document.getElementById("softpath").value = "";
				document.getElementById("softfile").value = "";
			}
		}
		switch(selected){
			case "proc_holding_add":
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_holding&param1=add&param2=" + document.getElementById("H_holding").value + "&param3=" + document.getElementById("H_jurlico").value, true);
			break;
			case "proc_holding_remove":
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_holding&param1=remove&param2=" + document.getElementById("H_holding").value, true);
			break;
			case "proc_object_add":
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_object&param1=add&param2=" + document.getElementById("O_object").value + "&param3=" + document.getElementById("O_guid").value + "&param4=" + document.getElementById("O_jurlico").value, true);
			break;
			case "proc_object_remove":
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_object&param1=remove&param2=" + document.getElementById("O_object").value, true);
			break;
			case "proc_object_update":
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_object&param1=update&param2=" + document.getElementById("O_object").value + "&param3=" + document.getElementById("O_guid").value + "&param4=" + document.getElementById("O_jurlico").value, true);
			break;
			case "proc_guid_to":
				xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=proc_guid_to&param1=" + document.getElementById("G_selector").value + "&param2=" + document.getElementById("G_toval").value + "&param3=" + document.getElementById("G_guid").value, true);
			break;
			default:
				alert( "Произошла исключительная ситуация!" );
		}
		xmlhttp.send();
	}
 
	function LoadObjectTable(soft_in_table){ 
		xmlhttp=new XMLHttpRequest();
		xmlhttp.onreadystatechange=function() {
			if (this.readyState==4 && this.status==200) {
				document.getElementById("objecttable").innerHTML = xmlhttp.responseText;
			}
		}
		xmlhttp.open("GET","/update_apt/update_soft.php?obj=command&command=loadobjecttable&soft_in_table=" + soft_in_table, true);
		xmlhttp.send();
	}
 
	function SendToCloud(){
		xmlhttp=new XMLHttpRequest();
		xmlhttp.onreadystatechange=function() {
			if (this.readyState==4 && this.status==200) {
				alert(xmlhttp.responseText);
			}
		}
		xmlhttp.open("GET","/update_apt/update_soft.php?obj=sendtocloud", true);
		xmlhttp.send();
	}
	</script>';
	echo '<div id="inputfile"><input id="softname" type="text" disabled> <input id="softpath" type="text"> <input id="softfile" name="software" type="file" onchange="LoadSoftFile();"> <div id="buttons"></div> </div><hr>';
	echo '<div id="objecttable">';
		GetObject($mysqli, 1, 'all');
	echo '</div>';
	echo '<div id="objecteditor"><select id="process_selector" size="1" onchange="SelectProc();">
		<option value="proc_holding_add">Добавить юрлицо к холдингу</option>
		<option value="proc_holding_remove">Удалить холдинг</option>
		<option value="proc_guid_to">Обновить GUID организации</option>
		<option value="proc_object_add" selected>Добавить объект</option>
		<option value="proc_object_remove">Удалить объект</option>
		<option value="proc_object_update">Обновить объект</option>
		</select> <button onClick="EditObject();">Выполнить</button></div>';
	echo '<div id="process_procedure"><input id="O_object" type="text" value="Объект"> <input id="O_jurlico" type="text" value="Юрлицо"> <input id="O_guid" type="text" value="GUID объекта"></div>';
	echo '<div id="process_primer">ПРИМЕР: Объект: farmin.apt01, Юрлицо: farmin, GUID: 960E4C49-EF07-11E2-AF2D-782BCB1FF45D</div>';
	echo '<hr>';
	echo '<div id="sendtocloud"><button onClick="SendToCloud();">Выгрузить в iRetail</button></div>';
	echo '<div id="softwares">';
		GetSoft($mysqli, 1);
	echo '</div>';
}

Для придания красоты нашей форме псевдо-checkbox-ов нам необходимо при клике на программу показывать выбранные торговые объекты, юрлица, холдинги с флагом, а не выбранные с пустым квадратом. Т.к. форма у нас генерируется динамически в момент открытия странички, придется её менять теневыми запросами к бэкэнду. Учитывая, что при изменении элементов DOM скрипт уже не может отслеживать их состояние, то нужно привязывать предопределенные функции к событиям новых элементов.
В итоге я написал функцию для выгрузки верхнего уровня дерева(холдинги, юрлица вне холдингов) при получении параметра 1 отдаст по умолчанию, с параметром 2 отдаст с хидером(нужен для XHR):

Показать

//верхний уровень дерева
function GetObject($mysqli, $param, $soft_inobject){
	if($param == 2){
		header("Content-type: text/txt; charset=utf-8");
	}
	if ($result = $mysqli->query("SELECT * FROM `guid_for_jur_hold` WHERE `holding` IS NOT NULL ORDER BY `holding`")) {
		while( $row = $result->fetch_assoc() ){
			$link_guid_h[$row['holding']] = $row['guid'];
			$for_script .= '"'.$row['holding'].'" : "'.$row['guid'].'",';
		}
		echo '<div id="for_script_holding">{'.substr($for_script, 0, -1).'}</div>';
	}
	$for_script = null;
	if ($result = $mysqli->query("SELECT * FROM `guid_for_jur_hold` WHERE `jurlico` IS NOT NULL ORDER BY `jurlico`")) {
		while( $row = $result->fetch_assoc() ){
			$link_guid_j[$row['jurlico']] = $row['guid'];
			$for_script .= '"'.$row['holding'].'" : "'.$row['guid'].'",';
		}
	}
	echo '<div id="for_script_noholdjurlico">{'.substr($for_script, 0, -1).'}</div>';
	if($soft_inobject == 'all'){
		if ($result = $mysqli->query('SELECT DISTINCT `holding` FROM `link_holding`')) {
			while( $row = $result->fetch_assoc() ){ 
					echo '<div class="holdings" id="HC.'.$row['holding'].'"><img src="/update_apt/unselect.png" width="12" height="12" onClick=\'MyListSelect(1, "holding","'.$row['holding'].'");\'>'.$row['holding'].' - {'.$link_guid_h[$row['holding']].'}</div><div id="H.'.$row['holding'].'"></div>';
				}
			$result->close(); 
		}		
		if ($result = $mysqli->query('SELECT DISTINCT `jurlico` FROM `link_jurlico` WHERE `jurlico` NOT IN (SELECT `jurlico` FROM `link_holding`) ORDER BY Id')) {
			echo '<div class="holdings">вне холдингов</div>';
			while( $row = $result->fetch_assoc() ){ 
					echo '<div class="jurlics" id="JC.'.$row['jurlico'].'"><img src="/update_apt/unselect.png" width="12" height="12" onClick=\'MyListSelect(1, "jurlico","'.$row['jurlico'].'");\'>'.$row['jurlico'].' - {'.$link_guid_j[$row['jurlico']].'}</div><div id="J.'.$row['jurlico'].'"></div>';
				}
			$result->close(); 
		}
	} else{
		if(isset($soft_inobject)){
			$holding_db = GetArraySoft($mysqli, 'holding', $soft_inobject);
			$jurlico_db = GetArraySoft($mysqli, 'jurlico', $soft_inobject);
			if ($result = $mysqli->query('SELECT DISTINCT `holding` FROM `link_holding`')) {
				while( $row = $result->fetch_assoc() ){
					if(in_array($row['holding'], $holding_db)){
						echo '<div class="holdings" id="HC.'.$row['holding'].'"><img src="/update_apt/select.png" width="12" height="12" onClick=\'MyListSelect(0, "holding","'.$row['holding'].'");\'>'.$row['holding'].' - {'.$link_guid_h[$row['holding']].'}</div><div id="H.'.$row['holding'].'"></div>';
					} else {
						echo '<div class="holdings" id="HC.'.$row['holding'].'"><img src="/update_apt/unselect.png" width="12" height="12" onClick=\'MyListSelect(1, "holding","'.$row['holding'].'");\'>'.$row['holding'].' - {'.$link_guid_h[$row['holding']].'}</div><div id="H.'.$row['holding'].'"></div>';
					}
				}
				$result->close(); 
			}		
			if ($result = $mysqli->query('SELECT DISTINCT `jurlico` FROM `link_jurlico` WHERE `jurlico` NOT IN (SELECT `jurlico` FROM `link_holding`) ORDER BY Id')) {
				echo '<div class="holdings">вне холдингов</div>';
				while( $row = $result->fetch_assoc() ){ 
					if(in_array($row['jurlico'], $jurlico_db)){
						echo '<div class="jurlics" id="JC.'.$row['jurlico'].'"><img src="/update_apt/select.png" width="12" height="12" onClick=\'MyListSelect(0, "jurlico","'.$row['jurlico'].'");\'>'.$row['jurlico'].' - {'.$link_guid_j[$row['jurlico']].'}</div><div id="J.'.$row['jurlico'].'"></div>';
					} else {
						echo '<div class="jurlics" id="JC.'.$row['jurlico'].'"><img src="/update_apt/unselect.png" width="12" height="12" onClick=\'MyListSelect(1, "jurlico","'.$row['jurlico'].'");\'>'.$row['jurlico'].' - {'.$link_guid_j[$row['jurlico']].'}</div><div id="J.'.$row['jurlico'].'"></div>';
					}
				}
				$result->close(); 
			}
		} 
	}
}

Этой функции необходимо получить список связей холдинга с софтом и юрлица с софтом, понимая что в дальнейшем такая же информация будет нужна и по объектам, оформил в отдельную функцию, которая получив софт и параметр вернет массив холдингов, юрлиц или объектов к которым привязан софт:

Показать

//получить объекты по софту
function GetArraySoft($mysqlid, $to_arr, $to_soft) {
	$i=0;$j=0;$k=0;
	$result = $mysqlid->query("SELECT `name` FROM `holding` WHERE `soft` LIKE '".$to_soft."' ORDER BY Id");
	while( $row = $result->fetch_assoc() ){ 
		$holding_db[$i] = $row['name'];
		$result2 = $mysqlid->query("SELECT `jurlico` FROM `link_holding` WHERE `holding` LIKE '".$row['name']."' ORDER BY Id");
		while( $row2 = $result2->fetch_assoc() ){ 
			$jurlico_db[$j] = $row2['jurlico'];
			$result3 = $mysqlid->query("SELECT `object` FROM `link_jurlico` WHERE `jurlico` LIKE '".$row2['jurlico']."' ORDER BY Id");
			while( $row3 = $result3->fetch_assoc() ){ 
				$object_db[$k] = $row3['object'];
				$k++;
			} 
			$result3->close();
			$j++;
		} 
		$result2->close(); 
		$i++;
	}
	$result->close();
 
	if($to_arr == 'holding'){
		return $holding_db;
	}
 
	$result = $mysqlid->query("SELECT `name` FROM `jurlico` WHERE `soft` LIKE '".$to_soft."' ORDER BY Id");
	while( $row = $result->fetch_assoc() ){ 
		$jurlico_db[$j] = $row['name'];
		$result2 = $mysqlid->query("SELECT `object` FROM `link_jurlico` WHERE `jurlico` LIKE '".$row['name']."' ORDER BY Id");
		while( $row2 = $result2->fetch_assoc() ){ 
			$object_db[$k] = $row2['object'];
			$k++;
		} 
		$result2->close(); 
		$j++;
	}
	$result->close(); 
 
	if($to_arr == 'jurlico'){
		return $jurlico_db;
	}
 
	$result = $mysqlid->query("SELECT `name` FROM `object` WHERE `soft` LIKE '".$to_soft."' ORDER BY Id");
	while( $row = $result->fetch_assoc() ){ 
		$object_db[$k] = $row['name'];
		$k++;
	}
	$result->close(); 
 
	if($to_arr == 'object'){
		return $object_db;
	}
}

Также при загрузке используется функция по генерации таблицы с софтом внизу интерфейса(аналогично с параметром):

Показать

//таблица с ПО внизу(высасывает актуальные данные из БД)
function GetSoft($mysqliс, $param){
	if($param == 2){
		header("Content-type: text/txt; charset=utf-8");
	}
 
	if ($result = $mysqliс->query('SELECT `name`, `soft` FROM `holding` ORDER BY `soft`')) {
		while( $row = $result->fetch_assoc() ){ 
				$soft_holding[$row['soft']] .= $row['name'].';';
			}
		$result->close(); 
	}	
	if ($result = $mysqliс->query('SELECT `name`, `soft` FROM `jurlico` ORDER BY `soft`')) {
		while( $row = $result->fetch_assoc() ){ 
				$soft_jurlico[$row['soft']] .= $row['name'].';';
			}
		$result->close(); 
	}	
	if ($result = $mysqliс->query('SELECT `name`, `soft` FROM `object` ORDER BY `soft`')) {
		while( $row = $result->fetch_assoc() ){ 
				$soft_object[$row['soft']] .= $row['name'].';';
			}
		$result->close(); 
	}
 
	if ($result = $mysqliс->query('SELECT * FROM `software` ORDER BY Id')) {
		echo '<table><tr><th>ПО</th><th>папка</th><th>холдинги</th><th>юрлица</th><th>объекты</th></tr>';
		while( $row = $result->fetch_assoc() ){ 
				echo '<tr><td onClick=\'LoadSoftWare("'.$row['name'].'", "'.$row['path'].'");\'><ins>'.$row['name'].'</ins></td>';
				echo '<td>'.$row['path'].'</td>';
				echo '<td>'.$soft_holding[$row['name']].'</td>';
				echo '<td>'.$soft_jurlico[$row['name']].'</td>';
				echo '<td>'.$soft_object[$row['name']].'</td>';
				echo '</tr>';
			}
		$result->close();
		echo '</table>';
	} 
}

Javascript в странице вытягивает также нижний и средний уровень дерева(в функцию оформлять незачем, т.к. обращения будут только через теневой запрос), но эти уровни должны опять таки учитывать связку с софтом, для чего у нас уже есть функция. И сам код:

Показать

//подгрузка объектов для xhr(средний уровень дерева)
if(($_GET['obj'] == 'holding') && isset($_GET['objval']) && isset($_GET['soft_in_table'])){
	header("Content-type: text/txt; charset=utf-8");	
	if ($result = $mysqli->query("SELECT * FROM `guid_for_jur_hold` WHERE `jurlico` IS NOT NULL ORDER BY `jurlico`")) {
		while( $row = $result->fetch_assoc() ){
			$link_guid[$row['jurlico']] = $row['guid'];
			$for_script .= '"'.$row['jurlico'].'" : "'.$row['guid'].'",';
		}
	}
	echo '<div id="for_script_jurlico">{'.substr($for_script, 0, -1).'}</div>';
	if ($result = $mysqli->query("SELECT * FROM `link_holding` WHERE `holding` LIKE '".$_GET['objval']."' ORDER BY `jurlico`")) {
		$jurlico_db = GetArraySoft($mysqli, 'jurlico', $_GET['soft_in_table']);
		while( $row = $result->fetch_assoc() ){ 
			if(in_array($row['jurlico'], $jurlico_db)){
				echo '<div class="jurlics" id="JC.'.$row['jurlico'].'"><img src="/update_apt/select.png" width="12" height="12" onClick=\'MyListSelect(0, "jurlico","'.$row['jurlico'].'");\'>'.$row['jurlico'].' - {'.$link_guid[$row['jurlico']].'}</div><div id="J.'.$row['jurlico'].'"></div>';
			} else {
				echo '<div class="jurlics" id="JC.'.$row['jurlico'].'"><img src="/update_apt/unselect.png" width="12" height="12" onClick=\'MyListSelect(1, "jurlico","'.$row['jurlico'].'");\'>'.$row['jurlico'].' - {'.$link_guid[$row['jurlico']].'}</div><div id="J.'.$row['jurlico'].'"></div>';
			}
		}
		$result->close(); 
	}
}
//подгрузка объектов для xhr(нижний уровень дерева)
if(($_GET['obj'] == 'jurlico') && isset($_GET['objval']) && isset($_GET['soft_in_table'])){
	header("Content-type: text/txt; charset=utf-8");
	if ($result = $mysqli->query("SELECT * FROM `link_object` ORDER BY `object`")) {
		while( $row = $result->fetch_assoc() ){
			$link_guid[$row['object']] = $row['guid'];
			$for_script .= '"'.$row['object'].'" : "'.$row['guid'].'",';
		}
	}
	echo '<div id="for_script_object">{'.substr($for_script, 0, -1).'}</div>';
	$result->close(); 
	if ($result = $mysqli->query("SELECT * FROM `link_jurlico` WHERE `jurlico` LIKE '".$_GET['objval']."' ORDER BY `object`")) {
		$object_db = GetArraySoft($mysqli, 'object', $_GET['soft_in_table']);
		while( $row = $result->fetch_assoc() ){ 
			if(in_array($row['object'], $object_db)){
				echo '<div class="objects"  id="OC.'.$row['object'].'"><img src="/update_apt/select.png" width="12" height="12" onClick=\'MyListSelect(0, "object","'.$row['object'].'");\'>'.$row['object'].' - {'.$link_guid[$row['object']].'}</div>';
			} else {
				echo '<div class="objects"  id="OC.'.$row['object'].'"><img src="/update_apt/unselect.png" width="12" height="12" onClick=\'MyListSelect(2, "object","'.$row['object'].'");\'>'.$row['object'].' - {'.$link_guid[$row['object']].'}</div>';
			}
		}
		$result->close(); 
	}
}

А также опишем функционал работы с базой данных:

Показать

//для работы с процедурами в БД
if($_GET['obj'] == 'command'){
	$log=fopen('/var/log/update_soft/'.date('d.m.Y').'.log', "a+");
	header("Content-type: text/txt; charset=utf-8");
/* эту часть не разрабатываю, т.к. пока без надобности
	if(isset($_GET['command']) && isset($_GET['param1']) && isset($_GET['param2']) && isset($_GET['param3']) && isset($_GET['param4']) && !(preg_match('|^[a-z0-9._-]+$|i', $_GET['command']) && preg_match('|^[a-z0-9._-]+$|', $_GET['param1']) && preg_match('|^[a-z0-9._-]+$|', $_GET['param2']) && preg_match('|^[a-z0-9._-]+$|', $_GET['param3']) && preg_match('|^[a-z0-9._-]+$|', $_GET['param4']))){
		echo 'В тексте есть недопустимые символы!'.PHP_EOL;
		echo 'Разрешены только маленькие английские буквы, цифры, точка, тире и нижнее подчеркивание.';
		$mysqli->close();
		exit;
	}
	if(isset($_GET['command']) && isset($_GET['param1']) && isset($_GET['param2']) && isset($_GET['param3']) && !(preg_match('|^[a-z0-9._-]+$|i', $_GET['command']) && preg_match('|^[a-z0-9._-]+$|', $_GET['param1']) && preg_match('|^[a-z0-9._-]+$|', $_GET['param2']) && preg_match('|^[a-z0-9._-]+$|', $_GET['param3']))){
		if(($_GET['command'] == 'proc_holding') && ($_GET['param1'] == 'add')){ //разрешаем передачу null для proc_holding('add'), для удаления юрлица из объектов холдинга
			if(!isset($_GET['param3']) && isset($_GET['param2']) && preg_match('|^[a-z0-9._-]+$|i', $_GET['param2'])){
				goto NoVerification;
			}
			if(!isset($_GET['param2']) && isset($_GET['param3']) && preg_match('|^[a-z0-9._-]+$|i', $_GET['param3'])){
				goto NoVerification;
			}
		}
		echo 'В тексте есть недопустимые символы!'.PHP_EOL;
		echo 'Разрешены только маленькие английские буквы, цифры, точка, тире и нижнее подчеркивание.';
		$mysqli->close();
		exit;
	} 
	NoVerification: */
	if (($_GET['command'] == 'proc_software') && ($_GET['param1'] == 'add')) {
		fwrite($log, date('H:i:s')." Пользователь ".$login." с IP-адреса ".$_SERVER['REMOTE_ADDR']." выполнил попытку загрузить файл.".PHP_EOL);
		if((!isset($_FILES['softfile'])) || ($_FILES['softfile']["name"] != $_GET['param2'])) {
			echo 'Не выбран файл!';
			fwrite($log, 'Файл не получен!'.PHP_EOL);
			fwrite($log, PHP_EOL);
			fclose($log);
			$mysqli->close();
			exit;
		}
	} 
	if (($_GET['command'] == 'proc_soft_to') || ($_GET['command'] == 'proc_software') || ($_GET['command'] == 'proc_object')){
		fwrite($log, date('H:i:s')." Пользователь ".$login." с IP-адреса ".$_SERVER['REMOTE_ADDR']." выполнил:".PHP_EOL);
		fwrite($log, 'CALL '.$_GET['command'].'("'.$_GET['param1'].'", "'.$_GET['param2'].'", "'.$_GET['param3'].'", "'.$_GET['param4'].'")'.PHP_EOL);
		if($result = $mysqli->query('CALL '.$_GET['command'].'("'.$_GET['param1'].'", "'.$_GET['param2'].'", "'.$_GET['param3'].'", "'.$_GET['param4'].'")')){
			$row = $result->fetch_assoc();
			$Answer = $row['@Answer'];
			fwrite($log, 'Получен ответ: '.$row['@Answer'].PHP_EOL);
			if ($row['@Ctrl'] == 'add') {
				if(copy($_FILES["softfile"]["tmp_name"],'/home/new_update_soft/'.$_FILES['softfile']["name"])){
					$md5 = md5_file('/home/new_update_soft/'.$_FILES['softfile']["name"]);
					if(isset($md5)){
						$mysqli2 = new mysqli("*******", "******", "******", "update_soft");
						if ($mysqli2->connect_errno) {
							echo "Не удалось подключиться к MySQL:".$mysqli2->connect_error; 
							exit;
							}			
						$mysqli2->query("SET NAMES utf8");
						usleep(500);
						if($mysqli2->query("UPDATE `software` SET `hash`='".$md5."' WHERE `name`='".$_GET['param2']."'")){
							fwrite($log, 'Пользователь '.$login.' с IP-адреса '.$_SERVER['REMOTE_ADDR'].' добавил файл /home/new_update_soft/'.$_FILES['softfile']["name"].PHP_EOL);
						}
						$mysqli2->close();
					}
				} 
			}
			if ($row['@Ctrl'] == 'remove') {
				unlink('/home/new_update_soft/'.$_GET['param2']);
				fwrite($log, 'Пользователь '.$login.' с IP-адреса '.$_SERVER['REMOTE_ADDR'].' удалил файл /home/new_update_soft/'.$_GET['param2'].PHP_EOL);
			}
			if ($row['@Ctrl'] == 'removeandadd') {
				if(unlink('/home/new_update_soft/'.$_FILES['softfile']["name"])){
					if(copy($_FILES["softfile"]["tmp_name"],'/home/new_update_soft/'.$_FILES['softfile']["name"])){
						$md5 = md5_file('/home/new_update_soft/'.$_FILES['softfile']["name"]);
						if(isset($md5)){
							$mysqli2 = new mysqli("*******", "*****", "*********", "update_soft");
							if ($mysqli2->connect_errno) {
								echo "Не удалось подключиться к MySQL:".$mysqli2->connect_error; 
								exit;
								}			
							$mysqli2->query("SET NAMES utf8");
							usleep(500);
							if($mysqli2->query("UPDATE `software` SET `hash`='".$md5."' WHERE `name`='".$_GET['param2']."'")){
								$Answer = $_FILES['softfile']["name"].' обновлен только файл!';
								fwrite($log, 'Пользователь '.$login.' с IP-адреса '.$_SERVER['REMOTE_ADDR'].' обновил файл /home/new_update_soft/'.$_FILES['softfile']["name"].PHP_EOL);
							}
							$mysqli2->close();
						}
					}
				}
			}
			echo $Answer;
		} else {
			fwrite($log, 'Выполнено с ошибкой!'.PHP_EOL);
			echo 'Упс... Что-то пошло не так!(Запрос завершился с ошибкой)';
		}
		fwrite($log, PHP_EOL);
	}
	if (($_GET['command'] == 'proc_holding') || ($_GET['command'] == 'proc_guid_to')){
		fwrite($log, date('H:i:s')." Пользователь ".$login." с IP-адреса ".$_SERVER['REMOTE_ADDR']." выполнил:".PHP_EOL);
		fwrite($log, 'CALL '.$_GET['command'].'("'.$_GET['param1'].'", "'.$_GET['param2'].'", "'.$_GET['param3'].'")'.PHP_EOL);
		if($result = $mysqli->query('CALL '.$_GET['command'].'("'.$_GET['param1'].'", "'.$_GET['param2'].'", "'.$_GET['param3'].'")')){
			$row = $result->fetch_assoc();
			echo $row['@Answer'];
			fwrite($log, 'Получен ответ: '.$row['@Answer'].PHP_EOL);
		} else {
			fwrite($log, 'Выполнено с ошибкой!'.PHP_EOL);
			echo 'Упс... Что-то пошло не так!(Запрос завершился с ошибкой)';
		}
		fwrite($log, PHP_EOL);
	}
	if ($_GET['command'] == 'loadsofttable'){
		GetSoft($mysqli, 2);
	}
	if ($_GET['command'] == 'loadobjecttable'){
		if(isset($_GET['soft_in_table'])){
			GetObject($mysqli, 2, $_GET['soft_in_table']);
		} else {
			echo 'Упс... Что-то пошло не так!';
		}
	}
	fclose($log);
} 
 
$mysqli->close();
exit;

Из особенностей, помним что при $_GET[‘obj’] == null у нас выводится основной документ, если же $_GET[‘obj’] существует, можно ему назначить разные запросы.
Очень удобно передавать в параметрах имя процедуры(если количество аргументом всех процедур одинаково), принцип такой:
query(‘CALL ‘.$_GET[‘command’].'(«‘.$_GET[‘param1’].'», «‘.$_GET[‘param2’].'», «‘.$_GET[‘param3′].'»)’)
где $_GET[‘command’] — имя процедуры в БД. У нас фактически есть процедуры с 3 и с 4 аргументами.
Изначально не подразумевалась заливка (перезаливка) файлов с одинаковым именем, поэтому для контроля версий решил собирать md5 файла(а также проверки соответствия хранимого файла и записи в MySQL). Ко всему прочему пришлось ловить ответ процедуры и немного менять его в скрипте.
При удалении файла, его имя пишется в отдельную таблицу, откуда будет удаляться при успешной выгрузке в облако.
Путь к папке с файлами жестко зафиксирован в php(с т.з. безопасности и возможности записать), хотя можно и получать из javascript(фактически позволив пользователю менять папку назначения в браузере).
По невыясненным причинам процедуры забивали коннект и пришлось поднимать еще один, для записи md5-суммы файла.
Все действия логируются в целях безопасности.
В случае неудачного получения файла работа должна быть прервана.
Проверку введенных данных в связи с построенной структурой не совсем удобно организовывать в javascript(динамический DOM не позволяет отслеживать изменение значения объектов, а вешать перезагружающийся с объектом скрипт — не очень красиво), поэтому есть смысл её организовать в php. Т.к. пока не потребовалось, работу в этой области не веду.

Из особенностей интерфейса, т.к. у нас динамически генерируемое дерево, то нам необходимо получать uid объектов. При этом они могут изменяться в режиме реального времени. Для поддержания актуальных данных создано пару невидимых блоков в которых в json-формате лежат актуальные данные по uid-ам.
Для лучшего понимания что кликабельно, в css было сделано изменение курсора на наших псевдо-checkbox-ах, на именах файлов в таблице и т.д. Все javascript-ы расписывать не вижу смысла, т.к. ничего сложного они из себя не представляют.
В функциях php было наложение адресов памяти из-за одноименных переменных, поэтому местами $mysqli стала $mysqliс например.
Вот собственно и все причуды интерфейса.