using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reflection; using Interfaces; using SQLite.Net; using SQLite.Net.Platform.Win32; using SQLiteRepository.Properties; 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(); _connection.CreateTable(); _connection.CreateTable(); _connection.CreateTable(); _connection.CreateTable(); _connection.CreateTable(); _logger.Trace("Initialised SQLite Repository"); _logger.Trace("Checking For Upgrades"); CheckForDbUpgrade(); } private void CheckForDbUpgrade() { var data = _connection.Query("select * from DbVersion"); if (!data.Any()) { //Pre-Upgrade database, need upgrading _logger.Trace("Pre version 0.2 RC database found, performing update.."); ExecuteUpgradeFromVersion("0.1"); //execute 0.2 upgrade scripts onwards. } else { var installedVersion = new Version(data.First().VersionNumber); var currentVersion = new Version(AssemblyInfo.ASSEMBLY_VERSION); if (currentVersion.CompareTo(installedVersion) > 0) //greater than 0 - current version is newer { _logger.Trace("Installed Database Version: {0} is older than current version {1}"); ExecuteUpgradeFromVersion(installedVersion.ToString()); } } } private void ExecuteUpgradeFromVersion(string installedVersion) { const string PREFIX = "SQLiteRepository.UpgradeScripts."; var instVers = new Version(installedVersion); //so we have established that each script for upgrade will be .sql, so now start at lowest version and work up! var assembly = Assembly.GetExecutingAssembly(); var upgradeScripts = assembly.GetManifestResourceNames() .Where(x=>x.StartsWith(PREFIX)) .OrderBy(x=>new Version(Path.GetFileNameWithoutExtension(x.Replace(PREFIX, "")))) .Where(x => { var scriptVersion = new Version(Path.GetFileNameWithoutExtension(x.Replace(PREFIX, ""))).CompareTo(instVers); return scriptVersion > 0; } ) .ToList(); //now have an ordered list of upgrade script files, so execute in order! foreach (var upgradeScript in upgradeScripts) { using (var stream = assembly.GetManifestResourceStream(upgradeScript)) using(var str = new StreamReader(stream)) { var script = str.ReadToEnd(); _logger.Trace("Executing upgrade script with name: {0}", upgradeScript); _connection.Execute(script); } } SetDbVersion(AssemblyInfo.ASSEMBLY_VERSION); } private void SetDbVersion(string vers) { _connection.DeleteAll(); _connection.Insert(new DbVersion {VersionNumber = vers}); _logger.Trace("Set Database version to: {0}", vers); } public UserList GetUsers(int pageNumber = -1, int pageSize = -1, int groupId = -1) { var ret = new UserList(); List users; int userCount; if (pageNumber != -1 && pageSize != -1) { users = _connection.Query(SQLiteProcedures.GET_ALL_USERS_PAGINATE, pageSize, (pageNumber - 1) * pageSize); userCount = _connection.ExecuteScalar(SQLiteProcedures.GET_TOTAL_USER_COUNT); } else if (groupId != -1) { users = _connection.Query( SQLiteProcedures.GET_ALL_USERS_BY_GROUP, groupId); userCount = users.Count; } else { users = _connection.Query(SQLiteProcedures.GET_ALL_USERS); userCount = users.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); userObj.AssociatedIdentifiers = GetAssociatedIdentifiers(user.Id); userObj.State = GetUserState(GetLogDirection(user.Id)); userObj.LastEventDateTime = GetLastLogDateTime(GetLastTimeLog(user.Id)); userObj.Groups = GetGroups(user.Id); ret.Users.Add(userObj); } if (pageNumber == -1 && pageSize == -1) { ret.PageSize = 10; 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( 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( 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 }); } userObj.State = GetUserState(GetLogDirection(user.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( SQLiteProcedures.GET_USER_BY_ID, id); if (!users.Any()) return ret; var user = users.First(); ret = ChangeToUserObject(user); ret.Groups = GetGroups(); var usersGroups = GetGroups(user.Id); foreach (var group in usersGroups) { ret.Groups.First(x => x.Id == group.Id).IsAssociatedToUser = true; } var cards = _connection.Query( 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; } 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); } ret.UserInformation = GetUser(userId); return ret; } private float GetUserContractedHours(int userId) { var hoursQuery = _connection.Query(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( 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, LastUsed = card.LastUsed.DateTime }); } 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(); //Get a list of current associated identifiers, convert into a list of Identifier Objects.. var currentCards = _connection.Query(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( 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( SQLiteProcedures.UPDATE_USER_DETAILS, 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( SQLiteProcedures.UPDATE_CARD_USER_ID, userId, cardId); } foreach (var card in cardsToRemove) { _connection.Query( SQLiteProcedures.UPDATE_CARD_USER_ID, -1, card); } #endregion #region Update Group Associations SetUserGroups(userId, user.Groups.Where(x => x.IsAssociatedToUser).ToList()); #endregion userIdResult = userId; return ret; } public LogEventResponse LogEventTime(Identifier identifier, out int logId) { var ret = new LogEventResponse(); var cardIdQuery = _connection.Query( 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); UpdateIdentifierLastUsed(DateTimeOffset.UtcNow, ident.Id); logId = -1; //dont try to log any timelogs against this card, as it is unassigned to a user. ret.ProcessResponse=OperationResponse.SUCCESS; ret.Direction = LogDirection.UNKNOWN; return ret; } 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.. 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); var logs = _connection.Query( SQLiteProcedures.GET_LOGS_IN_LAST_X_MINUTES, threshold.Ticks, ident.UserId_FK); _logger.Trace("Checking last swipe event gap"); if (logs.Any()) { _logger.Error("Not logging event for user id: {0}, logged event within TimeGap Threshold of {1}", ident.UserId_FK, threshold); logId = -1; ret.ProcessResponse = OperationResponse.FAILED; ret.Direction = LogDirection.UNKNOWN; return ret; } } #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 var timeLog = new TimeLogDb { SwipeEventDateTime = DateTime.UtcNow, UserId_FK = ident.UserId_FK, IdentifierId = ident.Id, Direction = logDirection, Year = year, CalendarWeek = calendarWeek, Source = LogSourceDb.IDENTIFIER }; _connection.Insert(timeLog); UpdateIdentifierLastUsed(timeLog.SwipeEventDateTime, timeLog.IdentifierId); logId = timeLog.Id; ret.Direction = (LogDirection)(int)logDirection; ret.ProcessResponse = OperationResponse.SUCCESS; return ret; } /*Groups*/ //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 resp = _connection.Insert(groupDb); groupId = groupDb.GroupId; return OperationResponse.CREATED; } public List GetGroups(int userId = -1) { var ret = new List(); List query; if (userId == -1) { query = _connection.Query("select gp.GroupId, gp.GroupName, " + "sum(case when gp.GroupId = ujdb.GroupId_FK then 1 else 0 end) as AssignedUserCount " + "from GroupDb gp " + "left join UserGroupJoinDb ujdb " + "on ujdb.GroupId_FK = gp.GroupId " + "group by gp.GroupId"); } else { query = _connection.Query( "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); } foreach (var group in query) { ret.Add(new Group { Id = group.GroupId, Name = group.GroupName, UserCount = int.Parse(group.AssignedUserCount ?? "0") }); } return ret; } public Group GetGroup(int groupId) { var query = _connection.Query("select * from GroupDb where GroupId = ?", groupId); if (query.Any()) { var group = query.First(); return new Group { Id = group.GroupId, Name = group.GroupName, UserCount = 0 }; } return new Group(); } public OperationResponse UpdateGroup(Group group) { //TODO: I would probably prefer to do this manually.... var resp = _connection.Query("update GroupDb set GroupName=? where GroupId=?", group.Name, group.Id); //var resp = _connection.Update(groupDb); return OperationResponse.UPDATED; } public OperationResponse DeleteGroup(int groupId) { _connection.Delete(groupId); return OperationResponse.DELETED; } public OperationResponse DeleteLog(TimeLog log) { var query = _connection.Query( "select * from TimeLogDb where Id=?", log.Id); if (!query.Any()) return OperationResponse.FAILED; _connection.ExecuteScalar("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( "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( "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 DateTime GetLastLogDateTime(TimeLogDb timeLog) { if (timeLog != null) { return timeLog.SwipeEventDateTime.DateTime; } return DateTime.MinValue; } private bool GetUserState(LogDirectionDb logDirection) { switch (logDirection) { case LogDirectionDb.OUT: return true; default: return false; } } 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 groups) { var groupIds = GetGroupIds(groups.Select(x => x.Name).ToList()); return SetUserGroups(userId, groupIds); } private bool SetUserGroups(int userId, int[] groupIds) { //remove the existing user>group associations _connection.Query("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 })); return true; } private int[] GetGroupIds(List groupNames) { var ret = new List(); foreach (var g in groupNames) { var query = _connection.Query("select GroupId from GroupDb where GroupName=?", g); if (!query.Any()) continue; var id = query.First(); ret.Add(id.GroupId); } return ret.ToArray(); } private List GetTimeLogList(int userId, int calendarWeek, int year) { var timeLogList = _connection.Query( 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 }).OrderBy(x=>x.EventTime.UtcDateTime).ToList(); var dict = new Dictionary(); var logList = new List(); //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.OrderBy(x=>x.EventTime)) { 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.EventTime.UtcDateTime).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); } /// /// determines if the number is an odd or even value /// /// number to determine is odd or even /// true - number is odd private bool IsOdd(int value) { return value % 2 != 0; } /// /// Get the new direction for the user based on previous entry logs in the system. /// /// /// 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") /// /// Id of the user to get the log direction of. /// indicating what direction the new log is. private LogDirectionDb GetLogDirection(int userId) { var logDirection = LogDirectionDb.UNKNOWN; if (userId != -1) { var lastEntry = GetLastTimeLog(userId); if (lastEntry != null) { // See if the datetime retrieved is yesterday. If yesterday, logDirection = true (in) if (IsLogDateTimeYesterdayOrOlder(lastEntry.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 (lastEntry.Direction == LogDirectionDb.IN) logDirection = LogDirectionDb.OUT; else if (lastEntry.Direction == LogDirectionDb.OUT) logDirection = LogDirectionDb.IN; } } else { //assume its the first then! logDirection = LogDirectionDb.IN; } } return logDirection; } private TimeLogDb GetLastTimeLog(int userId) { var lastEntry = _connection.Query( SQLiteProcedures.GET_LAST_TIMELOG_DIRECTION, userId); if (lastEntry.Any()) { return lastEntry.First(); } return null; } private void UpdateIdentifierLastUsed(DateTimeOffset dt, int cardId) { var res = _connection.ExecuteScalar(SQLiteProcedures.UPDATE_CARD_LAST_USED, dt, cardId); } private List GetAssociatedIdentifiers(int userId) { var cards = _connection.Query( SQLiteProcedures.GET_CARDS_BY_USER_ID, userId); var ret = new List(); foreach (var card in cards) { ret.Add(new Identifier() { UniqueId = card.CardUId, IsAssociatedToUser = true, Id = card.Id }); } return ret; } /// /// Get the calendar week of the year according to the ISO8601 standard (starts monday). /// /// the date to get the calendar week of. /// the calendar week of the year in integer form (1-52) 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); } /// /// Check whether the specified DateTime is from yesterday or older. /// /// the DateTime object to check is yesterday or older /// true - is yesterday or older. 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 }; } } }