Merge branch 'ManuallyCreateLogs-#29' into Release0.2

This commit is contained in:
Watts 2017-04-13 21:08:54 +01:00
commit a3c4212f0b
17 changed files with 848 additions and 204 deletions

View File

@ -102,5 +102,9 @@ namespace Interfaces
Group GetGroup(int groupId);
OperationResponse UpdateGroup(Group group);
OperationResponse DeleteGroup(int groupId);
OperationResponse DeleteLog(TimeLog log);
OperationResponse CreateLog(TimeLog log);
OperationResponse UpdateLog(TimeLog log);
}
}

View File

@ -11,6 +11,7 @@ namespace Interfaces
public DateTimeOffset EventTime { get; set; }
public int CalendarWeek { get; set; }
public int Year { get; set; }
public LogSource Source { get; set; }
}
public enum LogDirection
@ -19,4 +20,12 @@ namespace Interfaces
IN = 1,
OUT = 2
}
public enum LogSource
{
UNKNOWN=0,
IDENTIFIER=1,
UI=2,
TRAYAPP=3
}
}

View File

@ -0,0 +1,10 @@
namespace SQLiteRepository
{
public enum LogSourceDb
{
UNKNOWN=0,
IDENTIFIER = 1,
UI = 2,
TRAYAPP = 3
}
}

View File

@ -18,6 +18,14 @@ namespace SQLiteRepository
"select * from " + nameof(UserIdentity) + " order by " + nameof(UserIdentity.LastName) + " collate nocase, " +
nameof(UserIdentity.FirstName) + " collate nocase limit ? offset ?";
public const string GET_ALL_USERS_BY_GROUP =
"select u." + nameof(UserIdentity.Id) + ", u." + nameof(UserIdentity.FirstName) + ", u." +
nameof(UserIdentity.LastName) + ", u." + nameof(UserIdentity.HoursPerWeek) + ", u." +
nameof(UserIdentity.IsContractor) + " from " + nameof(UserIdentity) + " u left join " +
nameof(UserGroupJoinDb) + " ugj on ugj." + nameof(UserGroupJoinDb.UserId_FK) + " = u." +
nameof(UserIdentity.Id) + " where ugj." + nameof(UserGroupJoinDb.GroupId_FK) + "=? order by u." +
nameof(UserIdentity.LastName) + " collate nocase, u." + nameof(UserIdentity.LastName) + " collate nocase";
public const string GET_USER_BY_ID =
"select * from " + nameof(UserIdentity) + " where " + nameof(UserIdentity.Id) + "=?";
@ -38,6 +46,11 @@ namespace SQLiteRepository
"update " + nameof(CardUniqueId) + " set " + nameof(CardUniqueId.LastUsed) + " = ? where " +
nameof(CardUniqueId.Id) + " = ?";
public const string UPDATE_USER_DETAILS =
"update " + nameof(UserIdentity) + " set " + nameof(UserIdentity.FirstName) + "=?, " +
nameof(UserIdentity.LastName) + "=?, " + nameof(UserIdentity.HoursPerWeek) + "=?," +
nameof(UserIdentity.IsContractor) + "=? where " + nameof(UserIdentity.Id) + "=?";
public const string SEARCH_USER_LIST =
"SELECT * FROM " + nameof(UserIdentity) + " where(" + nameof(UserIdentity.FirstName) + " Like ? OR " +
nameof(UserIdentity.LastName) + " Like ?)";

View File

@ -50,7 +50,7 @@ namespace SQLiteRepository
{
users =
_connection.Query<UserIdentity>(
"select u.Id, u.FirstName, u.LastName, u.HoursPerWeek, u.IsContractor from UserIdentity u left join UserGroupJoinDb ugj on ugj.UserId_FK = u.Id where ugj.GroupId_FK=?",
SQLiteProcedures.GET_ALL_USERS_BY_GROUP,
groupId);
userCount = users.Count;
}
@ -311,7 +311,7 @@ namespace SQLiteRepository
{
//edit..
_connection.Query<UserIdentity>(
"update UserIdentity set FirstName=?, LastName=?, HoursPerWeek=?,IsContractor=? where Id=?",
SQLiteProcedures.UPDATE_USER_DETAILS,
user.FirstName,
user.LastName,
user.HoursPerWeek,
@ -353,7 +353,7 @@ namespace SQLiteRepository
#region Update Group Associations
SetUserGroups(userId, user.Groups.Where(x=>x.IsAssociatedToUser).ToList());
SetUserGroups(userId, user.Groups.Where(x => x.IsAssociatedToUser).ToList());
#endregion
@ -392,7 +392,7 @@ namespace SQLiteRepository
#region Check the user hasnt registered an event in the last few minutes..
if (ident.UserId_FK!=-1)
if (ident.UserId_FK != -1)
{ //only check log gap if the card is associated to a user
var hysteresisThresholdMinutes = Convert.ToInt32(ConfigurationHandler.ConfigurationHandler.GetConfiguration("SwipeTimeGap") ?? "3");
var threshold = DateTime.UtcNow.AddMinutes(0 - hysteresisThresholdMinutes);
@ -423,7 +423,8 @@ namespace SQLiteRepository
IdentifierId = ident.Id,
Direction = logDirection,
Year = year,
CalendarWeek = calendarWeek
CalendarWeek = calendarWeek,
Source = LogSourceDb.IDENTIFIER
};
_connection.Insert(timeLog);
@ -438,13 +439,13 @@ namespace SQLiteRepository
//TODO: check group name can only be entered once.
public OperationResponse CreateGroup(Group group, out int groupId)
{
var groupDb = new GroupDb { GroupName=group.Name};
var groupDb = new GroupDb { GroupName = group.Name };
var resp = _connection.Insert(groupDb);
groupId = groupDb.GroupId;
return OperationResponse.CREATED;
}
public List<Group> GetGroups(int userId=-1)
public List<Group> GetGroups(int userId = -1)
{
var ret = new List<Group>();
List<GroupDb> query;
@ -459,13 +460,12 @@ namespace SQLiteRepository
}
else
{
var t = new UserGroupJoinDb();
query =
_connection.Query<GroupDb>(
"select gdb.GroupId, gdb.GroupName, gdb.AssignedUserCount"+
" from GroupDb gdb"+
" left join UserGroupJoinDb ujdb"+
" on gdb.GroupId = ujdb.GroupId_FK"+
"select gdb.GroupId, gdb.GroupName, gdb.AssignedUserCount" +
" from GroupDb gdb" +
" left join UserGroupJoinDb ujdb" +
" on gdb.GroupId = ujdb.GroupId_FK" +
" where ujdb.UserId_FK = ?",
userId);
}
@ -473,9 +473,9 @@ namespace SQLiteRepository
{
ret.Add(new Group
{
Id=group.GroupId,
Id = group.GroupId,
Name = group.GroupName,
UserCount = int.Parse(group.AssignedUserCount??"0")
UserCount = int.Parse(group.AssignedUserCount ?? "0")
});
}
return ret;
@ -500,11 +500,6 @@ namespace SQLiteRepository
public OperationResponse UpdateGroup(Group group)
{
//TODO: I would probably prefer to do this manually....
var groupDb = new GroupDb
{
GroupId = group.Id,
GroupName = group.Name
};
var resp = _connection.Query<GroupDb>("update GroupDb set GroupName=? where GroupId=?", group.Name, group.Id);
//var resp = _connection.Update(groupDb);
return OperationResponse.UPDATED;
@ -516,9 +511,76 @@ namespace SQLiteRepository
return OperationResponse.DELETED;
}
public OperationResponse DeleteLog(TimeLog log)
{
var query = _connection.Query<TimeLogDb>(
"select * from TimeLogDb where Id=?", log.Id);
if (!query.Any())
return OperationResponse.FAILED;
_connection.ExecuteScalar<TimeLogDb>("delete from TimeLogDb where Id=?", log.Id);
return OperationResponse.DELETED;
}
public OperationResponse CreateLog(TimeLog log)
{
var calendarWeek = GetIso8601CalendarWeek(log.EventTime.UtcDateTime);
var year = log.EventTime.Year;
var dbLog = new TimeLogDb
{
SwipeEventDateTime = log.EventTime,
Direction = (LogDirectionDb)(int)log.Direction,
Year = year,
CalendarWeek = calendarWeek,
Source = (LogSourceDb)(int)log.Source,
UserId_FK = log.UserId,
IdentifierId = ConvertSourceToIdentifierId(log.Source),
};
_connection.Insert(dbLog);
return OperationResponse.CREATED;
}
public OperationResponse UpdateLog(TimeLog log)
{
var query = _connection.Query<TimeLogDb>(
"select * from TimeLogDb where Id=?", log.Id);
if(!query.Any())
return OperationResponse.FAILED;
if (log.CalendarWeek > 52 || log.CalendarWeek < 1)
{
log.CalendarWeek = GetIso8601CalendarWeek(log.EventTime.UtcDateTime);
}
if (log.Year < 2017)
{
log.Year = log.EventTime.Year;
}
_connection.ExecuteScalar<TimeLogDb>(
"update TimeLogDb set UserId_FK=?, Direction=?,SwipeEventDateTime=?,CalendarWeek=?,Year=?,Source=? where Id=?",
log.UserId, (LogDirectionDb) (int) log.Direction, log.EventTime, log.CalendarWeek, log.Year,
(LogSourceDb) (int) log.Source, log.Id);
return OperationResponse.UPDATED;
}
private int ConvertSourceToIdentifierId(LogSource logSource)
{
switch (logSource)
{
case LogSource.UI:
return -100;
case LogSource.TRAYAPP:
return -200;
default:
return -10;
}
}
private bool SetUserGroups(int userId, List<Group> groups)
{
var groupIds = GetGroupIds(groups.Select(x=>x.Name).ToList());
var groupIds = GetGroupIds(groups.Select(x => x.Name).ToList());
return SetUserGroups(userId, groupIds);
}
@ -527,7 +589,7 @@ namespace SQLiteRepository
//remove the existing user>group associations
_connection.Query<UserGroupJoinDb>("delete from UserGroupJoinDb where UserId_FK = ?", userId);
//add the new group associations.
_connection.InsertAll(groupIds.Select(x => new UserGroupJoinDb {GroupId_FK = x, UserId_FK = userId}));
_connection.InsertAll(groupIds.Select(x => new UserGroupJoinDb { GroupId_FK = x, UserId_FK = userId }));
return true;
}
@ -560,7 +622,7 @@ namespace SQLiteRepository
EventTime = x.SwipeEventDateTime,
UserId = x.UserId_FK,
Year = x.Year
}).ToList();
}).OrderBy(x=>x.EventTime.UtcDateTime).ToList();
var dict = new Dictionary<DayOfWeek, DailyLogs>();
var logList = new List<DailyLogs>();
@ -572,7 +634,7 @@ namespace SQLiteRepository
}
//add the logs to the respective day of the week.
foreach (var log in timeLogs)
foreach (var log in timeLogs.OrderBy(x=>x.EventTime))
{
dict[log.EventTime.DayOfWeek].Logs.Add(log);
}
@ -592,7 +654,7 @@ namespace SQLiteRepository
foreach (DayOfWeek day in Enum.GetValues(typeof(DayOfWeek)))
{
if (logList.Any(x => x.Day == day)) continue;
var dailyLog = new DailyLogs {Day = day, DayOfWeek = day.ToString()};
var dailyLog = new DailyLogs { Day = day, DayOfWeek = day.ToString() };
logList.Add(dailyLog);
}
@ -601,13 +663,13 @@ namespace SQLiteRepository
dailyCollection.Value.DailyTotal = CalculateDailyTotal(dailyCollection.Value);
}
return logList.OrderBy(x => ((int) x.Day + 6)%7).ToList();
return logList.OrderBy(x => ((int)x.Day + 6) % 7).ToList();
}
private double CalculateDailyTotal(DailyLogs dailyLogs)
{
var totalInTime = TimeSpan.FromSeconds(0);
var logs = dailyLogs.Logs.OrderBy(x => x.Id).ToArray();
var logs = dailyLogs.Logs.OrderBy(x => x.EventTime.UtcDateTime).ToArray();
var totalCalcMax = IsOdd(logs.Length) ? logs.Length - 1 : logs.Length;
for (int i = 0; i < totalCalcMax; i += 2)
{

View File

@ -61,6 +61,7 @@
<ItemGroup>
<Compile Include="CardUniqueId.cs" />
<Compile Include="GroupDb.cs" />
<Compile Include="LogSourceDb.cs" />
<Compile Include="SQLiteRepository.cs" />
<Compile Include="Constants.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

View File

@ -13,5 +13,6 @@ namespace SQLiteRepository
public DateTimeOffset SwipeEventDateTime { get; set; }
public int CalendarWeek { get; set; }
public int Year { get; set; }
public LogSourceDb Source { get; set; }
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Web.Http;
using WindowsDataCenter.Helpers;
using Interfaces;
namespace WindowsDataCenter
{
[RoutePrefix("api/logs")]
public class LogsController:ApiController
{
private IRepository _repo;
private ILogger _logger;
public LogsController(IRepository repo, ILogger logger)
{
if (repo == null)
{
throw new ArgumentNullException(nameof(repo));
}
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_repo = repo;
_logger = logger;
}
[HttpPost]
[Route("create")]
[CacheControl(MaxAge = 0)]
public IHttpActionResult CreateAndEditResultLog([FromBody] TimeLog log)
{
log.Source = LogSource.UI;
var resp = log.Id > 0 ? _repo.UpdateLog(log) : _repo.CreateLog(log);
return Ok(new {Id=log.Id, OperationResponse=resp});
}
[HttpDelete]
[Route("delete")]
[CacheControl(MaxAge = 0)]
public IHttpActionResult DeleteLog([FromBody] TimeLog log)
{
_logger.Info("Removing Log {0} for user id {1}", log.Id, log.UserId);
_repo.DeleteLog(log);
return Ok();
}
}
}

View File

@ -44,16 +44,5 @@ namespace WindowsDataCenter
Content = new StringContent(logId.ToString())
});
}
/// <summary>
///
/// </summary>
/// <param name="log"></param>
/// <returns></returns>
[HttpPost]
[Route("manual")]
public IHttpActionResult ManuallyPostData([FromBody] ManualLog log)
{
throw new NotImplementedException();
}
}
}

View File

@ -151,6 +151,7 @@
<Compile Include="Controllers\CardsController.cs" />
<Compile Include="Configuration.cs" />
<Compile Include="Controllers\GroupController.cs" />
<Compile Include="Controllers\LogsController.cs" />
<Compile Include="Controllers\TimelogController.cs" />
<Compile Include="DefaultComponents\DefaultLogger.cs" />
<Compile Include="Helpers\CacheControlAttribute.cs" />
@ -199,6 +200,9 @@
<Content Include="www\css\bootstrap.min.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="www\css\knockout.contextmenu.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="www\favicon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
@ -220,6 +224,9 @@
<Content Include="www\js\bootstrap.min.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="www\js\knockout.contextmenu.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="www\spa.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>

View File

@ -3,7 +3,7 @@
/**
* Create a request URL - references apiEndpoints object to construct url with args, and optional callback url.
* @param {string} routePath
* @param {Array<Object<string>} params - Key, Value object detailing the param name (key) and value (value).
* @param {Array<Object<string>>} params - Key, Value object detailing the param name (key) and value (value).
* @param {boolean} requiresCallback - True - add callback function for JSONP/CORS.
* @param {boolean} isAbsolutePath - True, create a relative URL (without root).
* @returns {string} the url generated

View File

@ -0,0 +1,59 @@
/* knockout.contextmenu v1.0.0
Nicolás Escalante - nlante@gmail.com
Issues: https://github.com/nescalante/knockout.contextmenu/issues
License: MIT */
.context-menu {
position: absolute;
padding: 0;
margin: 0;
z-index: 1030;
background-color: #ffffff;
}
.context-menu ul {
line-height: 1.6;
padding: 0;
margin: 0;
border: 1px solid #dddddd;
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.3);
}
.context-menu ul > li {
padding: 4px 20px;
margin: 0;
z-index: 1031;
list-style-type: none;
cursor: pointer;
white-space: nowrap;
color: #333333;
}
.context-menu ul > li:hover {
background-color: #eeeeee;
}
.context-menu ul > li.disabled,
.context-menu ul > li.disabled a {
color: #666666;
cursor: default;
}
.context-menu ul > li.checked:before {
position: absolute;
content: "\2713";
left: 7px;
}
.context-menu ul > li.with-url {
padding: 0;
}
.context-menu ul > li.with-url a {
display: block;
padding: 4px 20px;
text-decoration: none;
color: #333333;
}
.context-menu ul > li.separator {
margin: 4px 0;
padding: 0;
border-bottom: 1px solid #cccccc;
cursor: default;
}
.context-menu ul > li.separator:hover {
background-color: #ffffff;
}

View File

@ -4,16 +4,20 @@
<title>Flexi Time Data Viewer</title>
<link rel="shortcut icon" href="favicon.ico" />
<link rel="stylesheet preload" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet preload" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.1/css/bootstrap-datepicker3.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
<!--<link rel="stylesheet preload" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.1/css/bootstrap-datepicker3.min.css"/>-->
<link href="spa.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<link href="css/knockout.contextmenu.css" rel="stylesheet" type="text/css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js" type="text/javascript"></script>
<script src="js/knockout.contextmenu.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sammy.js/0.7.6/sammy.js" type="text/javascript"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/momentjs/2.10.6/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.1/js/bootstrap-datepicker.js"></script>
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.6.1/js/bootstrap-datepicker.js"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/js/bootstrap-datetimepicker.min.js"></script>
</head>
<body data-bind="css: {footerBody: errorData() !== null}">
<nav class="navbar navbar-default">
@ -246,9 +250,6 @@
</div>
<br/>
<br/>
<!--<div class="row">
</div>-->
<div class="row">
<div class="col-md-3 col-xs-12">
<div id="datePickerContainer">
@ -259,7 +260,7 @@
<!-- main content panel. -->
<table class="table table-striped">
<thead>
<tr>
<tr data-bind="contextMenu: $root.createContextMenu">
<th>Day Of Week</th>
<!-- ko foreach: new Array($root.getTimeLogEntryArrayLength(MaxDailyLogCount)) -->
<th>In</th>
@ -271,24 +272,30 @@
<tbody>
<!-- ko foreach: TimeLogs-->
<tr>
<td class="valign" data-bind="text: DayOfWeek"></td>
<td class="valign" data-bind="text: DayOfWeek, contextMenu: $root.createContextMenu"></td>
<!-- ko foreach: Logs -->
<td class="valign" data-bind="text: $root.convertToDisplayTime(EventTime)"></td>
<td class="valign" data-bind="text: $root.convertToDisplayTime(EventTime), contextMenu: $root.editContextMenu"></td>
<!-- /ko -->
<!-- ko foreach: new Array($root.correctLogOffset($parent.MaxDailyLogCount)-LogCount)-->
<td class="valign"></td>
<td class="valign" data-bind="contextMenu: $root.createContextMenu"></td>
<!-- /ko -->
<td class="valign"data-bind="text: $root.convertToHours(DailyTotal)"></td>
<td class="valign" data-bind="text: $root.convertToHours(DailyTotal), contextMenu: $root.createContextMenu"></td>
</tr>
<!-- /ko -->
<tr>
<td class="valign" data-bind="attr:{colspan: $root.correctLogOffset(MaxDailyLogCount)+1}">Weekly Total</td>
<td class="valign" for="dailyHrsTotal" data-bind="text: $root.convertToHours(WeeklyTotal)"></td>
<td class="valign" data-bind="attr:{colspan: $root.correctLogOffset(MaxDailyLogCount)+1}, contextMenu: $root.createContextMenu">Weekly Total</td>
<td class="valign" for="dailyHrsTotal" data-bind="text: $root.convertToHours(WeeklyTotal), contextMenu: $root.createContextMenu"></td>
</tr>
</tbody>
</table>
</div>
</div>
<!--<menu type="context" id="manualLogsMenu">
<menu label="ManuallyCreateLogs">
<menuitem label="Edit" onclick="$root.editLogClick()"/>
<menuitem label="Create" onlcick="$root.createLogClick()"/>
</menu>
</menu>-->
</div>
<div id="aboutDialog" class="modal fade" role="dialog" data-bind="with: appDetails">
@ -318,8 +325,42 @@
</div>
</div>
</div>
<footer data-bind="with: errorData, css: {footer: $root.errorData()!==null}">
<div id="manualLogDialog" class="modal fade" role="dialog" data-bind="with: manualLog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4>MANUAL EDIT</h4>
</div>
<div class="modal-body" style="height: 300px;">
<form action="#manualLog" method="post" class="form-group">
<input type="hidden" name="Id" data-bind="value: Id"/>
<div class="form-group">
<div class="input-group date" id="datetimepicker1">
<input type="text" class="form-control" />
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
</div>
<div>
<div class="input-group">
<select data-bind="options: $root.possibleLogDirections,
optionsText: function(item) { return item.Text },
optionsValue: function(item){ return item.value },
value: Direction,
optionsCaption: 'Choose...'"></select>
</div>
</div>
<br/>
<button type="submit" class="btn btn-primary">Submit</button>
<button type="button" class="btn btn-secondary close" data-dismiss="modal">Cancel</button>
</form>
</div>
</div>
</div>
</div>
<footer data-bind="with: errorData, css: {footer: $root.errorData()!==null}">
<div class="container">
<!-- Alert/Error banner-->
<div class="alert alert-danger alert-dismissible" role="alert">
@ -331,8 +372,8 @@
<div data-bind="text: errorDate"></div>
</div>
</div>
</footer>
<script src="spa.min.js" type="text/javascript"></script>
</footer>
<script src="Helpers.js" type="text/javascript"></script>
<script src="spa.js" type="text/javascript"></script>
</body>
</html>

View File

@ -0,0 +1,362 @@
/* knockout.contextmenu v1.0.0
Nicolás Escalante - nlante@gmail.com
Issues: https://github.com/nescalante/knockout.contextmenu/issues
License: MIT */
(function (undefined) {
"use strict";
// client
if (typeof ko !== undefined + "") {
bindContextMenu(ko);
}
// node
if (typeof module !== undefined + "" && module.exports && typeof require !== undefined + "") {
bindContextMenu(require("knockout"));
}
function bindContextMenu(ko) {
var currentMenu;
var elementMapping = [];
var utils = ko.utils;
var registerEvent = utils.registerEventHandler;
var isObservable = ko.isObservable;
registerEvent(document, "click", function (event) {
var button = event.which || event.button;
if (!event.defaultPrevented && button < 2) {
hideCurrentMenu();
}
});
utils.contextMenu = {
getMenuFor: function (element, event) {
var result = getMapping(element);
if (result) {
return result.get(event);
}
},
openMenuFor: function (element, event) {
var result = getMapping(element);
if (result) {
return result.open(event);
}
},
};
ko.bindingHandlers.contextMenu = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var eventsToHandle = valueAccessor() || {};
var allBindings = allBindingsAccessor();
var defaultClass = allBindings.contextMenuClass || "context-menu";
var activeElement;
// bind on click? bind on context click?
if (allBindings.bindMenuOnClick) {
registerEvent(element, "click", openMenu);
}
if (allBindings.bindMenuOnContextMenu === undefined || allBindings.bindMenuOnContextMenu) {
registerEvent(element, "contextmenu", openMenu);
}
elementMapping.push({
element: element,
get: function () {
return activeElement;
},
open: openMenu,
hide: function () {
if (activeElement) {
activeElement.hide();
}
}
});
function mouseX(evt) {
if (evt.pageX) {
return evt.pageX;
} else if (evt.clientX) {
return evt.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft);
} else {
return null;
}
}
function mouseY(evt) {
if (evt.pageY) {
return evt.pageY;
} else if (evt.clientY) {
return evt.clientY + (document.documentElement.scrollTop || document.body.scrollTop);
} else {
return null;
}
}
function openMenu(event) {
activeElement = getMenu(event);
var menuElement = activeElement.element;
hideCurrentMenu();
if (menuElement) {
// make visibility hidden, then add to DOM so that we can get the height/width of the menu
menuElement.style.visibility = "hidden";
(document.body || document).appendChild(menuElement);
// set location
if (event) {
var bottomOfViewport = window.innerHeight + window.pageYOffset;
var rightOfViewport = window.innerWidth + window.pageXOffset;
if (mouseY(event) + menuElement.offsetHeight > bottomOfViewport) {
menuElement.style.top = 1 * (bottomOfViewport - menuElement.offsetHeight - 10) + "px";
} else {
menuElement.style.top = mouseY(event) + "px";
}
if (mouseX(event) + menuElement.offsetWidth > rightOfViewport) {
menuElement.style.left = 1 * (rightOfViewport - menuElement.offsetWidth - 10) + "px";
} else {
menuElement.style.left = mouseX(event) + "px";
}
event.preventDefault();
event.stopPropagation();
} else {
menuElement.style.top = (element.offsetTop + element.offsetHeight) + "px";
menuElement.style.left = (element.offsetLeft + element.offsetWidth) + "px";
}
// now set to visible
menuElement.style.visibility = "";
}
// replace current menu with the recently created
currentMenu = menuElement;
return activeElement;
}
function getMenu(event) {
var menu;
var hasChecks = false;
var elements = [];
var actions = [];
var items = [];
var props = Object.keys(
ko.isObservable(eventsToHandle) ?
eventsToHandle() :
eventsToHandle
);
props.forEach(function (eventNameOutsideClosure) {
pushItem(eventNameOutsideClosure);
});
if (elements.length) {
menu = document.createElement("div");
menu.className = defaultClass;
// you may need padding to menus that has checks
menu.innerHTML = "<ul class=\"" + (hasChecks ? "has-checks" : "") + '">' +
elements.join("") +
"</ul>";
// map items to actions
elements.forEach(function (item, index) {
registerEvent(menu.children[0].children[index], "click", function (event) {
var result = actions[index](viewModel, event);
if (!result && event) {
event.preventDefault();
}
});
});
}
return {
element: menu,
items: items,
open: openMenu,
hide: function () {
if (menu && menu.parentNode) {
menu.parentNode.removeChild(menu);
}
currentMenu = null;
},
};
function pushItem(eventName) {
var item = getMenuProperties(eventName);
var classes = [];
var id = "";
var liHtml;
if (item.isVisible) {
hasChecks = hasChecks || item.isBoolean;
if (item.id) {
id = item.id;
}
// set css classes
if (item.isChecked) {
classes.push("checked");
}
if (item.isDisabled) {
classes.push("disabled");
}
if (item.isSeparator) {
classes.push("separator");
}
if (item.url) {
classes.push("with-url");
}
liHtml = "<li " + (id ? ('id="' + id + '" ') : "") +
' class="' + classes.join(" ") + '">' +
item.html +
"</li>";
elements.push(liHtml);
actions.push(item.action);
}
items.push(item);
}
}
function getMenuProperties(eventName) {
var text = "";
var html = "";
var currentEvent = ko.isObservable(eventsToHandle) ?
eventsToHandle()[eventName] :
eventsToHandle[eventName];
var item = currentEvent || {};
var id = item.id;
var url = (isObservable(item.url) ? item.url() : item.url);
var isVisible = item.visible === undefined || item.visible === null ||
(isObservable(item.visible) && item.visible()) ||
(!isObservable(item.visible) && !!item.visible);
var isChecked = false;
var isEnabled = !item.disabled ||
(isObservable(item.disabled) && !item.disabled()) ||
(isObservable(item.enabled) && item.enabled()) ||
(!isObservable(item.enabled) && !!item.enabled);
var isBoolean = false;
var isDisabled = !isEnabled;
var isSeparator = !!currentEvent.separator;
if (!isSeparator) {
text = isObservable(item.text) ? item.text() : item.text;
if (!text) {
text = eventName;
}
if (url) {
html = '<a href="' + url + '">' + text + "</a>";
} else {
html = text;
}
}
if ((isObservable(item) && typeof item() === "boolean") ||
(isObservable(item.action) && typeof item.action() === "boolean")) {
isBoolean = true;
if ((item.action && item.action()) ||
(typeof item === "function" && item())) {
isChecked = true;
}
}
return {
html: html,
text: text,
url: url,
id: id,
isVisible: isVisible,
isChecked: isChecked,
isEnabled: isEnabled,
isDisabled: isDisabled,
isBoolean: isBoolean,
isSeparator: isSeparator,
action: action
};
function action(viewModel, event) {
var error = eventName + " option must have an action or an url.";
if (isDisabled) {
return false;
}
// check if option is a boolean
if (isObservable(item) && typeof item() === "boolean") {
item(!item());
}
// is an object? well, lets check it properties
else if (typeof item === "object") {
// check if has an action or if its a separator
if (!item.action && !url && !isSeparator) {
throw error;
}
// evaluate action
else if (item.action) {
if (isObservable(item.action) && typeof item.action() === "boolean") {
item.action(!item.action());
} else {
item.action(viewModel, event);
}
}
}
// its not an observable, should be a function
else if (typeof item === "function") {
item(viewModel, event);
}
// nothing to do with this
else {
throw error;
}
return true;
}
}
}
};
function hideCurrentMenu() {
if (currentMenu && currentMenu.parentNode) {
currentMenu.parentNode.removeChild(currentMenu);
}
currentMenu = null;
}
function getMapping(element) {
var i = 0;
for (; i < elementMapping.length; i++) {
if (elementMapping[i].element === element) {
return elementMapping[i];
}
}
}
}
})();

View File

@ -12,7 +12,7 @@
}
}
.bootstrap-datetimepicker-widget tr:hover {
background-color: #808080;
background-color: #a9a9a9;
}
.datepicker tr.highlight {
background: #eeeeee;

View File

@ -1,7 +1,12 @@
function DataVM() {
"use strict";
var self = this;
self.helpers = new Helpers();
self.menuOptions = ["Home"];
self.possibleLogDirections = ko.observableArray([
{ Text: "In", value: 1 },
{ Text: "Out", value: 2 }
]),
self.chosenMenuItemId = ko.observable();
self.appDetails = ko.observable(null);
self.userList = ko.observable(null);
@ -10,8 +15,10 @@
self.userTimeLogData = ko.observable(null);
self.unassignedCardData = ko.observable(null);
self.chosenTimeLogUserId = -1;
self.selectedTimeLogDate = ko.observable(null);
self.selectedCalendarWeek = ko.observable(0);
self.errorData = ko.observable(null);
self.manualLog = ko.observable(null);
self.apiEndpoints = {
root: "http://localhost:8800",
getUserList: "/api/users",
@ -20,7 +27,9 @@
getTimeLogs: "/api/timelogs",
getUnassignedCards: "/api/cards/unassigned",
getGroups: "/api/groups",
getAppDetails: "/api/app"
getAppDetails: "/api/app",
manualLogsCreate: "/api/logs/create",
manualLogsDelete: "/api/logs/delete"
};
self.uiPages = {
users: "users",
@ -39,7 +48,7 @@
}
var url = "timelogs" + "/" + userId;
if (args) {
url = self.createRequestUrl(url, args, false, false);
url = self.helpers.createRequestUrl(url, args, false, false);
}
location.hash = url;
};
@ -52,86 +61,11 @@
};
self.errorData(errDat);
}
self.processRequestFailure = function (xmlHttpRequest, textStatus, errorThrown) {
if (xmlHttpRequest.readyState === 4) {
return {
errorCode: xmlHttpRequest.status,
errorMessage: xmlHttpRequest.statusText,
errorSource: ""
};
}
else if (xmlHttpRequest.readyState === 0) {
return {
errorCode: xmlHttpRequest.status,
errorMessage: "Network Error - Is the server available?",
errorSource: ""
};
}
else {
return {
errorCode: xmlHttpRequest.status,
errorMessage: "Unknown Error",
errorSource: ""
};
}
};
/**
* Create a request URL - references apiEndpoints object to construct url with args, and optional callback url.
* @param {string} routePath
* @param {Array<Object<string>} params - Key, Value object detailing the param name (key) and value (value).
* @param {boolean} requiresCallback - True - add callback function for JSONP/CORS.
* @param {boolean} isAbsolutePath - True, create a relative URL (without root).
* @returns {string} the url generated
* @example
* createRequestUrl("/api/endpoint", [{key:"param", value:"value"}], true, false);
* returns: "http://192.168.2.2/api/endpoint?param=value&callback=?"
*/
self.createRequestUrl = function (routePath, params, requiresCallback, isAbsoluteUrl) {
var appender = "?";
var url = "";
if (isAbsoluteUrl) {
url = self.apiEndpoints.root;
}
url = url + routePath;
if (params !== undefined
&& params !== null) {
if (params.length > 0) {
for (var i = 0; i < params.length; i++) {
url += appender + params[i].key + "=" + params[i].value;
appender = "&";
}
}
}
if (requiresCallback) {
url += appender + "callback=?";
}
return url;
};
/**
* Function to redirect to a page in the sammy.js eco system.
* Relies on "pagedestination" tag in the html. This is a button click handler.
* @param {Object<unknown>} data - dunno?
* @param {Object<buttonhandle>} event - handle to the button that was clicked.
* @returns {nothing} - redirects to the url referenced by the pageDestination tag.
*/
self.returnButtonClick = function (data, event) {
var target = null;
if (event.target) target = event.target;
else if (event.srcElement) target = event.srcElement;
var destination = "";
if (target != null) {
for (var i = 0; i < target.attributes.length; i++) {
if (target.attributes[i].nodeName === "pagedestination") {
destination = target.attributes[i].value;
break;
}
}
var destination = self.helpers.getPageDestination(data, event);
if (destination !== "") {
self.goToMenuOption(destination); //redirect to whereever the button is telling us to go..
}
} else {
console.log("target is null, going nowhere");
}
};
self.convertToHours = function (value) {
var hrs = value / 60;
@ -146,21 +80,21 @@
var date = new Date(dateValue);
return date.getHours() + ":" + self.padNumber(date.getMinutes());
};
self.correctLogOffset = function (logCount) {
self.correctLogOffset = function(logCount) {
if (logCount % 2 !== 0) {
logCount += 1;
}
return logCount;
}
self.round = function (value, decimals) {
};
self.round = function(value, decimals) {
return parseFloat(Math.round(value * 100) / 100).toFixed(decimals);
}
};
self.getTimeLogEntryArrayLength = function(maxDailyLogs) {
return Math.round(maxDailyLogs/2);
};
self.padNumber = function(number) {
return (number < 10 ? '0' : '') + number;
}
return (number < 10 ? "0" : "") + number;
};
self.convertToDisplayDateTime = function (dateValue) {
var date = new Date(dateValue); // dd MM YY HH:mm:ss e.g.: 01 Mar 17 17:34:02
return date.getDate() + " "
@ -215,7 +149,7 @@
value: data
}
];
var url = self.createRequestUrl("users", args, false, false);
var url = self.helpers.createRequestUrl("users", args, false, false);
location.hash = url;
console.log(url);
};
@ -230,38 +164,53 @@
value: pageNumber
}
];
var url = self.createRequestUrl("users", args, false, false);
var url = self.helpers.createRequestUrl("users", args, false, false);
location.hash = url;
console.log(url);
};
self.initDatePicker = function (selectedDate) {
$("#weeklyDatePicker").datepicker({
weekStart: 1,
maxViewMode: 2,
endDate: "+0d",
todayBtn: "linked",
format: "yyyy-mm-dd",
todayHighlight: true,
calendarWeeks: true
});
if (!selectedDate) {
selectedDate = new Date();
} else {
selectedDate = new Date(selectedDate);
}
$("#weeklyDatePicker").datepicker("setDate", selectedDate);
moment.locale("en", { week: { dow: 1 } });
$("#weeklyDatePicker").datetimepicker({
format: "DD/MM/YYYY",
inline: true,
showTodayButton: true,
calendarWeeks: true,
maxDate: "now",
date: selectedDate
});
};
self.assignHandler = function () {
self.assignHandler = function() {
var elem = $("#weeklyDatePicker")[0];
var data = jQuery.hasData(elem) && jQuery._data(elem);
if (!data.events.changeDate) {
$("#weeklyDatePicker").on("changeDate", function (e) {
var kk = e.date;
self.selectedCalendarWeek(moment(kk).isoWeek());
self.goToTimeLogs(self.chosenTimeLogUserId, null, [{ key: "selectedDate", value: moment(kk).format("MM-DD-YYYY") }]);
if (!data.events) {
$("#weeklyDatePicker")
.on("dp.change",
function(e) {
var value = e.date;
self.selectedCalendarWeek(moment(value).isoWeek());
self.goToTimeLogs(self.chosenTimeLogUserId,
null,
[{ key: "selectedDate", value: moment(value).format("MM-DD-YYYY") }]);
});
}
};
self.assignUpdateHandler = function() {
var elem = $("#datetimepicker1")[0];
var data = jQuery.hasData(elem) && jQuery._data(elem);
if (!data.events) {
$("#datetimepicker1")
.on("dp.change",
function(e) {
var value = e.date.toISOString();
self.manualLog().EventTime = value;
});
}
};
self.getUserList = function (pageSize, pageNumber, groupId) {
var args = null;
if (pageSize && pageNumber) {
@ -282,52 +231,52 @@
value: groupId
}];
}
var url = self.createRequestUrl(self.apiEndpoints.getUserList, args, false);
var url = self.helpers.createRequestUrl(self.apiEndpoints.getUserList, args, false);
$.getJSON(url, function (res) {
self.userList(res);
$('[data-toggle="tooltip"]').tooltip();
}).fail(function (response, status, error) {
console.log("error - getusers");
var errObj = self.processRequestFailure(response, status, error);
var errObj = self.helpers.processRequestFailure(response, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "getUserList");
});
};
self.getAppDetails = function() {
var url = self.createRequestUrl(self.apiEndpoints.getAppDetails, null, false, false);
var url = self.helpers.createRequestUrl(self.apiEndpoints.getAppDetails, null, false, false);
$.getJSON(url, function (res) {
self.appDetails(res);
}).fail(function (response, status, error) {
console.log("error - getusers");
var errObj = self.processRequestFailure(response, status, error);
var errObj = self.helpers.processRequestFailure(response, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "getUserList");
});
};
self.searchUsers = function(query) {
var url = self.createRequestUrl(self.apiEndpoints.getUserList,
var url = self.helpers.createRequestUrl(self.apiEndpoints.getUserList,
[{ key: "query", value: query }], false, false);
$.getJSON(url,
function(res) {
self.userList(res);
}).fail(function(resp, status, error) {
self.goToMenuOption(self.uiPages.home());
var errObj = self.processRequestFailure(resp, status, error);
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "searchUsers");
}
);
};
self.getUserDetails = function (userId) {
var url = self.createRequestUrl(self.apiEndpoints.getUserDetails + "/" + userId, null, false);
var url = self.helpers.createRequestUrl(self.apiEndpoints.getUserDetails + "/" + userId, null, false);
$.getJSON(url, function (res) {
self.chosenUserDetails(res);
}).fail(function (resp, status, error) {
console.log("error - getuserdetails");
var errObj = self.processRequestFailure(resp, status, error);
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "getUserDetails");
self.goToMenuOption(self.uiPages.home());
});
};
self.submitChangedUser = function (user) {
var url = self.apiEndpoints.editUser;
var url = self.helpers.createRequestUrl(self.apiEndpoints.editUser, null, false, false);
$.post(url, user, function () {
}, "json")
.done(function () {
@ -335,7 +284,7 @@
self.goToMenuOption(self.uiPages.home());
})
.fail(function (resp, status, error) {
var errObj = self.processRequestFailure(resp, status, error);
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "submitChangedUser");
self.chosenUserDetails(null);
self.goToMenuOption(self.uiPages.home());
@ -346,7 +295,7 @@
if (selectedDate) {
urlArgs.push({ key: "selectedDate", value: selectedDate });
}
var url = self.createRequestUrl(self.apiEndpoints.getTimeLogs,
var url = self.helpers.createRequestUrl(self.apiEndpoints.getTimeLogs,
urlArgs,
false);
$.getJSON(url, function (res) {
@ -355,33 +304,103 @@
self.assignHandler();
}).fail(function (resp, status, error) {
console.log("error - getuserdetails");
var errObj = self.processRequestFailure(resp, status, error);
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "getTimeLogData");
self.goToMenuOption(self.uiPages.home()); //go home.
});
};
self.getUnassignedCardData = function () {
var url = self.createRequestUrl(self.apiEndpoints.getUnassignedCards, null, false);
var url = self.helpers.createRequestUrl(self.apiEndpoints.getUnassignedCards, null, false);
$.getJSON(url, function (res) {
self.unassignedCardData(res);
}).fail(function (resp, status, error) {
console.log("error - getuserdetails");
var errObj = self.processRequestFailure(resp, status, error);
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "getUnassignedCardData");
});
};
self.getGroups = function (successFunc) {
var url = self.createRequestUrl(self.apiEndpoints.getGroups, null, false);
var url = self.helpers.createRequestUrl(self.apiEndpoints.getGroups, null, false);
return $.getJSON(url, function (res) {
successFunc(res);
//self.chosenUserDetails().Groups = res;
//self.chosenUserDetails.valueHasMutated();
}).fail(function (resp, status, error) {
console.log("error - getGroups");
var errObj = self.processRequestFailure(resp, status, error);
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "getGroups");
});
};
self.createManualLog = function(newLog) {
var url = self.helpers.createRequestUrl(self.apiEndpoints.manualLogsCreate, null, false, false);
$.post(url, newLog, function () {
}, "json")
.done(function () {
self.manualLog(null);
$('#manualLogDialog').modal("hide");
self.goToMenuOption(self.uiPages.home());
})
.fail(function (resp, status, error) {
var errObj = self.helpers.processRequestFailure(resp, status, error);
self.assignErrorObject(errObj.errorCode, errObj.errorMessage, "createManualLog");
self.chosenUserDetails(null);
self.goToMenuOption(self.uiPages.home());
});
};
self.deleteManualLog = function (logToDelete) {
var url = self.helpers.createRequestUrl(self.apiEndpoints.manualLogsDelete, null, false, false);
$.ajax({
url: url,
type: "DELETE",
data: logToDelete,
success: function (result) {
console.log("successfully deleted .." + result);
self.goToMenuOption(self.uiPages.home());
}
});
};
self.createContextMenu = ko.observableArray([
{ text: "Create", action: createlog }
]);
self.editContextMenu = ko.observableArray([
{ text: "Create", action: createlog },
{ text: "Edit", action: editlog },
{ text: "Delete", action: deleteLog }
]);
function editlog (data) {
self.manualLog(data);
$('#manualLogDialog').modal("show");
$('#datetimepicker1').datetimepicker({
format: "YYYY-DD-MM HH:mm:ss",
date: new Date(data.EventTime),
minDate: moment(new Date(data.EventTime)).startOf('week'),
maxDate: moment(new Date(data.EventTime)).endOf('week')
});
self.assignUpdateHandler();
};
function createlog(data) {
self.manualLog({
CalendarWeek:-1,
Direction:-1,
EventTime: new Date().toISOString(),
Id: -1,
IdentifierId: -1,
UserId: self.chosenTimeLogUserId,
Year: 0
});
$('#manualLogDialog').modal("show");
$('#datetimepicker1').datetimepicker({
format: "YYYY-DD-MM HH:mm:ss",
minDate: moment(self.selectedTimeLogDate()).startOf("week"),
maxDate: moment(self.selectedTimeLogDate()).endOf("week")
});
self.assignUpdateHandler();
};
function deleteLog(data) {
if (confirm("Are you sure you want to delete this log?")) {
self.deleteManualLog(data);
}
};
Sammy(function () {
this.get("#users", function () {
var query = this.params.query;
@ -389,9 +408,11 @@
var pageNumber = this.params.pageNumber;
var groupId = this.params.groupId;
self.chosenMenuItemId("Home");
self.groupsList(null);
self.chosenUserDetails(null);
self.userList(null);
self.userTimeLogData(null);
self.manualLog(null);
if (self.appDetails() === null) {
self.getAppDetails();
}
@ -405,18 +426,28 @@
});
this.get("#userData/:userId", function () {
self.chosenMenuItemId("Data");
self.groupsList(null);
self.chosenUserDetails(null);
self.userList(null);
self.getUserDetails(this.params.userId);
self.userTimeLogData(null);
self.manualLog(null);
self.getUserDetails(this.params.userId);
self.getUnassignedCardData();
});
this.get("#timelogs/:userId", function () {
var selectedDate = this.params.selectedDate;
self.chosenMenuItemId("Other");
self.userList(null);
self.chosenUserDetails(null);
if (this.params.selectedDate) {
self.selectedTimeLogDate(this.params.selectedDate);
} else {
self.selectedTimeLogDate(new Date());
}
self.chosenTimeLogUserId = this.params.userId;
self.getTimeLogData(this.params.userId, selectedDate);
self.chosenMenuItemId("Other");
self.groupsList(null);
self.chosenUserDetails(null);
self.userList(null);
self.userTimeLogData(null);
self.manualLog(null);
self.getTimeLogData(this.params.userId, self.selectedTimeLogDate());
});
this.get("#newUser", function () {
self.chosenMenuItemId("newUser");
@ -455,14 +486,21 @@
self.submitChangedUser(self.chosenUserDetails());
return false;
});
this.post("#manualLog",
function() {
self.createManualLog(self.manualLog());
$('#manualLogDialog').modal("hide");
//self.goToTimeLogs(self.chosenTimeLogUserId, null, [{ key: "selectedDate", value: self.selectedTimeLogDate() }]);
});
//default route (home page)
this.get("", function () { this.app.runRoute("get", "#" + self.uiPages.home()) });
}).run();
};
ko.applyBindings(new DataVM());
$(document).on("mouseenter", ".datepicker-days tbody tr", function () {
$(this).addClass('highlight');
});
$(document).on("mouseleave", ".datepicker-days tbody tr", function () {
$(this).removeClass('highlight');
});
//$(document).on("mouseenter", ".datepicker-days tbody tr", function () {
// $(this).addClass("highlight");
//});
//$(document).on("mouseleave", ".datepicker-days tbody tr", function () {
// $(this).removeClass("highlight");
//});