update User, User group, IO modules
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-23 11:06:26 +07:00
parent 523f98b74c
commit 50a2587cef
32 changed files with 4728 additions and 52 deletions

View File

@@ -358,7 +358,9 @@
<p class="dashboardMirWidgetPanelNote" data-i18n="dashboard.menu.comingSoon">Widget nhóm này sẽ có trong bản cập nhật sau.</p>
</div>
<div class="dashboardMirWidgetPanel" data-panel="io" hidden>
<p class="dashboardMirWidgetPanelNote" data-i18n="dashboard.menu.comingSoon">I/O widgets — sắp có.</p>
<button type="button" class="dashboardMirWidgetPick" data-add-widget="io_connect" data-i18n="dashboard.widget.io_connect">Connect I/O module</button>
<button type="button" class="dashboardMirWidgetPick" data-add-widget="io_status" data-i18n="dashboard.widget.io_status">I/O status</button>
<button type="button" class="dashboardMirWidgetPick" data-add-widget="io_configuration" data-i18n="dashboard.widget.io_configuration">I/O configuration</button>
</div>
<div class="dashboardMirWidgetPanel" data-panel="misc" hidden>
<button type="button" class="dashboardMirWidgetPick" data-add-widget="robot_summary" data-i18n="dashboard.widget.robot_summary">Robot summary</button>
@@ -1069,6 +1071,323 @@
</dialog>
</div>
<div class="page" id="pageUserGroups" data-page-content="user-groups" hidden>
<div id="userGroupsListView" class="mapsMirPage">
<header class="mapsMirHeader">
<div class="mapsMirHeaderText">
<h1 class="mapsMirTitle" data-i18n="userGroups.title">User groups</h1>
<p class="mapsMirSubtitle">
<span data-i18n="userGroups.subtitle">Create and edit user groups.</span>
<button type="button" class="mapsMirHelpBtn" id="userGroupsHelpBtn" data-i18n-title="userGroups.helpTitle" aria-label="Help">
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/><text x="8" y="11.5" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">?</text></svg>
</button>
</p>
</div>
<div class="mapsMirHeaderActions">
<button type="button" class="mapsMirBtn mapsMirBtn--green" id="userGroupCreateBtn">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M7 1v12M1 7h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span data-i18n="userGroups.create">Create user group</span>
</button>
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="userGroupsClearFiltersBtn">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><circle cx="7" cy="7" r="5.5" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M4.5 4.5l5 5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
<span data-i18n="userGroups.clearFilters">Clear filters</span>
</button>
</div>
</header>
<div class="mapsMirFilterBar">
<label class="mapsMirFilterLabel" for="userGroupsFilterInput" data-i18n="userGroups.filterLabel">Filter:</label>
<input type="search" id="userGroupsFilterInput" class="mapsMirFilterInput" data-i18n-placeholder="userGroups.filterPlaceholder" placeholder="Filter by name..." autocomplete="off" />
<span id="userGroupsFilterCount" class="mapsMirFilterCount">0 item(s) found</span>
<div class="mapsMirPager">
<button type="button" class="mapsMirPageBtn" id="userGroupsPageFirst" aria-label="First page">&laquo;</button>
<button type="button" class="mapsMirPageBtn" id="userGroupsPagePrev" aria-label="Previous page">&lsaquo;</button>
<span id="userGroupsPageLabel" class="mapsMirPageLabel">Page 1 of 1</span>
<button type="button" class="mapsMirPageBtn" id="userGroupsPageNext" aria-label="Next page">&rsaquo;</button>
<button type="button" class="mapsMirPageBtn" id="userGroupsPageLast" aria-label="Last page">&raquo;</button>
</div>
</div>
<div class="mapsMirTableWrap">
<table class="mapsMirTable mapsMirTable--userGroups" id="userGroupsTable">
<thead>
<tr>
<th class="userGroupsMirThIcon" aria-hidden="true"></th>
<th data-i18n="userGroups.colName">Name</th>
<th data-i18n="userGroups.colUsers">Users</th>
<th data-i18n="userGroups.colPin">PIN sign-in</th>
<th data-i18n="userGroups.colPermissions">Permissions</th>
<th class="mapsMirThFunctions" data-i18n="userGroups.colFunctions">Functions</th>
</tr>
</thead>
<tbody id="userGroupList"></tbody>
</table>
<div id="userGroupListEmpty" class="mapsMirEmpty" hidden data-i18n="userGroups.empty">No user groups yet.</div>
</div>
</div>
<dialog id="userGroupEditDialog" class="mapsMirDialog mapsMirDialog--wide">
<form id="userGroupEditForm" method="dialog">
<h2 class="mapsMirDialogTitle" id="userGroupEditTitle" data-i18n="userGroups.createTitle">Create user group</h2>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="userGroups.fieldName">Name</span>
<input type="text" id="userGroupEditName" required />
</label>
<label class="mapsMirField mapsMirField--checkbox">
<input type="checkbox" id="userGroupEditAllowPin" />
<span data-i18n="userGroups.fieldAllowPin">Allow PIN sign-in</span>
</label>
<div class="userGroupsPermSection">
<h3 class="userGroupsPermTitle" data-i18n="userGroups.permissionsTitle">Permissions</h3>
<p class="mapsMirFieldHint" data-i18n="userGroups.permissionsHint">Set read or write access per module. None hides the module from the menu.</p>
<table class="userGroupsPermTable">
<thead>
<tr>
<th data-i18n="userGroups.permColModule">Module</th>
<th data-i18n="userGroups.permColAccess">Access</th>
</tr>
</thead>
<tbody id="userGroupEditPermsBody"></tbody>
</table>
</div>
<div class="mapsMirDialogFooter">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="userGroupEditCancelBtn" data-i18n="common.cancel">Cancel</button>
<button type="button" class="mapsMirBtn mapsMirBtn--danger" id="userGroupEditDeleteBtn" hidden data-i18n="common.delete">Delete</button>
<button type="submit" class="mapsMirBtn mapsMirBtn--green" id="userGroupEditSaveBtn" data-i18n="userGroups.save">Save</button>
</div>
</form>
</dialog>
<dialog id="userGroupDeleteConfirmDialog" class="mapsMirDialog">
<div class="mapsMirDialogPanel">
<h2 class="mapsMirDialogTitle" data-i18n="userGroups.deleteTitle">Delete user group?</h2>
<p id="userGroupDeleteConfirmText" class="mapsMirDialogText"></p>
<p class="mapsMirDialogHint" data-i18n="userGroups.deleteHint">Users must be moved to another group before this group can be deleted.</p>
<div class="mapsMirDialogFooter">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="userGroupDeleteCancelBtn" data-i18n="common.no">No</button>
<button type="button" class="mapsMirBtn mapsMirBtn--danger" id="userGroupDeleteYesBtn" data-i18n="common.yes">Yes</button>
</div>
</div>
</dialog>
</div>
<div class="page" id="pageIoModules" data-page-content="io-modules" hidden>
<div id="ioModulesListView" class="mapsMirPage">
<header class="mapsMirHeader">
<div class="mapsMirHeaderText">
<h1 class="mapsMirTitle" data-i18n="ioModules.title">I/O modules</h1>
<p class="mapsMirSubtitle">
<span data-i18n="ioModules.subtitle">Create and manage I/O connections.</span>
<button type="button" class="mapsMirHelpBtn" id="ioModulesHelpBtn" data-i18n-title="ioModules.helpTitle" aria-label="Help">
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/><text x="8" y="11.5" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">?</text></svg>
</button>
</p>
</div>
<div class="mapsMirHeaderActions">
<button type="button" class="mapsMirBtn mapsMirBtn--green" id="ioModuleCreateBtn">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M7 1v12M1 7h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span data-i18n="ioModules.create">Create I/O connection</span>
</button>
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="ioModulesClearFiltersBtn">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><circle cx="7" cy="7" r="5.5" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M4.5 4.5l5 5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
<span data-i18n="ioModules.clearFilters">Clear filters</span>
</button>
</div>
</header>
<div class="mapsMirFilterBar">
<label class="mapsMirFilterLabel" for="ioModulesFilterInput" data-i18n="ioModules.filterLabel">Filter:</label>
<input type="search" id="ioModulesFilterInput" class="mapsMirFilterInput" data-i18n-placeholder="ioModules.filterPlaceholder" placeholder="Filter by name or IP..." autocomplete="off" />
<span id="ioModulesFilterCount" class="mapsMirFilterCount">0 item(s) found</span>
<div class="mapsMirPager">
<button type="button" class="mapsMirPageBtn" id="ioModulesPageFirst" aria-label="First page">&laquo;</button>
<button type="button" class="mapsMirPageBtn" id="ioModulesPagePrev" aria-label="Previous page">&lsaquo;</button>
<span id="ioModulesPageLabel" class="mapsMirPageLabel">Page 1 of 1</span>
<button type="button" class="mapsMirPageBtn" id="ioModulesPageNext" aria-label="Next page">&rsaquo;</button>
<button type="button" class="mapsMirPageBtn" id="ioModulesPageLast" aria-label="Last page">&raquo;</button>
</div>
</div>
<div class="mapsMirTableWrap">
<table class="mapsMirTable mapsMirTable--ioModules" id="ioModulesTable">
<thead>
<tr>
<th class="ioModulesMirThIcon" aria-hidden="true"></th>
<th data-i18n="ioModules.colName">Name</th>
<th data-i18n="ioModules.colType">Type</th>
<th data-i18n="ioModules.colIp">IP address</th>
<th data-i18n="ioModules.colStatus">Status</th>
<th class="mapsMirThFunctions" data-i18n="ioModules.colFunctions">Functions</th>
</tr>
</thead>
<tbody id="ioModuleList"></tbody>
</table>
<div id="ioModuleListEmpty" class="mapsMirEmpty" hidden data-i18n="ioModules.empty">No I/O modules yet.</div>
</div>
</div>
<dialog id="ioModuleEditDialog" class="mapsMirDialog">
<form id="ioModuleEditForm" method="dialog">
<h2 class="mapsMirDialogTitle" id="ioModuleEditTitle" data-i18n="ioModules.createTitle">Create I/O connection</h2>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="ioModules.fieldSite">Site</span>
<select id="ioModuleEditSite" required></select>
</label>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="ioModules.fieldName">Name</span>
<input type="text" id="ioModuleEditName" required autocomplete="off" />
</label>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="ioModules.fieldType">I/O module type</span>
<select id="ioModuleEditType" required>
<option value="bluetooth" data-i18n="ioModules.type.bluetooth">Bluetooth</option>
<option value="wise" data-i18n="ioModules.type.wise">WISE</option>
</select>
</label>
<p id="ioModuleEditTypeHint" class="mapsMirFieldHint" data-i18n="ioModules.typeHint.bluetooth">Output ports 14.</p>
<div class="mapsMirFieldRow mapsMirFieldRow--actions">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="ioModuleTestBtn" data-i18n="ioModules.testConnection">Test connection</button>
</div>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="ioModules.fieldIp">IP address</span>
<input type="text" id="ioModuleEditIp" required autocomplete="off" inputmode="decimal" />
</label>
<div class="mapsMirDialogFooter">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="ioModuleEditCancelBtn" data-i18n="common.cancel">Cancel</button>
<button type="button" class="mapsMirBtn mapsMirBtn--danger" id="ioModuleEditDeleteBtn" hidden data-i18n="common.delete">Delete</button>
<button type="submit" class="mapsMirBtn mapsMirBtn--green" id="ioModuleEditSaveBtn" data-i18n="ioModules.save">Create</button>
</div>
</form>
</dialog>
<dialog id="ioModuleDeleteConfirmDialog" class="mapsMirDialog">
<div class="mapsMirDialogPanel">
<h2 class="mapsMirDialogTitle" data-i18n="ioModules.deleteTitle">Delete I/O connection?</h2>
<p id="ioModuleDeleteConfirmText" class="mapsMirDialogText"></p>
<div class="mapsMirDialogFooter">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="ioModuleDeleteCancelBtn" data-i18n="common.no">No</button>
<button type="button" class="mapsMirBtn mapsMirBtn--danger" id="ioModuleDeleteYesBtn" data-i18n="common.yes">Yes</button>
</div>
</div>
</dialog>
</div>
<div class="page" id="pageUsers" data-page-content="users" hidden>
<div id="usersListView" class="mapsMirPage">
<header class="mapsMirHeader">
<div class="mapsMirHeaderText">
<h1 class="mapsMirTitle" data-i18n="users.title">Users</h1>
<p class="mapsMirSubtitle">
<span data-i18n="users.subtitle">Create and edit users.</span>
<button type="button" class="mapsMirHelpBtn" id="usersHelpBtn" data-i18n-title="users.helpTitle" aria-label="Help">
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/><text x="8" y="11.5" text-anchor="middle" font-size="10" font-weight="700" fill="currentColor">?</text></svg>
</button>
</p>
</div>
<div class="mapsMirHeaderActions">
<button type="button" class="mapsMirBtn mapsMirBtn--green" id="userCreateBtn">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M7 1v12M1 7h12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span data-i18n="users.create">Create user</span>
</button>
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="usersClearFiltersBtn">
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><circle cx="7" cy="7" r="5.5" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M4.5 4.5l5 5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
<span data-i18n="users.clearFilters">Clear filters</span>
</button>
</div>
</header>
<div class="mapsMirFilterBar">
<label class="mapsMirFilterLabel" for="usersFilterInput" data-i18n="users.filterLabel">Filter:</label>
<input type="search" id="usersFilterInput" class="mapsMirFilterInput" data-i18n-placeholder="users.filterPlaceholder" placeholder="Filter by name or username..." autocomplete="off" />
<span id="usersFilterCount" class="mapsMirFilterCount">0 item(s) found</span>
<div class="mapsMirPager">
<button type="button" class="mapsMirPageBtn" id="usersPageFirst" aria-label="First page">&laquo;</button>
<button type="button" class="mapsMirPageBtn" id="usersPagePrev" aria-label="Previous page">&lsaquo;</button>
<span id="usersPageLabel" class="mapsMirPageLabel">Page 1 of 1</span>
<button type="button" class="mapsMirPageBtn" id="usersPageNext" aria-label="Next page">&rsaquo;</button>
<button type="button" class="mapsMirPageBtn" id="usersPageLast" aria-label="Last page">&raquo;</button>
</div>
</div>
<div class="mapsMirTableWrap">
<table class="mapsMirTable mapsMirTable--users" id="usersTable">
<thead>
<tr>
<th class="usersMirThIcon" aria-hidden="true"></th>
<th data-i18n="users.colName">Name</th>
<th data-i18n="users.colUsername">Username</th>
<th data-i18n="users.colGroup">User group</th>
<th data-i18n="users.colEmail">Email</th>
<th data-i18n="users.colPin">PIN</th>
<th class="mapsMirThFunctions" data-i18n="users.colFunctions">Functions</th>
</tr>
</thead>
<tbody id="userList"></tbody>
</table>
<div id="userListEmpty" class="mapsMirEmpty" hidden data-i18n="users.empty">No users yet.</div>
</div>
</div>
<dialog id="userEditDialog" class="mapsMirDialog">
<form id="userEditForm" method="dialog">
<h2 class="mapsMirDialogTitle" id="userEditTitle" data-i18n="users.createTitle">Create user</h2>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="users.fieldName">Name</span>
<input type="text" id="userEditDisplayName" autocomplete="name" required />
</label>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="users.fieldUsername">Username</span>
<input type="text" id="userEditUsername" autocomplete="username" required />
</label>
<label class="mapsMirField" id="userEditPasswordField">
<span class="mapsMirFieldLabel" data-i18n="users.fieldPassword">Password</span>
<input type="password" id="userEditPassword" autocomplete="new-password" minlength="4" />
</label>
<p id="userEditPasswordHint" class="mapsMirFieldHint" hidden data-i18n="users.passwordEditHint">Users change their own password from the menu in the upper right corner.</p>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="users.fieldEmail">Email address</span>
<input type="email" id="userEditEmail" autocomplete="email" />
</label>
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="users.fieldGroup">User group</span>
<select id="userEditGroup" required></select>
</label>
<label class="mapsMirField mapsMirField--checkbox">
<input type="checkbox" id="userEditEnabled" checked />
<span data-i18n="users.fieldEnabled">Enabled</span>
</label>
<div id="userEditPinSection" class="mapsMirField">
<label class="mapsMirField mapsMirField--checkbox">
<input type="checkbox" id="userEditPinEnabled" />
<span data-i18n="users.fieldPinEnable">Allow PIN sign-in</span>
</label>
<label class="mapsMirField" id="userEditPinField" hidden>
<span class="mapsMirFieldLabel" data-i18n="users.fieldPin">PIN code (4 digits)</span>
<input type="text" id="userEditPin" inputmode="numeric" pattern="[0-9]{4}" maxlength="4" autocomplete="off" />
</label>
<span id="userEditPinHint" class="mapsMirFieldHint" hidden data-i18n="users.pinGroupHint">PIN is only available for user groups that allow PIN sign-in.</span>
</div>
<div class="mapsMirDialogFooter">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="userEditCancelBtn" data-i18n="common.cancel">Cancel</button>
<button type="button" class="mapsMirBtn mapsMirBtn--danger" id="userEditDeleteBtn" hidden data-i18n="common.delete">Delete</button>
<button type="submit" class="mapsMirBtn mapsMirBtn--green" id="userEditSaveBtn" data-i18n="users.save">Save</button>
</div>
</form>
</dialog>
<dialog id="userDeleteConfirmDialog" class="mapsMirDialog">
<div class="mapsMirDialogPanel">
<h2 class="mapsMirDialogTitle" data-i18n="users.deleteTitle">Delete user?</h2>
<p id="userDeleteConfirmText" class="mapsMirDialogText"></p>
<p class="mapsMirDialogHint" data-i18n="users.deleteHint">Only the user profile is removed. Maps, missions and other data created by this user remain unchanged.</p>
<div class="mapsMirDialogFooter">
<button type="button" class="mapsMirBtn mapsMirBtn--outline" id="userDeleteCancelBtn" data-i18n="common.no">No</button>
<button type="button" class="mapsMirBtn mapsMirBtn--danger" id="userDeleteYesBtn" data-i18n="common.yes">Yes</button>
</div>
</div>
</dialog>
</div>
<div class="page" id="pageMaps" data-page-content="maps" hidden>
<div id="mapsListView" class="mapsMirPage">
<header class="mapsMirHeader">
@@ -1666,10 +1985,7 @@
<label class="mapsMirField">
<span class="mapsMirFieldLabel" data-i18n="maps.editor.io.module">I/O module</span>
<input type="text" id="mapIoModule" list="mapIoModuleList" autocomplete="off" required />
<datalist id="mapIoModuleList">
<option value="GPIO module 1"></option>
<option value="PLC I/O 1"></option>
</datalist>
<datalist id="mapIoModuleList"></datalist>
</label>
<div class="mapsMirFieldRow">
<label class="mapsMirField">
@@ -2238,6 +2554,9 @@ GET /api/v2.0.0/status</pre>
<script src="/maps.js"></script>
<script src="/sounds.js"></script>
<script src="/transitions.js"></script>
<script src="/users.js"></script>
<script src="/user-groups.js"></script>
<script src="/io-modules.js"></script>
<script src="/map-editor.js"></script>
<script src="/topbar.js"></script>
<script src="/dashboard.js"></script>