Feature: Held Devices Noticeboard
Provides a noticeboard for all devices, not just those assigned to users.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
@model IEnumerable<Disco.Web.Areas.Public.Models.UserHeldDevices.UserHeldDeviceModel>
|
||||
@{
|
||||
ViewBag.Title = Html.ToBreadcrumb("Public Reports", MVC.Public.Public.Index(), "Technician Held Devices", null);
|
||||
Html.BundleDeferred("~/Style/Public/UserHeldDevices");
|
||||
ViewBag.Title = Html.ToBreadcrumb("Public Reports", MVC.Public.Public.Index(), "Held Devices for Users", null);
|
||||
Html.BundleDeferred("~/Style/Public/HeldDevices");
|
||||
}
|
||||
<div class="clearfix page">
|
||||
<div class="column1">
|
||||
@@ -14,10 +14,10 @@
|
||||
@foreach (var item in DevicesInProcess)
|
||||
{
|
||||
<tr>
|
||||
<td class="userId">
|
||||
<td class="id">
|
||||
@item.UserId
|
||||
</td>
|
||||
<td class="userDisplayName">
|
||||
<td class="description">
|
||||
@item.UserDisplayName@{
|
||||
if (!string.IsNullOrEmpty(item.EstimatedReturnTime))
|
||||
{
|
||||
@@ -39,10 +39,10 @@
|
||||
@foreach (var item in WaitingForUserActionJobs)
|
||||
{
|
||||
<tr>
|
||||
<td class="userId">
|
||||
<td class="id">
|
||||
@item.UserId
|
||||
</td>
|
||||
<td class="userDisplayName">
|
||||
<td class="description">
|
||||
@item.UserDisplayName
|
||||
</td>
|
||||
<td class="timestamp@(item.IsAlert ? " Alert" : string.Empty)">
|
||||
@@ -61,10 +61,10 @@
|
||||
@foreach (var item in DevicesReadyForReturn)
|
||||
{
|
||||
<tr>
|
||||
<td class="userId">
|
||||
<td class="id">
|
||||
@item.UserId
|
||||
</td>
|
||||
<td class="userDisplayName">
|
||||
<td class="description">
|
||||
@item.UserDisplayName
|
||||
</td>
|
||||
<td class="timestamp@(item.IsAlert ? " Alert" : string.Empty)">
|
||||
|
||||
@@ -43,8 +43,8 @@ namespace Disco.Web.Areas.Public.Views.UserHeldDevices
|
||||
|
||||
#line 2 "..\..\Areas\Public\Views\UserHeldDevices\Index.cshtml"
|
||||
|
||||
ViewBag.Title = Html.ToBreadcrumb("Public Reports", MVC.Public.Public.Index(), "Technician Held Devices", null);
|
||||
Html.BundleDeferred("~/Style/Public/UserHeldDevices");
|
||||
ViewBag.Title = Html.ToBreadcrumb("Public Reports", MVC.Public.Public.Index(), "Held Devices for Users", null);
|
||||
Html.BundleDeferred("~/Style/Public/HeldDevices");
|
||||
|
||||
|
||||
#line default
|
||||
@@ -104,7 +104,7 @@ WriteLiteral(">\r\n");
|
||||
#line hidden
|
||||
WriteLiteral(" <tr>\r\n <td");
|
||||
|
||||
WriteLiteral(" class=\"userId\"");
|
||||
WriteLiteral(" class=\"id\"");
|
||||
|
||||
WriteLiteral(">\r\n");
|
||||
|
||||
@@ -119,7 +119,7 @@ WriteLiteral(" ");
|
||||
#line hidden
|
||||
WriteLiteral("\r\n </td>\r\n <td");
|
||||
|
||||
WriteLiteral(" class=\"userDisplayName\"");
|
||||
WriteLiteral(" class=\"description\"");
|
||||
|
||||
WriteLiteral(">\r\n");
|
||||
|
||||
@@ -223,7 +223,7 @@ WriteLiteral(">\r\n");
|
||||
#line hidden
|
||||
WriteLiteral(" <tr>\r\n <td");
|
||||
|
||||
WriteLiteral(" class=\"userId\"");
|
||||
WriteLiteral(" class=\"id\"");
|
||||
|
||||
WriteLiteral(">\r\n");
|
||||
|
||||
@@ -238,7 +238,7 @@ WriteLiteral(" ");
|
||||
#line hidden
|
||||
WriteLiteral("\r\n </td>\r\n <td");
|
||||
|
||||
WriteLiteral(" class=\"userDisplayName\"");
|
||||
WriteLiteral(" class=\"description\"");
|
||||
|
||||
WriteLiteral(">\r\n");
|
||||
|
||||
@@ -253,15 +253,15 @@ WriteLiteral(" ");
|
||||
#line hidden
|
||||
WriteLiteral("\r\n </td>\r\n <td");
|
||||
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 1816), Tuple.Create("\"", 1874)
|
||||
, Tuple.Create(Tuple.Create("", 1824), Tuple.Create("timestamp", 1824), true)
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 1795), Tuple.Create("\"", 1853)
|
||||
, Tuple.Create(Tuple.Create("", 1803), Tuple.Create("timestamp", 1803), true)
|
||||
|
||||
#line 48 "..\..\Areas\Public\Views\UserHeldDevices\Index.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 1833), Tuple.Create<System.Object, System.Int32>(item.IsAlert ? " Alert" : string.Empty
|
||||
, Tuple.Create(Tuple.Create("", 1812), Tuple.Create<System.Object, System.Int32>(item.IsAlert ? " Alert" : string.Empty
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 1833), false)
|
||||
, 1812), false)
|
||||
);
|
||||
|
||||
WriteLiteral(">\r\n Since ");
|
||||
@@ -329,7 +329,7 @@ WriteLiteral(">\r\n");
|
||||
#line hidden
|
||||
WriteLiteral(" <tr>\r\n <td");
|
||||
|
||||
WriteLiteral(" class=\"userId\"");
|
||||
WriteLiteral(" class=\"id\"");
|
||||
|
||||
WriteLiteral(">\r\n");
|
||||
|
||||
@@ -344,7 +344,7 @@ WriteLiteral(" ");
|
||||
#line hidden
|
||||
WriteLiteral("\r\n </td>\r\n <td");
|
||||
|
||||
WriteLiteral(" class=\"userDisplayName\"");
|
||||
WriteLiteral(" class=\"description\"");
|
||||
|
||||
WriteLiteral(">\r\n");
|
||||
|
||||
@@ -359,15 +359,15 @@ WriteLiteral(" ");
|
||||
#line hidden
|
||||
WriteLiteral("\r\n </td>\r\n <td");
|
||||
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 2641), Tuple.Create("\"", 2699)
|
||||
, Tuple.Create(Tuple.Create("", 2649), Tuple.Create("timestamp", 2649), true)
|
||||
WriteAttribute("class", Tuple.Create(" class=\"", 2612), Tuple.Create("\"", 2670)
|
||||
, Tuple.Create(Tuple.Create("", 2620), Tuple.Create("timestamp", 2620), true)
|
||||
|
||||
#line 70 "..\..\Areas\Public\Views\UserHeldDevices\Index.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 2658), Tuple.Create<System.Object, System.Int32>(item.IsAlert ? " Alert" : string.Empty
|
||||
, Tuple.Create(Tuple.Create("", 2629), Tuple.Create<System.Object, System.Int32>(item.IsAlert ? " Alert" : string.Empty
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 2658), false)
|
||||
, 2629), false)
|
||||
);
|
||||
|
||||
WriteLiteral(">\r\n Ready ");
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
Layout = null;
|
||||
Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR");
|
||||
Html.BundleDeferred("~/ClientScripts/Core");
|
||||
Html.BundleDeferred("~/Style/Public/UserHeldDevicesNoticeboard");
|
||||
Html.BundleDeferred("~/Style/Public/HeldDevicesNoticeboard");
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Disco - Technician Held Devices</title>
|
||||
<title>Disco - Technician Held Devices for Users</title>
|
||||
@Html.BundleRenderDeferred()
|
||||
</head>
|
||||
<body>
|
||||
<div id="page">
|
||||
<header id="mainHeader">
|
||||
Technician Held Devices
|
||||
Technician Held Devices for Users
|
||||
</header>
|
||||
<section id="mainSection">
|
||||
<div id="inProcess" class="list">
|
||||
@@ -368,33 +368,12 @@
|
||||
|
||||
var removeModel = function (model) {
|
||||
if (model) {
|
||||
if (model.updateAtToken) {
|
||||
window.clearTimeout(model.updateAtToken);
|
||||
};
|
||||
model.htmlLi.slideUp('fast', function () {
|
||||
model.htmlLi.remove();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleModelUpdate = function (model) {
|
||||
if (model.updateAtToken) {
|
||||
window.clearTimeout(model.updateAtToken);
|
||||
};
|
||||
if (model.UpdateAt) {
|
||||
if (typeof model.UpdateAt == 'string' && model.UpdateAt.indexOf('\/Date(') == 0) {
|
||||
model.UpdateAt = new Date(parseInt(model.UpdateAt.substr(6)));
|
||||
}
|
||||
var nowMilliseconds = new Date().getTime();
|
||||
var updateAtMilliseconds = (model.UpdateAt - nowMilliseconds);
|
||||
if (updateAtMilliseconds > 0) {
|
||||
model.updateAtToken = window.setTimeout(function () { updatedModel(model.UserId); }, updateAtMilliseconds);
|
||||
} else {
|
||||
model.UpdateAt = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var processModel = function (id, model, init) {
|
||||
if (!calculateFilter(model)) {
|
||||
removeModel(models[id]);
|
||||
@@ -428,9 +407,6 @@
|
||||
model.htmlLi.slideDown();
|
||||
}
|
||||
} else {
|
||||
if (existing.updateAtToken) {
|
||||
window.clearTimeout(existing.updateAtToken);
|
||||
};
|
||||
model.htmlLi = existing.htmlLi;
|
||||
model.htmlLi.slideUp('fast', function () {
|
||||
model.htmlLi.html(model.htmlContent).slideDown();
|
||||
@@ -448,7 +424,6 @@
|
||||
if (model.htmlLi && model.IsAlert) {
|
||||
model.htmlLi.addClass('alert');
|
||||
}
|
||||
scheduleModelUpdate(model);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace Disco.Web.Areas.Public.Views.UserHeldDevices
|
||||
Layout = null;
|
||||
Html.BundleDeferred("~/ClientScripts/Modules/jQuery-SignalR");
|
||||
Html.BundleDeferred("~/ClientScripts/Core");
|
||||
Html.BundleDeferred("~/Style/Public/UserHeldDevicesNoticeboard");
|
||||
Html.BundleDeferred("~/Style/Public/HeldDevicesNoticeboard");
|
||||
|
||||
|
||||
#line default
|
||||
@@ -61,7 +61,7 @@ WriteLiteral(" http-equiv=\"X-UA-Compatible\"");
|
||||
|
||||
WriteLiteral(" content=\"IE=edge\"");
|
||||
|
||||
WriteLiteral(" />\r\n <title>Disco - Technician Held Devices</title>\r\n");
|
||||
WriteLiteral(" />\r\n <title>Disco - Technician Held Devices for Users</title>\r\n");
|
||||
|
||||
WriteLiteral(" ");
|
||||
|
||||
@@ -80,7 +80,7 @@ WriteLiteral(">\r\n <header");
|
||||
|
||||
WriteLiteral(" id=\"mainHeader\"");
|
||||
|
||||
WriteLiteral(">\r\n Technician Held Devices\r\n </header>\r\n <section");
|
||||
WriteLiteral(">\r\n Technician Held Devices for Users\r\n </header>\r\n <section");
|
||||
|
||||
WriteLiteral(" id=\"mainSection\"");
|
||||
|
||||
@@ -359,64 +359,48 @@ WriteLiteral(">\r\n $(function () {\r\n var models = {};\r\n
|
||||
" } else {\r\n insertTo(model, $inProcessContent);\r\n " +
|
||||
" window.setTimeout(updateScrollInProcess, 100);\r\n " +
|
||||
" }\r\n }\r\n }\r\n\r\n var removeModel = " +
|
||||
"function (model) {\r\n if (model) {\r\n if (model." +
|
||||
"updateAtToken) {\r\n window.clearTimeout(model.updateAtToke" +
|
||||
"n);\r\n };\r\n model.htmlLi.slideUp(\'fast\', fu" +
|
||||
"nction () {\r\n model.htmlLi.remove();\r\n " +
|
||||
" });\r\n }\r\n };\r\n\r\n var scheduleModelUpdate =" +
|
||||
" function (model) {\r\n if (model.updateAtToken) {\r\n " +
|
||||
" window.clearTimeout(model.updateAtToken);\r\n };\r\n " +
|
||||
" if (model.UpdateAt) {\r\n if (typeof model.UpdateAt == \'str" +
|
||||
"ing\' && model.UpdateAt.indexOf(\'\\/Date(\') == 0) {\r\n model" +
|
||||
".UpdateAt = new Date(parseInt(model.UpdateAt.substr(6)));\r\n }" +
|
||||
"\r\n var nowMilliseconds = new Date().getTime();\r\n " +
|
||||
" var updateAtMilliseconds = (model.UpdateAt - nowMilliseconds);\r\n " +
|
||||
" if (updateAtMilliseconds > 0) {\r\n model.update" +
|
||||
"AtToken = window.setTimeout(function () { updatedModel(model.UserId); }, updateA" +
|
||||
"tMilliseconds);\r\n } else {\r\n model.Upd" +
|
||||
"ateAt = null;\r\n }\r\n }\r\n };\r\n\r\n " +
|
||||
" var processModel = function (id, model, init) {\r\n if (!cal" +
|
||||
"culateFilter(model)) {\r\n removeModel(models[id]);\r\n " +
|
||||
" delete models[id];\r\n sortModels();\r\n " +
|
||||
" } else {\r\n var existing = models[id];\r\n m" +
|
||||
"odels[id] = model;\r\n\r\n // Add\r\n model.html" +
|
||||
"Content = $(\'<div>\').text(model.UserId + \' - \' + model.UserDisplayName);\r\n " +
|
||||
" if (!model.ReadyForReturn && model.EstimatedReturnTime) {\r\n " +
|
||||
" model.htmlContent.append($(\'<span class=\"small\">\').text(\' (Expe" +
|
||||
"cted: \' + model.EstimatedReturnTime + \')\'));\r\n }\r\n " +
|
||||
" if (model.WaitingForUserAction) {\r\n model.htmlCo" +
|
||||
"ntent.append($(\'<span class=\"small\">\').text(\' (Since \' + model.WaitingForUserAct" +
|
||||
"ionSince + \')\'));\r\n } else {\r\n if (mod" +
|
||||
"el.ReadyForReturn && model.ReadyForReturnSince) {\r\n m" +
|
||||
"odel.htmlContent.append($(\'<span class=\"small\">\').text(\' (Ready \' + model.ReadyF" +
|
||||
"orReturnSince + \')\'));\r\n }\r\n }\r\n\r\n " +
|
||||
" if (existing) {\r\n if (existing.ReadyForRe" +
|
||||
"turn != model.ReadyForReturn || existing.WaitingForUserAction != model.WaitingFo" +
|
||||
"rUserAction) {\r\n removeModel(existing);\r\n " +
|
||||
" model.htmlLi = $(\'<li>\').html(model.htmlContent).data(\'ModelId\'," +
|
||||
" id).hide();\r\n modelInsert(model);\r\n " +
|
||||
" if (init) {\r\n model.htmlLi.fadeIn();\r\n" +
|
||||
" } else {\r\n model.html" +
|
||||
"Li.slideDown();\r\n }\r\n } else {" +
|
||||
"\r\n if (existing.updateAtToken) {\r\n " +
|
||||
" window.clearTimeout(existing.updateAtToken);\r\n " +
|
||||
" };\r\n model.htmlLi = existing.htmlLi;\r\n " +
|
||||
" model.htmlLi.slideUp(\'fast\', function () {\r\n " +
|
||||
" model.htmlLi.html(model.htmlContent).slideDown();\r\n " +
|
||||
" });\r\n }\r\n } else {\r\n " +
|
||||
" model.htmlLi = $(\'<li>\').html(model.htmlContent).data(\'Mode" +
|
||||
"lId\', id).hide();\r\n modelInsert(model);\r\n " +
|
||||
" if (init) {\r\n model.htmlLi.fadeIn();\r\n " +
|
||||
" } else {\r\n model.htmlLi.slideDown(\'s" +
|
||||
"low\');\r\n }\r\n }\r\n if" +
|
||||
" (model.htmlLi && model.IsAlert) {\r\n model.htmlLi.addClas" +
|
||||
"s(\'alert\');\r\n }\r\n scheduleModelUpdate(mode" +
|
||||
"l);\r\n }\r\n };\r\n\r\n var updatedModel = functio" +
|
||||
"n (id) {\r\n var userId = id.toString();\r\n\r\n $.ajax(" +
|
||||
"{\r\n dataType: \'json\',\r\n url: \'");
|
||||
"function (model) {\r\n if (model) {\r\n model.html" +
|
||||
"Li.slideUp(\'fast\', function () {\r\n model.htmlLi.remove();" +
|
||||
"\r\n });\r\n }\r\n };\r\n\r\n var " +
|
||||
"processModel = function (id, model, init) {\r\n if (!calculateFilte" +
|
||||
"r(model)) {\r\n removeModel(models[id]);\r\n d" +
|
||||
"elete models[id];\r\n sortModels();\r\n } else {\r\n" +
|
||||
" var existing = models[id];\r\n models[id] =" +
|
||||
" model;\r\n\r\n // Add\r\n model.htmlContent = $" +
|
||||
"(\'<div>\').text(model.UserId + \' - \' + model.UserDisplayName);\r\n " +
|
||||
" if (!model.ReadyForReturn && model.EstimatedReturnTime) {\r\n " +
|
||||
" model.htmlContent.append($(\'<span class=\"small\">\').text(\' (Expected: \' + m" +
|
||||
"odel.EstimatedReturnTime + \')\'));\r\n }\r\n if" +
|
||||
" (model.WaitingForUserAction) {\r\n model.htmlContent.appen" +
|
||||
"d($(\'<span class=\"small\">\').text(\' (Since \' + model.WaitingForUserActionSince + " +
|
||||
"\')\'));\r\n } else {\r\n if (model.ReadyFor" +
|
||||
"Return && model.ReadyForReturnSince) {\r\n model.htmlCo" +
|
||||
"ntent.append($(\'<span class=\"small\">\').text(\' (Ready \' + model.ReadyForReturnSin" +
|
||||
"ce + \')\'));\r\n }\r\n }\r\n\r\n " +
|
||||
" if (existing) {\r\n if (existing.ReadyForReturn != mod" +
|
||||
"el.ReadyForReturn || existing.WaitingForUserAction != model.WaitingForUserAction" +
|
||||
") {\r\n removeModel(existing);\r\n " +
|
||||
" model.htmlLi = $(\'<li>\').html(model.htmlContent).data(\'ModelId\', id).hide()" +
|
||||
";\r\n modelInsert(model);\r\n " +
|
||||
"if (init) {\r\n model.htmlLi.fadeIn();\r\n " +
|
||||
" } else {\r\n model.htmlLi.slideDow" +
|
||||
"n();\r\n }\r\n } else {\r\n " +
|
||||
" model.htmlLi = existing.htmlLi;\r\n " +
|
||||
"model.htmlLi.slideUp(\'fast\', function () {\r\n mode" +
|
||||
"l.htmlLi.html(model.htmlContent).slideDown();\r\n });\r\n" +
|
||||
" }\r\n } else {\r\n " +
|
||||
" model.htmlLi = $(\'<li>\').html(model.htmlContent).data(\'ModelId\', id).hide();\r\n " +
|
||||
" modelInsert(model);\r\n if (init) {\r" +
|
||||
"\n model.htmlLi.fadeIn();\r\n } e" +
|
||||
"lse {\r\n model.htmlLi.slideDown(\'slow\');\r\n " +
|
||||
" }\r\n }\r\n if (model.htmlLi && mo" +
|
||||
"del.IsAlert) {\r\n model.htmlLi.addClass(\'alert\');\r\n " +
|
||||
" }\r\n }\r\n };\r\n\r\n var updatedMode" +
|
||||
"l = function (id) {\r\n var userId = id.toString();\r\n\r\n " +
|
||||
" $.ajax({\r\n dataType: \'json\',\r\n url: \'");
|
||||
|
||||
|
||||
#line 460 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
#line 435 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
Write(Url.Action(MVC.Public.UserHeldDevices.UserHeldDevice()));
|
||||
|
||||
|
||||
@@ -442,7 +426,7 @@ WriteLiteral(@"',
|
||||
window.location.href = '");
|
||||
|
||||
|
||||
#line 477 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
#line 452 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
Write(Url.Action(MVC.Public.UserHeldDevices.Noticeboard()));
|
||||
|
||||
|
||||
@@ -453,7 +437,7 @@ WriteLiteral("\';\r\n }, 10000);\r\n }\r\n
|
||||
"rsistantConnection = $.connection(\'");
|
||||
|
||||
|
||||
#line 484 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
#line 459 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
Write(Url.Content("~/Public/UserHeldDevices/Notifications"));
|
||||
|
||||
|
||||
@@ -464,7 +448,7 @@ WriteLiteral("\');\r\n persistantConnection.received(updatedModel
|
||||
"tion.start(function () {\r\n $.getJSON(\'");
|
||||
|
||||
|
||||
#line 488 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
#line 463 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
Write(Url.Action(MVC.Public.UserHeldDevices.UserHeldDevices()));
|
||||
|
||||
|
||||
@@ -489,14 +473,14 @@ WriteLiteral(">\r\n <img");
|
||||
|
||||
WriteLiteral(" style=\"width: 32px; height: 32px; margin-top: -5px; margin-bottom: -15px;\"");
|
||||
|
||||
WriteAttribute("src", Tuple.Create(" src=\"", 23895), Tuple.Create("\"", 23944)
|
||||
WriteAttribute("src", Tuple.Create(" src=\"", 22642), Tuple.Create("\"", 22691)
|
||||
|
||||
#line 500 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 23901), Tuple.Create<System.Object, System.Int32>(Links.ClientSource.Style.Images.Icon32_png
|
||||
#line 475 "..\..\Areas\Public\Views\UserHeldDevices\Noticeboard.cshtml"
|
||||
, Tuple.Create(Tuple.Create("", 22648), Tuple.Create<System.Object, System.Int32>(Links.ClientSource.Style.Images.Icon32_png
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
, 23901), false)
|
||||
, 22648), false)
|
||||
);
|
||||
|
||||
WriteLiteral(" alt=\"Disco Logo\"");
|
||||
|
||||
Reference in New Issue
Block a user