account.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. {% extends "base.html" %} {% set active_page = "accounts" %} {% block title %}
  2. Account overview {% endblock %} {% block divs %}
  3. <div class="pl-3">
  4. {% if can_view_account_auditlog %}
  5. <button
  6. class="btn btn-sm btn-responsive btn-info m-4"
  7. type="submit"
  8. title="View history of user actions."
  9. >
  10. <a href="/accounts/auditlog/{{ account.id }}">Audit log</a>
  11. </button>
  12. {% endif %}
  13. </div>
  14. <div class="container-fluid">
  15. <div class="row">
  16. <div class="col-md-2">
  17. {% if user_can_update_account %}
  18. <div class="sidepanel-container">
  19. <div
  20. class="left-sidepanel-label"
  21. style="transform: translateX(-30%) !important"
  22. >
  23. Edit Account
  24. </div>
  25. <div class="sidepanel left-sidepanel">
  26. <form class="form-horizontal" id="editaccount">
  27. <fieldset>
  28. <div class="asset-form">
  29. <h3>Edit {{ account.name }}</h3>
  30. <small
  31. >Owned by account: {{ account.name }} (ID: {{ account.id
  32. }})</small
  33. >
  34. <div class="form-group">
  35. <div class="form-group">
  36. <div class="col-md-3">
  37. <label for="name" class="control-label">Name</label>
  38. <input
  39. type="text"
  40. class="form-control"
  41. id="name"
  42. name="name"
  43. value="{{ account.name }}"
  44. required
  45. />
  46. </div>
  47. </div>
  48. <div class="form-group">
  49. <div class="col-md-3">
  50. <label for="primary_color" class="control-label"
  51. >Primary Color</label
  52. >
  53. <span
  54. class="fa fa-info d-inline-block ps-2"
  55. rel="tooltip"
  56. aria-hidden="true"
  57. tabindex="0"
  58. data-bs-placement="right"
  59. data-bs-toggle="tooltip"
  60. title="Primary color to use in UI, in hex format. Defaults to FlexMeasures' primary color (#1a3443)"
  61. ></span>
  62. <input
  63. type="text"
  64. class="form-control"
  65. data-bs-placement="right"
  66. id="primary_color"
  67. name="primary_color"
  68. value="{{ account.primary_color or '' }}"
  69. />
  70. </div>
  71. </div>
  72. <div class="form-group">
  73. <div class="col-md-3">
  74. <label for="secondary_color" class="control-label"
  75. >Secondary Color</label
  76. >
  77. <span
  78. class="fa fa-info d-inline-block ps-2"
  79. rel="tooltip"
  80. aria-hidden="true"
  81. tabindex="0"
  82. data-bs-placement="right"
  83. data-bs-toggle="tooltip"
  84. title="Secondary color to use in UI, in hex format. Defaults to FlexMeasures' primary color (#f1a122)"
  85. ></span>
  86. <input
  87. type="text"
  88. class="form-control"
  89. id="secondary_color"
  90. name="secondary_color"
  91. value="{{ account.secondary_color or '' }}"
  92. />
  93. </div>
  94. </div>
  95. <div class="form-group">
  96. <div class="col-md-3">
  97. <label for="logo_url" class="control-label"
  98. >Logo URL</label
  99. >
  100. <span
  101. class="fa fa-info d-inline-block ps-2"
  102. rel="tooltip"
  103. aria-hidden="true"
  104. tabindex="0"
  105. data-bs-placement="right"
  106. data-bs-toggle="tooltip"
  107. title="Logo URL to use in UI. Defaults to FlexMeasures' logo URL"
  108. ></span>
  109. <input
  110. type="text"
  111. class="form-control"
  112. id="logo_url"
  113. name="logo_url"
  114. value="{{ account.logo_url or '' }}"
  115. />
  116. </div>
  117. </div>
  118. {% if user_is_admin %}
  119. <div class="form-group">
  120. <div class="col-md-3">
  121. <label for="consultant_account_id" class="control-label"
  122. >Consultant Account</label
  123. >
  124. <select
  125. class="form-select"
  126. aria-label="Default select example"
  127. id="consultant_account_id"
  128. name="consultancy_account_id"
  129. >
  130. <option
  131. value="{{ account.consultancy_account_id or '' }}"
  132. selected
  133. >
  134. {% if account.consultancy_account_id %} {% for
  135. consultancy_account in accounts %} {% if
  136. consultancy_account.id ==
  137. account.consultancy_account_id %} {{
  138. consultancy_account.name }} {% endif %} {% endfor %}
  139. {% else %} Select Account {% endif %}
  140. </option>
  141. {% for account in accounts %}
  142. <option value="{{ account.id }}">
  143. {{ account.name }}
  144. </option>
  145. {% endfor %}
  146. </select>
  147. </div>
  148. </div>
  149. {% endif %}
  150. </div>
  151. <button
  152. class="btn btn-sm btn-responsive btn-success create-button"
  153. type="submit"
  154. value="Save"
  155. style="
  156. margin-top: 20px;
  157. float: right;
  158. border: 1px solid var(--light-gray);
  159. "
  160. >
  161. Save
  162. </button>
  163. </div>
  164. </fieldset>
  165. </form>
  166. </div>
  167. </div>
  168. {% endif %}
  169. </div>
  170. <div class="col-md-8">
  171. <div class="card">
  172. <h3>Account</h3>
  173. <small>Account: {{ account.name }}</small>
  174. <div class="table-responsive">
  175. <table class="table table-striped">
  176. <tbody>
  177. <tr>
  178. <td>ID</td>
  179. <td>{{ account.id }}</td>
  180. </tr>
  181. <tr>
  182. <td>Roles</td>
  183. <td>
  184. {{ account.account_roles | map(attribute='name') | join(", ")
  185. }}
  186. </td>
  187. </tr>
  188. {% if account.consultancy_account_name %}
  189. <tr>
  190. <td>Consultancy</td>
  191. <td>{{ account.consultancy_account_name }}</td>
  192. </tr>
  193. {% endif %} {% if account.primary_color %}
  194. <tr>
  195. <td>Primary Color</td>
  196. <td>
  197. <div
  198. style="
  199. width: 20px;
  200. height: 20px;
  201. background-color: {{ account.primary_color }};
  202. display: inline-block;
  203. "
  204. ></div>
  205. </td>
  206. </tr>
  207. {% endif %} {% if account.secondary_color %}
  208. <tr>
  209. <td>Secondary Color</td>
  210. <td>
  211. <div
  212. style="
  213. width: 20px;
  214. height: 20px;
  215. background-color: {{ account.secondary_color }};
  216. display: inline-block;
  217. "
  218. ></div>
  219. </td>
  220. </tr>
  221. {% endif %} {% if account.logo_url %}
  222. <tr>
  223. <td>Logo URL</td>
  224. <td>
  225. <img
  226. src="{{ account.logo_url }}"
  227. alt="Logo"
  228. style="max-width: 100px"
  229. />
  230. </td>
  231. </tr>
  232. {% endif %}
  233. </tbody>
  234. </table>
  235. </div>
  236. </div>
  237. <div class="card">
  238. <h3 id="usersTableTitle">All users</h3>
  239. <div class="form-check form-check-inline">
  240. <label class="form-check-label">
  241. <input
  242. id="inactiveUsersCheckbox"
  243. name="include_inactive"
  244. type="checkbox"
  245. />
  246. Include inactive
  247. </label>
  248. </div>
  249. <div class="table-responsive">
  250. <table
  251. class="table table-striped paginate nav-on-click"
  252. title="View this user"
  253. id="usersTable"
  254. ></table>
  255. </div>
  256. </div>
  257. <div class="card">
  258. <h3>Assets</h3>
  259. <div class="table-responsive">
  260. <table
  261. class="table table-striped paginate nav-on-click"
  262. title="View this asset"
  263. id="assetTable"
  264. ></table>
  265. </div>
  266. </div>
  267. </div>
  268. <div class="col-md-2"></div>
  269. </div>
  270. </div>
  271. <script>
  272. function User(
  273. id,
  274. username,
  275. email,
  276. roles,
  277. account,
  278. timezone,
  279. lastLogin,
  280. lastSeen,
  281. active
  282. ) {
  283. this.id = id;
  284. this.username = `<span>${username}</span>`;
  285. this.email = `<a href="mailto:${email}" title="Mail this user">${email}</a>`;
  286. this.roles = roles.map((role) => role).join(", ");
  287. this.url = `/users/${id}`;
  288. if (account == null) this.account = "PUBLIC";
  289. else
  290. this.account = `
  291. <a href="/accounts/${account["id"]}" title="View this account">${account["name"]}</a>
  292. `;
  293. this.timezone = timezone;
  294. this.lastLogin = lastLogin;
  295. this.lastSeen = lastSeen;
  296. this.active = active;
  297. }
  298. $(document).ready(function () {
  299. let includeInactive = false;
  300. const tableTitle = $("#usersTableTitle");
  301. // Initialize the DataTable
  302. const table = $("#usersTable").dataTable({
  303. order: [
  304. [0, "asc"]
  305. ],
  306. serverSide: true,
  307. // make the table row vertically aligned with header
  308. columns: [{
  309. data: "username",
  310. title: "Username",
  311. orderable: true
  312. },
  313. {
  314. data: "email",
  315. title: "Email",
  316. orderable: true
  317. },
  318. {
  319. data: "roles",
  320. title: "Roles",
  321. orderable: false
  322. },
  323. {
  324. data: "account",
  325. title: "Account",
  326. orderable: false
  327. },
  328. {
  329. data: "timezone",
  330. title: "Timezone",
  331. orderable: false
  332. },
  333. {
  334. data: "lastLogin",
  335. title: "Last login",
  336. orderable: true
  337. },
  338. {
  339. data: "lastSeen",
  340. title: "Last seen",
  341. orderable: true
  342. },
  343. {
  344. data: "active",
  345. title: "Active",
  346. orderable: false
  347. },
  348. {
  349. data: "url",
  350. title: "URL",
  351. className: "d-none"
  352. },
  353. ],
  354. ajax: function (data, callback, settings) {
  355. const basePath = window.location.origin;
  356. let filter = data["search"]["value"];
  357. let orderColumnIndex = data["order"][0]["column"]
  358. let orderDirection = data["order"][0]["dir"];
  359. let orderColumnName = data["columns"][orderColumnIndex]["data"];
  360. let url = `${basePath}/api/v3_0/users?page=${data["start"] / data["length"] + 1}&per_page=${data["length"]}&include_inactive=${includeInactive}&account_id={{ account.id }}`;
  361. if (filter.length > 0) {
  362. url = `${url}&filter=${filter}`;
  363. }
  364. if (orderColumnName) {
  365. url = `${url}&sort_by=${orderColumnName}&sort_dir=${orderDirection}`;
  366. }
  367. $.ajax({
  368. type: "get",
  369. url: url,
  370. success: function (response, text) {
  371. let clean_response = [];
  372. response["data"].forEach((element) =>
  373. clean_response.push(
  374. new User(
  375. element["id"],
  376. element["username"],
  377. element["email"],
  378. element["flexmeasures_roles"],
  379. element["account"],
  380. element["timezone"],
  381. element["last_login_at"],
  382. element["last_seen_at"],
  383. element["active"]
  384. )
  385. )
  386. );
  387. callback({
  388. data: clean_response,
  389. recordsTotal: response["num-records"],
  390. recordsFiltered: response["filtered-records"],
  391. });
  392. },
  393. error: function (request, status, error) {
  394. console.log("Error: ", error);
  395. },
  396. });
  397. },
  398. });
  399. // Event listener for the checkbox to toggle includeInactive state
  400. $("#inactiveUsersCheckbox").change(function () {
  401. includeInactive = this.checked;
  402. table.api().ajax.reload();
  403. if (includeInactive) {
  404. tableTitle.text("All users");
  405. } else {
  406. tableTitle.text("All active users");
  407. }
  408. });
  409. });
  410. const asset_icon_map = JSON.parse("{{ asset_icon_map | tojson | safe }}");
  411. function Asset(id, name, account, latitude, longitude, sensors, asset_type) {
  412. let icon = asset_icon_map[asset_type.toLowerCase()];
  413. if (icon === undefined) icon = `icon-${asset_type}`;
  414. this.name = `
  415. <i class="${icon} left-icon">${name}</i>
  416. `;
  417. this.id = id;
  418. this.location = "";
  419. this.url = `/assets/${id}`;
  420. this.status = `
  421. <a href="/assets/${id}/status">
  422. <button type="button" class="btn btn-primary">Status</button>
  423. </a>
  424. `;
  425. if (account == null) this.owner = "PUBLIC";
  426. else
  427. this.owner = `
  428. <a href="/accounts/${account["id"]}" title="View this account">${account["name"]}</a>
  429. `;
  430. this.num_sensors = sensors.length;
  431. if (latitude != null && longitude != null)
  432. this.location = `LAT: ${latitude}, LONG: ${longitude}`;
  433. }
  434. $(document).ready(function () {
  435. $("#assetTable").dataTable({
  436. order: [[1, "asc"]],
  437. serverSide: true,
  438. columns: [
  439. { data: "id", title: "Asset ID" },
  440. {data: "name", title: "Name", orderable: true},
  441. {data: "owner", title: "Account", orderable: true},
  442. {data: "location", title: "Location", orderable: false},
  443. {data: "num_sensors", title: "Sensors", orderable: false},
  444. {data: "status", title: "Status", orderable: false},
  445. { data: "url", title: "URL", className: "d-none" },
  446. ],
  447. ajax: function (data, callback, settings) {
  448. const basePath = window.location.origin;
  449. let filter = data["search"]["value"];
  450. let orderColumnIndex = data["order"][0]["column"]
  451. let orderDirection = data["order"][0]["dir"];
  452. let orderColumnName = data["columns"][orderColumnIndex]["data"];
  453. let url = `${basePath}/api/v3_0/assets?page=${
  454. Math.floor(data["start"] / data["length"]) + 1
  455. }&per_page=${data["length"]}&include_public=true&account_id=${
  456. {{ account.id }}
  457. }`;
  458. if (filter.length > 0) {
  459. url = `${url}&filter=${filter}`;
  460. }
  461. if (orderColumnName){
  462. url = `${url}&sort_by=${orderColumnName}&sort_dir=${orderDirection}`;
  463. }
  464. $.ajax({
  465. type: "get",
  466. url: url,
  467. success: function (response, text) {
  468. let clean_response = [];
  469. response["data"].forEach((element) =>
  470. clean_response.push(
  471. new Asset(
  472. element["id"],
  473. element["name"],
  474. element["owner"],
  475. element["latitude"],
  476. element["longitude"],
  477. element["sensors"],
  478. element["generic_asset_type"]["name"]
  479. )
  480. )
  481. );
  482. callback({
  483. data: clean_response,
  484. recordsTotal: response["num-records"],
  485. recordsFiltered: response["filtered-records"],
  486. });
  487. },
  488. error: function (request, status, error) {
  489. console.log("Error: ", error);
  490. },
  491. });
  492. },
  493. });
  494. });
  495. </script>
  496. <script defer>
  497. let currentPage = 1;
  498. const basePath = window.location.origin;
  499. const form = document.getElementById("editaccount");
  500. const tableBody = document.getElementById("users-table-body");
  501. const paginationControls = document.getElementById("pagination-controls");
  502. form.addEventListener("submit", function (event) {
  503. event.preventDefault(); // Prevent the default form submission
  504. // Collect form data
  505. const formData = new FormData(event.target);
  506. // create json payload from formData and set empty string to null
  507. let payload;
  508. payload = JSON.stringify(
  509. Object.fromEntries(
  510. Array.from(formData.entries()).map(([key, value]) => [
  511. key,
  512. value === "" ? null : value,
  513. ])
  514. )
  515. );
  516. // Make a PATCH request to the API
  517. fetch(basePath + "/api/v3_0/accounts/" + "{{account.id}}", {
  518. method: "PATCH",
  519. headers: {
  520. "Content-Type": "application/json",
  521. },
  522. body: payload,
  523. })
  524. .then((response) => response.json())
  525. .then((data) => {
  526. if (data.status == 200) {
  527. showToast("Account updated successfully!", "success");
  528. } else {
  529. if (data.message && typeof data.message === "string") {
  530. showToast(data.message, "error");
  531. } else {
  532. const errors = data.message.json;
  533. for (const key in errors) {
  534. showToast(`${key}: ${errors[key]}`, "error");
  535. }
  536. }
  537. }
  538. });
  539. });
  540. </script>
  541. {% block paginate_tables_script %} {{ super() }} {% endblock %} {% endblock %}