implement logs in last x minute check. can be configured from app.config file using "SwipeTimeGap" configuration. defaults to 3 minutes if configuration is not there. #36
557 lines
20 KiB
C#
557 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Configuration;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Security.Cryptography.X509Certificates;
|
|
using Interfaces;
|
|
using SQLite.Net;
|
|
using SQLite.Net.Platform.Win32;
|
|
|
|
namespace SQLiteRepository
|
|
{
|
|
public class SQLiteRepository : IRepository
|
|
{
|
|
private readonly SQLiteConnection _connection;
|
|
private readonly ILogger _logger;
|
|
private string _path = "flexitimedb.db";
|
|
|
|
public SQLiteRepository(ILogger logger)
|
|
{
|
|
_path =
|
|
new Uri(Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().CodeBase), "flexitimedb.db"))
|
|
.LocalPath;
|
|
if (logger == null) throw new ArgumentNullException(nameof(logger));
|
|
_logger = logger;
|
|
|
|
_connection = new SQLiteConnection(new SQLitePlatformWin32(), _path);
|
|
|
|
_connection.CreateTable<CardUniqueId>();
|
|
_connection.CreateTable<UserIdentity>();
|
|
_connection.CreateTable<TimeLogDb>();
|
|
_logger.Trace("Initialised SQLite Repository");
|
|
}
|
|
|
|
public UserList GetUsers(int pageNumber = -1, int pageSize = -1)
|
|
{
|
|
var ret = new UserList();
|
|
List<UserIdentity> users;
|
|
int userCount;
|
|
if (pageNumber == -1 && pageSize == -1)
|
|
{
|
|
users = _connection.Query<UserIdentity>(SQLiteProcedures.GET_ALL_USERS);
|
|
userCount = users.Count;
|
|
}
|
|
else
|
|
{
|
|
users = _connection.Query<UserIdentity>(SQLiteProcedures.GET_ALL_USERS_PAGINATE,
|
|
pageSize, (pageNumber - 1) * pageSize);
|
|
userCount = _connection.ExecuteScalar<int>(SQLiteProcedures.GET_TOTAL_USER_COUNT);
|
|
}
|
|
|
|
if (!users.Any())
|
|
{
|
|
if (pageNumber == -1 && pageSize == -1)
|
|
{
|
|
ret.PageNumber = 1;
|
|
ret.PageSize = 20;
|
|
}
|
|
else
|
|
{
|
|
ret.PageNumber = pageNumber;
|
|
ret.PageSize = pageSize;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
foreach (var user in users)
|
|
{
|
|
var userObj = ChangeToUserObject(user);
|
|
var cards = _connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.GET_CARDS_BY_USER_ID,
|
|
user.Id);
|
|
|
|
foreach (var card in cards)
|
|
{
|
|
userObj.AssociatedIdentifiers.Add(new Identifier()
|
|
{
|
|
UniqueId = card.CardUId,
|
|
IsAssociatedToUser = true,
|
|
Id = card.Id
|
|
});
|
|
}
|
|
ret.Users.Add(userObj);
|
|
}
|
|
if (pageNumber == -1 && pageSize == -1)
|
|
{
|
|
ret.PageSize = 1; //TODO: switch to ret.UserCount
|
|
ret.PageNumber = 1;
|
|
}
|
|
else
|
|
{
|
|
ret.PageSize = pageSize;
|
|
ret.PageNumber = pageNumber;
|
|
}
|
|
ret.TotalUserCount = userCount;
|
|
return ret;
|
|
}
|
|
|
|
public UserList Search(string searchParam)
|
|
{
|
|
_logger.Trace("Searching SQLite database for the term: {0}", searchParam);
|
|
var ret = new UserList();
|
|
searchParam = string.Format("%{0}%", searchParam);
|
|
var users = _connection.Query<UserIdentity>(
|
|
SQLiteProcedures.SEARCH_USER_LIST,
|
|
searchParam, searchParam);
|
|
|
|
_logger.Trace("Got {0} results for term: {1}", users.Count, searchParam);
|
|
if (!users.Any())
|
|
{
|
|
ret.PageNumber = 1;
|
|
ret.PageSize = 20;
|
|
return ret;
|
|
}
|
|
|
|
foreach (var user in users)
|
|
{
|
|
var userObj = ChangeToUserObject(user);
|
|
var cards = _connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.GET_CARDS_BY_USER_ID,
|
|
user.Id);
|
|
|
|
foreach (var card in cards)
|
|
{
|
|
userObj.AssociatedIdentifiers.Add(new Identifier()
|
|
{
|
|
UniqueId = card.CardUId,
|
|
IsAssociatedToUser = true,
|
|
Id = card.Id
|
|
});
|
|
}
|
|
ret.Users.Add(userObj);
|
|
}
|
|
//TODO: figure out paging here. - should there be any?
|
|
ret.PageSize = 20;
|
|
ret.PageNumber = 1;
|
|
|
|
return ret;
|
|
}
|
|
|
|
public User GetUser(int id)
|
|
{
|
|
var ret = new User();
|
|
|
|
try
|
|
{
|
|
var users = _connection.Query<UserIdentity>(
|
|
SQLiteProcedures.GET_USER_BY_ID,
|
|
id);
|
|
|
|
if (!users.Any()) return ret;
|
|
|
|
var user = users.First();
|
|
ret = ChangeToUserObject(user);
|
|
var cards = _connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.GET_CARDS_BY_USER_ID,
|
|
user.Id);
|
|
|
|
foreach (var card in cards)
|
|
{
|
|
ret.AssociatedIdentifiers.Add(new Identifier { UniqueId = card.CardUId, IsAssociatedToUser = true, Id = card.Id });
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "Error in GetUser with Id: {0}", id);
|
|
ret.UserId = id;
|
|
ret.FirstName = ret.LastName = string.Empty;
|
|
ret.HoursPerWeek = -1.0f;
|
|
ret.IsContractor = false;
|
|
ret.AssociatedIdentifiers.Clear();
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
//TODO: refac this as it duplicates a lot of code.
|
|
public TimeLogList GetTimeLogs(int userId)
|
|
{
|
|
return GetTimeLogs(userId, DateTime.UtcNow);
|
|
}
|
|
|
|
public TimeLogList GetTimeLogs(int userId, DateTime selectedDate)
|
|
{
|
|
var ret = new TimeLogList { SelectedDate = selectedDate.Date };
|
|
var calendarWeek = GetIso8601CalendarWeek(selectedDate);
|
|
ret.CalendarWeek = calendarWeek;
|
|
|
|
try
|
|
{
|
|
ret.TimeLogs = GetTimeLogList(userId, calendarWeek, selectedDate.Year);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "Error in GetTimeLogs with Id: {0} and selected date: {1}, Exception: {2}", userId, selectedDate, ex);
|
|
}
|
|
|
|
try
|
|
{
|
|
ret.HoursPerWeekMinutes = GetUserContractedHours(userId) * 60.0f;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "Error in GetUserContracterHours with Id: {0} and selected date: {1}, Exception: {2}", userId, selectedDate, ex);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
private float GetUserContractedHours(int userId)
|
|
{
|
|
var hoursQuery = _connection.Query<UserIdentity>(SQLiteProcedures.GET_USER_CONTRACTED_HOURS, userId);
|
|
if (hoursQuery.Any())
|
|
{
|
|
return hoursQuery.First().HoursPerWeek;
|
|
}
|
|
return -1.0f;
|
|
}
|
|
|
|
public IdentifierList GetUnassignedIdentifierList()
|
|
{
|
|
var ret = new IdentifierList();
|
|
var cardQuery = _connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.GET_UNASSIGNED_CARD_LIST,
|
|
Constants.UNASSIGNED_CARD_USER_ID);
|
|
|
|
foreach (var card in cardQuery)
|
|
{
|
|
ret.data.Add(new Identifier
|
|
{
|
|
Id = card.Id,
|
|
IsAssociatedToUser = card.UserId_FK != Constants.UNASSIGNED_CARD_USER_ID,
|
|
UniqueId = card.CardUId
|
|
});
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
//TODO: Check time logs table on update to ensure associated cards/unique identifiers are removed/added as appropriate.
|
|
public OperationResponse UpdateUser(User user, out int userIdResult)
|
|
{
|
|
//if(user.UserId <=0) return OperationResponse.FAILED;
|
|
|
|
var ret = OperationResponse.NONE;
|
|
var cardIds = new List<int>();
|
|
|
|
//Get a list of current associated identifiers, convert into a list of Identifier Objects..
|
|
var currentCards =
|
|
_connection.Query<CardUniqueId>(SQLiteProcedures.GET_CARDS_BY_USER_ID, user.UserId)
|
|
.Select(x => new Identifier { Id = x.Id, IsAssociatedToUser = true, UniqueId = x.CardUId });
|
|
var cardsToRemove = currentCards.Except(user.AssociatedIdentifiers).Select(x => x.Id).ToList();
|
|
|
|
#region GetUnique Identifiers
|
|
foreach (var card in user.AssociatedIdentifiers)
|
|
{
|
|
var existingCard = _connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.GET_CARDS_BY_UNIQUE_ID, card.UniqueId);
|
|
|
|
if (!existingCard.Any())
|
|
{
|
|
var cardInsert = new CardUniqueId { CardUId = card.UniqueId, UserId_FK = -1 };
|
|
_connection.Insert(cardInsert);
|
|
cardIds.Add(cardInsert.Id);
|
|
if (ret < OperationResponse.CREATED)
|
|
ret = OperationResponse.CREATED; //only change it if my status supercedes.
|
|
}
|
|
else
|
|
{
|
|
cardIds.Add(existingCard.First().Id);
|
|
if (ret < OperationResponse.UPDATED)
|
|
ret = OperationResponse.UPDATED;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Update/Create User
|
|
int userId;
|
|
|
|
if (user.UserId != -1)
|
|
{
|
|
//edit..
|
|
_connection.Query<UserIdentity>(
|
|
"update UserIdentity set FirstName=?, LastName=?, HoursPerWeek=?,IsContractor=? where Id=?",
|
|
user.FirstName,
|
|
user.LastName,
|
|
user.HoursPerWeek,
|
|
user.IsContractor,
|
|
user.UserId
|
|
);
|
|
userId = user.UserId;
|
|
}
|
|
else
|
|
{
|
|
var userInsert = new UserIdentity
|
|
{
|
|
FirstName = user.FirstName,
|
|
LastName = user.LastName,
|
|
HoursPerWeek = user.HoursPerWeek,
|
|
IsContractor = user.IsContractor
|
|
};
|
|
_connection.Insert(userInsert);
|
|
userId = userInsert.Id;
|
|
if (ret < OperationResponse.CREATED)
|
|
ret = OperationResponse.CREATED;
|
|
}
|
|
#endregion
|
|
|
|
#region Update Card/Unique Id entries.
|
|
foreach (var cardId in cardIds)
|
|
{
|
|
_connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.UPDATE_CARD_USER_ID,
|
|
userId, cardId);
|
|
}
|
|
foreach (var card in cardsToRemove)
|
|
{
|
|
_connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.UPDATE_CARD_USER_ID,
|
|
-1, card);
|
|
}
|
|
#endregion
|
|
|
|
userIdResult = userId;
|
|
return ret;
|
|
}
|
|
|
|
public OperationResponse LogEventTime(Identifier identifier, out int logId)
|
|
{
|
|
var cardIdQuery = _connection.Query<CardUniqueId>(
|
|
SQLiteProcedures.GET_CARDS_BY_UNIQUE_ID,
|
|
identifier.UniqueId);
|
|
|
|
#region Get/Insert the PK Id Identifier to associate the time log to.
|
|
var ident = new CardUniqueId();
|
|
if (!cardIdQuery.Any())
|
|
{
|
|
//new card, create it!
|
|
ident.UserId_FK = -1;
|
|
ident.CardUId = identifier.UniqueId;
|
|
_connection.Insert(ident);
|
|
}
|
|
else
|
|
{
|
|
//TODO: log when more than one comes back. should NEVER happen but....
|
|
ident = cardIdQuery.First();
|
|
}
|
|
#endregion
|
|
|
|
// Get The User Direction (are they going in or out)?
|
|
var logDirection = GetLogDirection(ident.UserId_FK);
|
|
|
|
#region Check the user hasnt registered an event in the last few minutes..
|
|
|
|
var hysteresisThresholdMinutes = Convert.ToInt32(ConfigurationManager.AppSettings["SwipeTimeGap"] ?? "3");
|
|
var threshold = DateTime.UtcNow.AddMinutes(0 - hysteresisThresholdMinutes);
|
|
var logs = _connection.Query<TimeLogDb>(
|
|
SQLiteProcedures.GET_LOGS_IN_LAST_X_MINUTES,
|
|
threshold, ident.UserId_FK);
|
|
if (logs.Any())
|
|
{
|
|
logId = -1;
|
|
return OperationResponse.FAILED;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Get the current time (for swiping). and calendar week/year to help recall the data.
|
|
var logTime = DateTime.UtcNow;
|
|
var calendarWeek = GetIso8601CalendarWeek(logTime);
|
|
var year = logTime.Year;
|
|
#endregion
|
|
|
|
//TODO: Handle When the identifier is assigned to a user (identifier has -1)
|
|
//when identifier not assigned to user, just store it anyway and carry on, can update later.
|
|
|
|
var timeLog = new TimeLogDb
|
|
{
|
|
SwipeEventDateTime = DateTime.UtcNow,
|
|
UserId_FK = ident.UserId_FK,
|
|
IdentifierId = ident.Id,
|
|
Direction = logDirection,
|
|
Year = year,
|
|
CalendarWeek = calendarWeek
|
|
};
|
|
|
|
_connection.Insert(timeLog);
|
|
logId = timeLog.Id;
|
|
|
|
return OperationResponse.SUCCESS;
|
|
}
|
|
|
|
private List<DailyLogs> GetTimeLogList(int userId, int calendarWeek, int year)
|
|
{
|
|
var timeLogList = _connection.Query<TimeLogDb>(
|
|
SQLiteProcedures.GET_TIMELOGS,
|
|
userId, calendarWeek, year);
|
|
|
|
var timeLogs = timeLogList.Select(x => new TimeLog
|
|
{
|
|
Id = x.Id,
|
|
CalendarWeek = x.CalendarWeek,
|
|
Direction = (LogDirection)x.Direction,
|
|
IdentifierId = x.IdentifierId,
|
|
EventTime = x.SwipeEventDateTime,
|
|
UserId = x.UserId_FK,
|
|
Year = x.Year
|
|
}).ToList();
|
|
|
|
var dict = new Dictionary<DayOfWeek, DailyLogs>();
|
|
var logList = new List<DailyLogs>();
|
|
|
|
//make sure each day of the week is accounted for in the dictionary.
|
|
foreach (DayOfWeek day in Enum.GetValues(typeof(DayOfWeek)))
|
|
{
|
|
dict.Add(day, new DailyLogs());
|
|
}
|
|
|
|
//add the logs to the respective day of the week.
|
|
foreach (var log in timeLogs)
|
|
{
|
|
dict[log.EventTime.DayOfWeek].Logs.Add(log);
|
|
}
|
|
var logGroups = timeLogs.GroupBy(x => x.EventTime.DayOfWeek);
|
|
foreach (var group in logGroups)
|
|
{
|
|
var groupLogs = group.ToList();
|
|
var dailyLog = new DailyLogs
|
|
{
|
|
Logs = groupLogs,
|
|
Day = group.Key,
|
|
DayOfWeek = group.Key.ToString()
|
|
};
|
|
dailyLog.DailyTotal = CalculateDailyTotal(dailyLog);
|
|
logList.Add(dailyLog);
|
|
}
|
|
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()};
|
|
logList.Add(dailyLog);
|
|
}
|
|
|
|
foreach (var dailyCollection in dict)
|
|
{
|
|
dailyCollection.Value.DailyTotal = CalculateDailyTotal(dailyCollection.Value);
|
|
}
|
|
|
|
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 totalCalcMax = IsOdd(logs.Length) ? logs.Length - 1 : logs.Length;
|
|
for (int i = 0; i < totalCalcMax; i += 2)
|
|
{
|
|
totalInTime += (logs[i + 1].EventTime - logs[i].EventTime);
|
|
}
|
|
return Math.Round(totalInTime.TotalMinutes, 2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// determines if the number is an odd or even value
|
|
/// </summary>
|
|
/// <param name="value">number to determine is odd or even</param>
|
|
/// <returns>true - number is odd</returns>
|
|
private bool IsOdd(int value)
|
|
{
|
|
return value % 2 != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the new direction for the user based on previous entry logs in the system.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the user has not logged in today, the direction will be In.
|
|
/// If the user has logged in already today, the direction will be the opposite of the last
|
|
/// recorded log direction. ("out" if "in", "in" if "out")
|
|
/// </remarks>
|
|
/// <param name="userId">Id of the user to get the log direction of.</param>
|
|
/// <returns><see cref="LogDirectionDb"/> indicating what direction the new log is.</returns>
|
|
private LogDirectionDb GetLogDirection(int userId)
|
|
{
|
|
var logDirection = LogDirectionDb.UNKNOWN;
|
|
if (userId != -1)
|
|
{
|
|
var lastEntry = _connection.Query<TimeLogDb>(
|
|
SQLiteProcedures.GET_LAST_TIMELOG_DIRECTION,
|
|
userId);
|
|
if (lastEntry.Any())
|
|
{
|
|
var lastLog = lastEntry.First();
|
|
// See if the datetime retrieved is yesterday. If yesterday, logDirection = true (in)
|
|
if (IsLogDateTimeYesterdayOrOlder(lastLog.SwipeEventDateTime.DateTime))
|
|
{
|
|
logDirection = LogDirectionDb.IN;
|
|
}
|
|
else
|
|
{
|
|
// we have a time log from today already, so just do the opposite of what we last did!
|
|
if (lastLog.Direction == LogDirectionDb.IN)
|
|
logDirection = LogDirectionDb.OUT;
|
|
else if (lastLog.Direction == LogDirectionDb.OUT)
|
|
logDirection = LogDirectionDb.IN;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//assume its the first then!
|
|
logDirection = LogDirectionDb.IN;
|
|
}
|
|
}
|
|
return logDirection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the calendar week of the year according to the ISO8601 standard (starts monday).
|
|
/// </summary>
|
|
/// <param name="date">the date to get the calendar week of.</param>
|
|
/// <returns>the calendar week of the year in integer form (1-52)</returns>
|
|
private int GetIso8601CalendarWeek(DateTime date)
|
|
{
|
|
var day = CultureInfo.InvariantCulture.Calendar.GetDayOfWeek(date);
|
|
if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday)
|
|
{
|
|
date = date.AddDays(3);
|
|
}
|
|
return CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(date, CalendarWeekRule.FirstFourDayWeek,
|
|
DayOfWeek.Monday);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check whether the specified DateTime is from yesterday or older.
|
|
/// </summary>
|
|
/// <param name="dt">the DateTime object to check is yesterday or older</param>
|
|
/// <returns>true - is yesterday or older.</returns>
|
|
private bool IsLogDateTimeYesterdayOrOlder(DateTime dt)
|
|
{
|
|
return dt.Date.CompareTo(DateTime.Today.Date) < 0;
|
|
}
|
|
|
|
private User ChangeToUserObject(UserIdentity user)
|
|
{
|
|
return new User
|
|
{
|
|
UserId = user.Id,
|
|
FirstName = user.FirstName,
|
|
LastName = user.LastName,
|
|
HoursPerWeek = user.HoursPerWeek,
|
|
IsContractor = user.IsContractor
|
|
};
|
|
}
|
|
}
|
|
}
|