Sfoglia il codice sorgente

+ ChangeOwner function

Piotr Labudda 6 anni fa
parent
commit
4ebb7b16d5

+ 368 - 0
SE/se-lib/Route/UrlAction/ChangeOwner.php

@@ -0,0 +1,368 @@
+<?php
+
+Lib::loadClass('RouteBase');
+Lib::loadClass('Response');
+Lib::loadClass('SchemaFactory');
+
+class Route_UrlAction_ChangeOwner extends RouteBase {
+
+	function defaultAction() {
+		if ('changeOwnerPostTask' === V::get('_postTask', '', $_POST)) {
+			UI::layout([ $this, 'changeOwnerView' ]);
+		} else {
+			UI::layout([ $this, 'defaultView' ]);
+		}
+	}
+	function titleView() {
+		echo UI::h('h3', [], [
+			"Zmiana osoby odpowiedzialnej",
+			'<br>',
+			UI::h('small', [], "Zmiany w polu 'Osoba odpowiedzialna' we wszystkich rekordach wybranej tabeli."),
+		]);
+	}
+	function defaultView() {
+		$this->titleView();
+
+		$SELECTED_TABLE_ID = V::get('id_table', '', $_GET);
+		$SELECTED_FROM_OWNER_ID = V::get('id_from', '', $_GET);
+		$SELECTED_TO_OWNER_ID = V::get('id_to', '', $_GET);
+		$SELECTED_TABLE_INFO = ($SELECTED_TABLE_ID) ? $this->getTableInfo($SELECTED_TABLE_ID) : [];
+		$SELECTED_FROM_OWNER_INFO = ($SELECTED_FROM_OWNER_ID) ? $this->getUserInfo($SELECTED_FROM_OWNER_ID) : [];
+		$SELECTED_TO_OWNER_INFO = ($SELECTED_TO_OWNER_ID) ? $this->getUserInfo($SELECTED_TO_OWNER_ID) : [];
+		DBG::log($SELECTED_TABLE_INFO, 'array', "SELECTED_TABLE_INFO ({$SELECTED_TABLE_ID})");
+		DBG::log($SELECTED_FROM_OWNER_INFO, 'array', "SELECTED_FROM_OWNER_INFO ({$SELECTED_FROM_OWNER_ID})");
+		DBG::log($SELECTED_TO_OWNER_INFO, 'array', "SELECTED_TO_OWNER_INFO ({$SELECTED_TO_OWNER_ID})");
+		echo UI::h('div', [ 'id' => "p5-change-owner-widget" ]);
+		echo UI::h('style', [], "
+			.dropdown-menu>li>a { max-width: 800px; }
+		");
+		echo UI::h('script', [ 'src' => "static/vendor.js" ]); // window.p5VendorJs: {React, ReactDOM, createReactClass, Redux}
+		UI::inlineJS(__FILE__ . '.view.js', [
+			'HTML_ID' => "p5-change-owner-widget",
+			'URL_SEARCH_TABLE_LIST' => $this->getLink('searchTableListAjaxTask'),
+			'URL_SEARCH_USER_LIST' => $this->getLink('searchUserListAjaxTask'),
+			'URL_FETCH_INFO' => $this->getLink('fetchInfoAjaxTask'),
+			'SELECTED_TABLE_ID' => $SELECTED_TABLE_ID,
+			'SELECTED_TABLE_INFO' => $SELECTED_TABLE_INFO,
+			'SELECTED_FROM_OWNER_ID' => $SELECTED_FROM_OWNER_ID,
+			'SELECTED_FROM_OWNER_INFO' => $SELECTED_FROM_OWNER_INFO,
+			'SELECTED_TO_OWNER_ID' => $SELECTED_TO_OWNER_ID,
+			'SELECTED_TO_OWNER_INFO' => $SELECTED_TO_OWNER_INFO,
+			'CHANGE_OWNER_POST_TASK_NAME' => 'changeOwnerPostTask',
+			'URL_BASE' => $this->getLink(),
+		]);
+	}
+
+	function changeOwnerView() {
+		$this->titleView();
+		$id_table = V::get('id_table', '', $_POST);
+		$id_from = V::get('id_from', '', $_POST);
+		$id_to = V::get('id_to', '', $_POST);
+		if (!$id_table) throw new Exception("Missing id_table");
+		if (!$id_from) throw new Exception("Missing id_from");
+		if (!$id_to) throw new Exception("Missing id_to");
+		$stats = $this->fetchInfoAjaxTask([
+			'id_table' => $id_table,
+			'id_from' => $id_from,
+			'id_to' => $id_to,
+		]);
+		DBG::log($stats, 'array', '$stats');
+		$tableInfo = $this->getTableInfo($id_table);
+		DBG::log($tableInfo, 'array', '$tableInfo');
+		$tableLabel = (!empty($tableInfo)) ? $tableInfo[0]['label'] : "";
+		echo UI::h('div', [ 'class' => "alert alert-info" ], "Wprowadzanie zmian w tabeli <em>[{$id_table}]</em> {$tableLabel}");
+		$totalToUpdate = $stats['body']['fromInfo']['total'];
+		if (!$totalToUpdate) throw new Exception("Brak rekordów do aktualizacji");
+
+		$namespace = DB::getPDO()->fetchValue(" select o.namespace from `CRM_#CACHE_ACL_OBJECT` o where o.idZasob = '{$id_table}' limit 1 ");
+		$acl = ACL::getAclByNamespace($namespace);
+		$rootTableName = $acl->getRootTableName();
+		$primaryKeyField = $acl->getPrimaryKeyField();
+		$sqlTable = DB::getPDO()->identifierQuote($rootTableName);
+		{ // TODO: loop / sql limit
+			try {
+				$rowsToUpdate = DB::getPDO()->fetchAll("
+					select t.ID, t.L_APPOITMENT_USER
+					from {$sqlTable} t
+					where t.L_APPOITMENT_USER = :id_from
+					limit 100
+				", [
+					':id_from' => $id_from,
+				]);
+				UI::table(['rows'=>$rowsToUpdate]);
+			} catch (Exception $e) {
+				// msg: "SQLSTATE[42S22]: Column not found: 1054 Unknown column 't.L_APPOITMENT_USER' in 'field list'"
+				DBG::log($e);
+				if ($e->getCode() === '42S22') {
+					throw new Exception("Brak pola Osoba odpowiedzialna w tabeli '{$namespace}'");
+				}
+				throw $e;
+			}
+			foreach ($rowsToUpdate as $item) {
+				try {
+					DB::getPDO()->update($rootTableName, $primaryKeyField, $item['ID'], [
+						'L_APPOITMENT_USER' => $id_to,
+						'A_RECORD_UPDATE_DATE' => "NOW()",
+						'A_RECORD_UPDATE_AUTHOR' => User::getLogin(),
+					]);
+					DB::getPDO()->insert($rootTableName . "_HIST", [
+						'ID_USERS2' => $item['ID'],
+						'L_APPOITMENT_USER' => $id_to,
+						'A_RECORD_UPDATE_DATE' => "NOW()",
+						'A_RECORD_UPDATE_AUTHOR' => User::getLogin(),
+					]);
+				} catch (Exception $e) {
+					DBG::log($e);
+					UI::alert('danger', [], "Nie udało się zaktualizować rekordu '{$item['ID']}': " . $e->getMessage());
+				}
+			}
+		}
+		UI::alert('success', "Koniec");
+	}
+
+	function getTableInfo($idTable) {
+		$userObject = SchemaFactory::loadDefaultObject('UserObject');
+		$userObject->setIdUser(User::getID());
+		// $userObject->setIdProcesFilter($filterIdProces);
+		$items = $userObject->getItems([
+			'cols' => [ 'ID_TABLE', 'name', 'label' ], // , 'opis' ],
+			'primaryKey' => $idTable,
+		]);
+		// UI::table([ 'rows' => $items ]);
+
+		$items = array_map(function ($item) {
+			// [16] => Array:
+			// [ID_TABLE] => 16
+			// [ID_USER] => 4517
+			// [ID_PROCES] => 
+			// [db] => 36
+			// [name] => BUILDINGS
+			// [label] => Budynki
+			// [opis] => Budynki - tabela budynków w zasięgu lub zainteresowanych podłączeniem do sieci BIALL-NET.
+			// [_rootTableName] => BUILDINGS
+			// [namespace] => default_db/BUILDINGS
+			return [
+				'id' => $item['ID_TABLE'],
+				'label' => $this->getTableLabelFromRow($item),
+				'_item_name' => $item['name'],
+				'_item_label' => $item['label'],
+				'_item_name_substr' => substr($item['name'], 0, strlen($item['label'])),
+				'_item' => $item,
+			];
+		}, $items);
+
+		return (!empty($items)) ? [ reset($items) ] : [];
+	}
+	function getUserInfo($userLogin) {
+		return DB::getPDO()->fetchAll("
+			select u.ADM_ACCOUNT as id
+				, u.ADM_NAME as label
+			from ADMIN_USERS u
+			where u.ADM_ACCOUNT = :login
+		", [ ':login' => $userLogin ]);
+	}
+
+	function searchTableListAjaxTaskAction() { Response::sendTryCatchJson([$this, 'searchTableListAjaxTask'], $args = 'JSON_FROM_REQUEST_BODY'); }
+	function searchTableListAjaxTask($args) {
+		$query = V::get('query', '', $args);
+		$wordsQuery = $this->convertQueryToSearchWords($query);
+		$items = $this->fetchAllowedTableList($wordsQuery);
+
+		return [
+			'type' => 'success',
+			'msg' => 'OK',
+			'__DBG__args' => $args,
+			'body' => [
+				'items' => array_values($items),
+			]
+		];
+	}
+
+	function searchUserListAjaxTaskAction() { Response::sendTryCatchJson([$this, 'searchUserListAjaxTask'], $args = 'JSON_FROM_REQUEST_BODY'); }
+	function searchUserListAjaxTask($args) {
+		$query = V::get('query', '', $args);
+		$wordsQuery = $this->convertQueryToSearchWords($query);
+		$sqlListWhere = [];
+		$pdo = DB::getPDO();
+		foreach ($wordsQuery as $word) {
+			$sqlListWhere[] = implode(" or ", [
+				"u.ID like " . $pdo->quote("%{$word}%"),
+				"u.ADM_ACCOUNT like " . $pdo->quote("%{$word}%"),
+				"u.ADM_NAME like " . $pdo->quote("%{$word}%"),
+			]);
+		}
+		$sqlWhere = (!empty($sqlListWhere)) ? implode(" and ", $sqlListWhere) : "1=1";
+		$items = DB::getPDO()->fetchAll("
+			select u.ADM_ACCOUNT as id
+				, u.ADM_NAME as label
+			from ADMIN_USERS u
+			where {$sqlWhere}
+		", [ ':id' => $idUser ]);
+
+		return [
+			'type' => 'success',
+			'msg' => 'OK',
+			'body' => [
+				'items' => array_values($items),
+			]
+		];
+	}
+
+	function convertQueryToSearchWords($query) {
+		$query = trim($query);
+		if (strlen($query) < 3) throw new Exception("Query must be more then 2 chars");
+
+		$wordsQuery = [];
+		if (false !== strpos($query, ' ')) {
+			$words = explode(' ', $query);
+			DBG::log($words, 'array', "\$words from \$query({$query}) - 1");
+			$words = array_map(function ($word) {
+				return trim(trim($word), '0');
+			}, $words);
+			DBG::log($words, 'array', "\$words from \$query({$query}) - 2");
+			$words = array_filter($words, function ($word) {
+				return (strlen($word) > 2);
+			});
+			DBG::log($words, 'array', "\$words from \$query({$query}) - 3");
+			if (empty($words)) throw new Exception("Query words must be more then 2 chars");
+			$wordsQuery = $words;
+		} else {
+			$wordsQuery[] = $query;
+		}
+		return $wordsQuery;
+	}
+
+	function fetchAllowedTableList($wordsQuery) {
+		$userObject = SchemaFactory::loadDefaultObject('UserObject');
+		$userObject->setIdUser(User::getID());
+		// $userObject->setIdProcesFilter($filterIdProces);
+		$items = $userObject->getItems([
+			'cols' => [ 'ID_TABLE', 'name', 'label' ], // , 'opis' ],
+		]);
+		// UI::table([ 'rows' => $items ]);
+		$items = array_filter($items, function ($item) use ($wordsQuery) {
+			foreach ($wordsQuery as $word) {
+				// DBG::log([
+				// 	'is1' => ($item['ID_TABLE'] == $word),
+				// 	'is2' => (false !== stripos($item['name'], $word)),
+				// 	'is3' => (false !== stripos($item['label'], $word)),
+				// 	'$item' => $item,
+				// ], 'array', "DBG:array_filter({$item['ID_TABLE']}) '{$word}'");
+				if ($item['ID_TABLE'] == $word) return true;
+				if (false !== stripos($item['name'], $word)) return true;
+				if (false !== stripos($item['label'], $word)) return true;
+			}
+			return false;
+		});
+		// UI::table([ 'rows' => $items ]);
+
+		return array_map(function ($item) {
+			// [16] => Array:
+			// [ID_TABLE] => 16
+			// [ID_USER] => 4517
+			// [ID_PROCES] => 
+			// [db] => 36
+			// [name] => BUILDINGS
+			// [label] => Budynki
+			// [opis] => Budynki - tabela budynków w zasięgu lub zainteresowanych podłączeniem do sieci BIALL-NET.
+			// [_rootTableName] => BUILDINGS
+			// [namespace] => default_db/BUILDINGS
+			return [
+				'id' => $item['ID_TABLE'],
+				'_item_name' => $item['name'],
+				'_item_label' => $item['label'],
+				'_item_name_substr' => substr($item['name'], 0, strlen($item['label'])),
+				'label' => $this->getTableLabelFromRow($item),
+				'_item' => $item,
+			];
+		}, $items);
+	}
+	function getTableLabelFromRow($item) {
+		$label = $item['name'];
+		if (!empty($item['label'])) {
+			$tableName = (false !== strpos($item['name'], '/')) ? substr($item['name'], strrpos($item['name'], '/') + 1) : $item['name'];
+			if (strlen($item['label']) < strlen($tableName) && substr($tableName, 0, strlen($item['label'])) === $item['label']) {
+				$label = $tableName;	
+			} else {
+				$label = $item['label'];
+			}
+		}
+		// if (!empty($item['opis']) && !empty($item['label'])) {
+
+		// }
+		return $label;
+	}
+
+	function fetchInfoAjaxTaskAction() { Response::sendTryCatchJson([$this, 'fetchInfoAjaxTask'], $args = 'JSON_FROM_REQUEST_BODY'); }
+	function fetchInfoAjaxTask($args) {
+		$id_table = V::get('id_table', '', $args);
+		$id_from = V::get('id_from', '', $args);
+		$id_to = V::get('id_to', '', $args);
+		if (!$id_table) throw new Exception("Missing id table");
+		$responseBody = [];
+		$responseBody['by_owner'] = []; // [ { login, total }, ... ]
+		$responseBody['fromInfo'] = []; // { id, name, status, total, ... }
+		$responseBody['toInfo'] = [];   // { id, name, status, total, ... }
+		$responseBody['errors'] = []; // [ { type, msg } ]
+		$responseBody['error_missing_owner_field'] = false; // bool
+
+		$namespace = DB::getPDO()->fetchValue(" select o.namespace from `CRM_#CACHE_ACL_OBJECT` o where o.idZasob = '{$id_table}' limit 1 ");
+		$acl = ACL::getAclByNamespace($namespace);
+		$rootTableName = $acl->getRootTableName();
+		$sqlTable = DB::getPDO()->identifierQuote($rootTableName);
+		try {
+			$responseBody['by_owner'] = DB::getPDO()->fetchAll("
+				select t.L_APPOITMENT_USER as id, count(*) as total
+				from {$sqlTable} t
+				where t.L_APPOITMENT_USER = :id_from or t.L_APPOITMENT_USER = :id_to
+				group by t.L_APPOITMENT_USER
+				limit 10
+			", [
+				':id_from' => ($id_from) ? $id_from : "XXXXX",
+				':id_to' => ($id_to) ? $id_to : "XXXXX",
+			]);
+		} catch (Exception $e) {
+			// msg: "SQLSTATE[42S22]: Column not found: 1054 Unknown column 't.L_APPOITMENT_USER' in 'field list'"
+			DBG::log($e);
+			if ($e->getCode() === '42S22') {
+				$responseBody['error_missing_owner_field'] = true;
+			}
+			$responseBody['errors']['by_owner'] = $e->getMessage();
+		}
+		$fromInfo = $this->_fetchOwnerInfo($id_from);
+		$toInfo = $this->_fetchOwnerInfo($id_to);
+
+		$responseBody['fromInfo'] = (!$fromInfo) ? null : array_merge($fromInfo, [
+			'total' => array_reduce($responseBody['by_owner'], function ($ret, $item) use ($id_from) {
+				return ($item['id'] === $id_from) ? $item['total'] : $ret;
+			}, 0)
+		]);
+		$responseBody['toInfo'] = (!$toInfo) ? null : array_merge($toInfo, [
+			'total' => array_reduce($responseBody['by_owner'], function ($ret, $item) use ($id_to) {
+				return ($item['id'] === $id_to) ? $item['total'] : $ret;
+			}, 0)
+		]);
+
+		return [
+			'type' => 'success',
+			'msg' => 'OK',
+			'__DBG__namespace' => $namespace,
+			'__DBG__acl' => $acl,
+			'__DBG__args' => $args,
+			'body' => $responseBody,
+		];
+	}
+	function _fetchOwnerInfo($id) { // @param $id: string || null
+		return DB::getPDO()->fetchFirst("
+			select t.ADM_ACCOUNT as id
+				, t.ADM_NAME as name
+				, t.EMPLOYEE_TYPE as type
+				, t.A_STATUS as status
+			from `ADMIN_USERS` t
+			where t.ADM_ACCOUNT = :id_from
+		", [
+			':id_from' => $id,
+		]);
+	}
+
+}

+ 471 - 0
SE/se-lib/Route/UrlAction/ChangeOwner.php.view.js

@@ -0,0 +1,471 @@
+var DBG = DBG || false;
+var DBG1 = true;
+if (!HTML_ID) throw "Missing HTML_ID"
+if (!URL_BASE) throw "Missing URL_BASE"
+if (!URL_SEARCH_TABLE_LIST) throw "Missing URL_SEARCH_TABLE_LIST"
+if (!URL_SEARCH_USER_LIST) throw "Missing URL_SEARCH_USER_LIST"
+if (!URL_FETCH_INFO) throw "Missing URL_FETCH_INFO"
+if (!CHANGE_OWNER_POST_TASK_NAME) throw "Missing CHANGE_OWNER_POST_TASK_NAME"
+var SELECTED_TABLE_ID = SELECTED_TABLE_ID || null
+var SELECTED_TABLE_INFO = SELECTED_TABLE_INFO || null
+var SELECTED_FROM_OWNER_ID = SELECTED_FROM_OWNER_ID || null
+var SELECTED_FROM_OWNER_INFO = SELECTED_FROM_OWNER_INFO || null
+var SELECTED_TO_OWNER_ID = SELECTED_TO_OWNER_ID || null
+var SELECTED_TO_OWNER_INFO = SELECTED_TO_OWNER_INFO || null
+
+if (!global.fetch) throw "Missing fetch";
+if (!global.p5VendorJs) throw "Missing p5VendorJs";
+if (!global.p5VendorJs.Typeahead) throw "Missing Typeahead";
+if (!global.p5VendorJs.AsyncTypeahead) throw "Missing AsyncTypeahead";
+
+var createReactClass = global.p5VendorJs.createReactClass;
+var h = global.p5VendorJs.React.createElement;
+var React = global.p5VendorJs.React;
+var ReactDOM = global.p5VendorJs.ReactDOM;
+var Redux = global.p5VendorJs.Redux;
+var ReduxThunk = global.p5VendorJs.ReduxThunk;
+var createStoreWithThunkMiddleware = Redux.applyMiddleware(ReduxThunk)(Redux.createStore); // TODO: to vendor.js
+var Typeahead = global.p5VendorJs.Typeahead;
+var AsyncTypeahead = window.p5VendorJs.AsyncTypeahead;
+
+function makeJsonRequest(url, props) {
+	return global.fetch(url, {
+		method: 'POST',
+		header: { 'contentType': 'applications/json' },
+		credentials: 'same-origin',
+		body: JSON.stringify(props.body)
+	})
+	.then(function (response) {
+		return response.text();
+	})
+	.then(function (responseText) {
+		try {
+			return JSON.parse(responseText);
+		} catch (e) {
+			throw responseText;
+		}
+	})
+	.then(function (result) {
+		DBG1 && console.log('DBG:makeJsonRequest: result', result);
+		if (result.type == 'success') {
+			return result.body;
+		}
+	})
+}
+
+function filterByLabelHelper(listItem, node) { // previous: filterByHelper
+	// DBG && console.log("DBG:filterByLabelHelper", { listItem, node })
+	var query = (node && node.text) ? node.text : '';
+	var label = listItem.label.toLowerCase();
+	var words = (
+		(query.indexOf(' ') > 0) ? query.split(' ') : [ query ]
+	).map(function (word) { return word.trim().replace(/^[0]+/g, '').toLowerCase(); })
+		.filter(function (word) { return word.length > 2; });
+	var foundWords = words.filter(function (word) {
+		return (label.indexOf(word) > -1);
+	})
+	return (foundWords.length === words.length);
+}
+function filterByAllHelper(listItem, node) {
+	// DBG && console.log("DBG:filterByAllHelper", { listItem, node })
+	var query = (node && node.text) ? node.text : '';
+	var label = listItem.label.toLowerCase();
+	var id = listItem.id.toLowerCase();
+	var words = (
+		(query.indexOf(' ') > 0) ? query.split(' ') : [ query ]
+	).map(function (word) { return word.trim().replace(/^[0]+/g, '').toLowerCase(); })
+		.filter(function (word) { return word.length > 2; });
+	var foundWords = words.filter(function (word) {
+		return (label.indexOf(word) > -1 || id.indexOf(word) > -1);
+	})
+	return (foundWords.length === words.length);
+}
+
+var p5UI__ChangeOwner_SearchTableWidget = createReactClass({
+	getInitialState: function () {
+		return {
+			isSearching: false,
+			options: this.props.selected || [],
+		}
+	},
+	handleSearch: function (query) {
+		this.setState({ isSearching: true });
+		var _setState = this.setState.bind(this);
+		makeJsonRequest(URL_SEARCH_TABLE_LIST, {
+			body: {
+				query: query,
+			}
+		})
+		.then(function (responseBody) {
+			var items = (responseBody && responseBody.items && responseBody.items.length) ? responseBody.items : []
+			if (!items.length) p5UI__notifyAjaxCallback({ type: 'warning', msg: "Brak danych pasujących do kryteriów wyszukiwania" });
+			DBG1 && console.log('items fetched:', items);
+			_setState({ isSearching: false, options: items });
+		})
+		.catch(function (error) {
+			DBG1 && console.log('request failed', error);
+		})
+	},
+
+	render: function () {
+		DBG1 && console.log("DBG:ChangeOwner:SearchTable:render", { props: this.props, state: this.state });
+		return h('div', {}, [
+			h(AsyncTypeahead, {
+				isLoading: this.state.isSearching,
+				allowNew: false,
+				multiple: false,
+				options: this.state.options || [],
+				selected: this.props.selected || [],
+				labelKey: "label",
+				emptyLabel: "Brak danych pasujących do kryteriów wyszukiwania",
+				searchText: "Wyszukiwanie...",
+				// labelKey: function (option) {
+				//   return [
+				//     option.nazwa.replace('"', ''),
+				//     option.nip,
+				//     option.krs,
+				//     option.regon,
+				//     option.S_miejscowosc,
+				//   ].join(' ')
+				// },
+				minLength: 3,
+				onSearch: this.handleSearch,
+				placeholder: "Wyszukaj...",
+				autoFocus: false,
+				filterBy: filterByLabelHelper,
+				renderMenuItemChildren: function (option, props) {
+					return h(p5UI__ChangeOwner_SearchTableItem, { key: option.id, data: option });
+				},
+				onChange: this.props.onChange,
+			})
+		])
+	}
+});
+var p5UI__ChangeOwner_SearchTableItem = createReactClass({
+	// this.props: { key: option.ID, baza: selectedBaza, data: option }
+	render: function () {
+		return h('div', {}, [
+			h('em', {}, "[" + this.props.data.id + "]"),
+			" ",
+			this.props.data.label,
+		]);
+	}
+});
+
+var p5UI__ChangeOwner_SearchUserWidget = createReactClass({
+	getInitialState: function () {
+		return {
+			isSearching: false,
+			options: (this.props.selected && this.props.selected.length > 0) ? this.props.selected : [],
+		}
+	},
+	handleSearch: function (query) {
+		this.setState({ isSearching: true });
+		var _setState = this.setState.bind(this);
+		makeJsonRequest(URL_SEARCH_USER_LIST, {
+			body: {
+				id_table: this.props.id_table,
+				query: query,
+			}
+		})
+		.then(function (responseBody) {
+			var items = (responseBody && responseBody.items && responseBody.items.length) ? responseBody.items : []
+			if (!items.length) p5UI__notifyAjaxCallback({ type: 'warning', msg: "Brak danych pasujących do kryteriów wyszukiwania" });
+			DBG1 && console.log('items fetched:', items);
+			_setState({ isSearching: false, options: items });
+		})
+		.catch(function (error) {
+			DBG1 && console.log('request failed', error);
+		})
+	},
+
+	render: function () {
+		DBG1 && console.log("DBG:ChangeOwner:SearchUser:render", { props: this.props, state: this.state });
+		return h('div', {}, [
+			h(AsyncTypeahead, {
+				isLoading: this.state.isSearching,
+				allowNew: false,
+				multiple: false,
+				options: this.state.options,
+				selected: this.props.selected || [],
+				labelKey: "label",
+				emptyLabel: "Brak danych pasujących do kryteriów wyszukiwania",
+				searchText: "Wyszukiwanie...",
+				// labelKey: function (option) {
+				//   return [
+				//     option.nazwa.replace('"', ''),
+				//     option.nip,
+				//     option.krs,
+				//     option.regon,
+				//     option.S_miejscowosc,
+				//   ].join(' ')
+				// },
+				minLength: 3,
+				onSearch: this.handleSearch,
+				placeholder: "Wyszukaj...",
+				autoFocus: false,
+				filterBy: filterByAllHelper,
+				renderMenuItemChildren: function (option, props) {
+					return h(p5UI__ChangeOwner_SearchUserItem, { key: option.id, data: option });
+				},
+				onChange: this.props.onChange,
+			})
+		])
+	}
+});
+var p5UI__ChangeOwner_SearchUserItem = createReactClass({
+	// this.props: { key: option.ID, baza: selectedBaza, data: option }
+	render: function () {
+		return h('div', {}, [
+			h('em', {}, "[" + this.props.data.id + "]"),
+			" ",
+			this.props.data.label,
+		]);
+	}
+});
+
+
+var p5UI__ChangeOwnerWidget = createReactClass({
+	getInitialState: function () {
+		return {
+			selectedTableID: SELECTED_TABLE_ID,
+			selectedFromOwnerLogin: SELECTED_FROM_OWNER_ID,
+			selectedToOwnerLogin: SELECTED_TO_OWNER_ID,
+			selectedTableInfo: SELECTED_TABLE_INFO,
+			selectedFromOwnerInfo: SELECTED_FROM_OWNER_INFO,
+			selectedToOwnerInfo: SELECTED_TO_OWNER_INFO,
+			isSearchingInfo: false,
+			info: null,
+		}
+	},
+	componentDidMount: function () {
+		if (this.state.selectedTableID > 0) {
+			this.fetchInfo()
+		}
+	},
+	makeUrl: function (args) {
+		var queryArgs = Object.assign({
+			id_table: this.state.selectedTableID,
+			id_from: this.state.selectedFromOwnerLogin,
+			id_to: this.state.selectedToOwnerLogin,
+		}, args);
+		return [ URL_BASE ].concat(
+			Object.keys(queryArgs).reduce(function (ret, argName) {
+				if (!queryArgs[argName]) return ret;
+				ret.push(argName + '=' + queryArgs[argName])
+				return ret;
+			}, [])
+		).join("&")
+	},
+	handleChangeTableID: function (selected) {
+		DBG1 && console.log("DBG:ChangeOwner:handleChangeTableID", { selected })
+		var tableID = (selected.length > 0) ? selected[0].id : ''
+		this.setState({
+			selectedTableID: tableID,
+			selectedTableInfo: (selected.length > 0) ? [ selected[0] ] : null,
+		})
+		if (global.history && global.history.pushState) history.pushState({}, global.document.title, this.makeUrl({ id_table: tableID }));
+	},
+	handleChangeFromOwnerID: function (selected) {
+		DBG1 && console.log("DBG:ChangeOwner:handleChangeFromOwnerID", { selected })
+		var fromLogin = (selected.length > 0) ? selected[0].id : ''
+		// if (global.history && global.history.pushState) history.pushState({}, global.document.title, this.makeUrl({ id_from: fromLogin }));
+		this.setState({
+			selectedFromOwnerLogin: fromLogin,
+			selectedFromOwnerInfo: (selected.length > 0) ? [ selected[0] ] : null,
+		}, (function () {
+			if (global.history && global.history.pushState) history.pushState({}, global.document.title, this.makeUrl());
+			this.fetchInfo()
+		}).bind(this))
+	},
+	handleChangeToOwnerID: function (selected) {
+		DBG1 && console.log("DBG:ChangeOwner:handleChangeToOwnerID", { selected })
+		var toLogin = (selected.length > 0) ? selected[0].id : ''
+		this.setState({
+			selectedToOwnerLogin: toLogin,
+			selectedToOwnerInfo: (selected.length > 0) ? [ selected[0] ] : null,
+		}, (function () {
+			if (global.history && global.history.pushState) history.pushState({}, global.document.title, this.makeUrl());
+			this.fetchInfo()
+		}).bind(this))
+	},
+	fetchInfo: function () {
+		DBG1 && console.log("TODO:ChangeOwner:fetchInfo", { state: this.state })
+		// TODO: fetch info if id_table, and id_from, optional id_to
+		// TODO: handle race condition, skip old responses
+		this.setState({ isSearchingInfo: true });
+		var _setState = this.setState.bind(this);
+		makeJsonRequest(URL_FETCH_INFO, {
+			body: {
+				id_table: this.state.selectedTableID,
+				id_from: this.state.selectedFromOwnerLogin,
+				id_to: this.state.selectedToOwnerLogin,
+			}
+		})
+		.then(function (info) {
+			// if (!info.length) p5UI__notifyAjaxCallback({ type: 'warning', msg: "Brak danych pasujących do kryteriów wyszukiwania" });
+			DBG1 && console.log('DBG:ChangeOwner:handleChangeTableID info fetched:', info);
+			_setState({ isSearchingInfo: false, info: info });
+		})
+		.catch(function (error) {
+			DBG1 && console.log('DBG:ChangeOwner:handleChangeTableID request failed', error);
+		})
+	},
+	renderInfo: function () {
+		if (!this.state.info) return null;
+		if (!this.state.selectedTableID) return null;
+		if (!this.state.selectedFromOwnerLogin) return null;
+		if (!this.state.selectedToOwnerLogin) return null;
+		if (this.state.info && this.state.info.error_missing_owner_field) {
+			return h('div', { className: "alert alert-danger" }, "Brak pola Osoba odpowiedzialana w wybranej tabeli");
+		}
+		if (this.state.info && this.state.info.errors && this.state.info.errors.length) {
+			return h('div', {}, this.state.info.errors.map(function (err) {
+				return h('div', { className: "alert alert-danger" }, err);
+			}));
+		}
+		// "fromInfo": {
+		// 		"id": "wolczynskit",
+		// 		"name": "Tomasz Wólczyński",
+		// 		"type": "Pracownik",
+		// 		"status": "NORMAL",
+		// 		"total": "1793"
+		// },
+		// "toInfo": {
+		// 		"id": "michal.podejko",
+		// 		"name": "Michał Podejko",
+		// 		"type": "Pracownik",
+		// 		"status": "NORMAL",
+		// 		"total": "5940"
+		// },
+		var fromInfo = this.state.info.fromInfo
+		var toInfo = this.state.info.toInfo
+		var totalToUpdate = fromInfo.total || 0
+
+		return h('div', {}, [
+			h('table', { style: { width: "100%" } }, [
+				h('tbody', {}, [
+					h('tr', {}, [
+						h('td', { style: { width: "5%" } }, ""),
+						h('td', { style: { width: "40%" } }, [
+							(fromInfo) ? this.renderOwnerInfo(fromInfo) : h('div', { className: "alert alert-warning" }, "Brak danych"),
+						]),
+						h('td', {}, ""),
+						h('td', { style: { width: "40%" } }, [
+							(toInfo) ? this.renderOwnerInfo(toInfo) : h('div', { className: "alert alert-warning" }, "Brak danych"),
+						]),
+					])
+				])
+			]),
+			(totalToUpdate)
+			?	h('form', { method: "post", style: { textAlign: "center" } }, [
+					h('input', { type: "hidden", name: "id_table", value: this.state.selectedTableID }),
+					h('input', { type: "hidden", name: "id_from", value: this.state.selectedFromOwnerLogin }),
+					h('input', { type: "hidden", name: "id_to", value: this.state.selectedToOwnerLogin }),
+					h('input', { type: "hidden", name: "_postTask", value: CHANGE_OWNER_POST_TASK_NAME }),
+					h('button', { className: "btn btn-lg btn-primary" }, "Zmień " + totalToUpdate + " rekordów"),
+				])
+			:	null
+			,
+			// h('pre', {}, [
+			// 	JSON.stringify(this.state.info, null, "\t")
+			// ]),
+		]);
+	},
+	renderOwnerInfo: function (userInfo) {
+		return h('div', {}, [
+			h('p', {}, [
+				h('em', {}, "[" + userInfo.id + "]"),
+				" ",
+				h('span', {}, userInfo.name),
+				h('br'),
+				h('span', {}, "Status: " + userInfo.status),
+				h('br'),
+				h('span', {}, "Typ: " + userInfo.type),
+			]),
+			h('p', {}, [
+				h('span', {}, "Odnaleziono " + userInfo.total + " rekordów"),
+			]),
+		]);
+	},
+	render: function () {
+		DBG1 && console.log("DBG:ChangeOwner:render", { props: this.props, state: this.state });
+		return h('form', { method: "get", style: { fontSize: "14px", lineHeight: "2em" } }, [
+			h('input', { type: "hidden", name: "_route", value: "UrlAction_ChangeOwner" }),
+			h('input', { type: "hidden", name: "id_table", value: this.state.selectedTableID }),
+			h('input', { type: "hidden", name: "id_from", value: this.state.selectedFromOwnerLogin }),
+			h('input', { type: "hidden", name: "id_to", value: this.state.selectedToOwnerLogin }),
+			h('table', { style: { width: "100%", margin: "12px 0" } }, [
+				h('tbody', {}, [
+					h('tr', {}, [
+						h('td', { style: { width: "5%" } }, [
+							"Tabela: ",
+						]),
+						h('td', {}, [
+							h(p5UI__ChangeOwner_SearchTableWidget, {
+								selected: this.state.selectedTableInfo,
+								onChange: this.handleChangeTableID,
+							})
+						]),
+						h('td', {}, [
+							h('a', {
+								'title': "Usuń zaznaczenie",
+								'href': URL_BASE,
+								'class': "btn btn-link",
+							}, "usuń"),
+						]),
+					]),
+				]),
+			]),
+			(this.state.selectedTableID > 0 && this.state.selectedTableInfo.length > 0) && h(React.Fragment, {}, [
+				h('p', {}, [
+					"Zmień osobę odpowiedzialną w tabeli ",
+					h('em', {}, "[" + this.state.selectedTableID + "]"),
+					" " + this.state.selectedTableInfo[0].label
+				]),
+				h('table', { style: { width: "100%", margin: "12px 0" } }, [
+					h('tbody', {}, [
+						h('tr', {}, [
+							h('td', { style: { width: "5%", textAlign: "right", paddingRight: "3px" } }, [
+								"z: ",
+							]),
+							h('td', { style: { width: "40%" } }, [
+								h(p5UI__ChangeOwner_SearchUserWidget, {
+									id_table: this.state.selectedTableID,
+									selected: this.state.selectedFromOwnerInfo,
+									onChange: this.handleChangeFromOwnerID,
+								})
+							]),
+							h('td', {}, ""),
+							h('td', { style: { width: "5%", textAlign: "right", paddingRight: "3px" } }, [
+								"na: ",
+							]),
+							h('td', { style: { width: "40%" } }, [
+								h(p5UI__ChangeOwner_SearchUserWidget, {
+									id_table: this.state.selectedTableID,
+									selected: this.state.selectedToOwnerInfo,
+									onChange: this.handleChangeToOwnerID,
+								})
+							]),
+						]),
+					]),
+				]),
+				(this.state.isSearchingInfo)
+					?	h('div', {}, [
+							"Pobieranie informacji ..."
+						])
+					:	this.renderInfo()
+				,
+			]),
+		]);
+	}
+})
+
+
+ReactDOM.render(
+	h(p5UI__ChangeOwnerWidget, {
+		// store: createStoreWithThunkMiddleware(NetworkGraph.store),
+		// storeActions: NetworkGraph.createActions(),
+	}),
+	document.getElementById(HTML_ID)
+);