diff --git a/ShiftsLogger.Ledana/.gitattributes b/ShiftsLogger.Ledana/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/ShiftsLogger.Ledana/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/ShiftsLogger.Ledana/.gitignore b/ShiftsLogger.Ledana/.gitignore new file mode 100644 index 00000000..9491a2fd --- /dev/null +++ b/ShiftsLogger.Ledana/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/ShiftsLogger.Ledana/LICENSE b/ShiftsLogger.Ledana/LICENSE new file mode 100644 index 00000000..a8a795d3 --- /dev/null +++ b/ShiftsLogger.Ledana/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ledana gjoka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ShiftsLogger.Ledana/README.md b/ShiftsLogger.Ledana/README.md new file mode 100644 index 00000000..4c1cd3f6 --- /dev/null +++ b/ShiftsLogger.Ledana/README.md @@ -0,0 +1,79 @@ +Shifts Logger Application + +📌 Overview +This is my first project working with Web APIs. +The application consists of: + +A Web API built with ASP.NET Core and Entity Framework Core to perform CRUD operations on shifts. + +A Console UI application that consumes the API and allows users to interact with shifts and employees. + +The goal is to simulate a real-world shift management system with clean code, edge-case handling, and practical rules. + +⚙️ Features +Web API +CRUD operations: GET, POST, PUT, PATCH, DELETE + +Database access via Entity Framework Core + +Automatic calculation of shift duration in SQL (only start and end times need to be stored) + +Console UI +Displays a list of employees with available IDs + +Allows users to: + +Create shifts + +Start time = DateTime.Now + +End time = random hours/minutes added (up to 12 hours, overnight shifts allowed) + +Max 2 shifts per employee per day, no overlaps + +View shifts + +Paginated list of all shifts + +Filter by date, duration, or employee ID + +Sort by date, duration, or employee ID + +View shifts longer/shorter than a given duration + +Update shifts (full or partial) with validation rules + +Delete shifts that started or ended on the current day + +User feedback messages for successful or invalid operations + +🛠 Rules & Validations +Shifts cannot exceed 12 hours + +Employees can have max 2 shifts per day + +Shifts must not overlap + +Duration is always calculated automatically in the database + +🚀 Getting Started +Clone the repository + +Important AutoMapper Setup: + +Before running migrations, AutoMapper must be upgraded to version 15 or 16. Lower versions (like 12) are vulnerable and will not work with EF migrations. +Before running the application, AutoMapper must be set to version 11 or 12, matching the version of its extension. If versions mismatch, the app will fail to run. +This step is required until a cleaner fix is implemented. + +Set up the database with EF Core migrations + +Run the Web API project + +Run the Console UI project to interact with the API + +📖 Notes +This project is a learning exercise in building APIs and consuming them with a client application. + +The focus is on writing clean, organized, and testable code. + +Improvements will continue as I learn more about architecture, best practices, and testing. diff --git a/ShiftsLogger.Ledana/ShiftsLogger.Ledana.slnx b/ShiftsLogger.Ledana/ShiftsLogger.Ledana.slnx new file mode 100644 index 00000000..d2c972c3 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLogger.Ledana.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Controllers/EmployeeController.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Controllers/EmployeeController.cs new file mode 100644 index 00000000..61fcbd75 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Controllers/EmployeeController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using ShiftsLoggerAPI.Ledana.Models; +using ShiftsLoggerAPI.Ledana.Services; + +namespace ShiftsLoggerAPI.Ledana.Controllers +{ + [ApiController] + [Route("[controller]")] + public class EmployeeController : ControllerBase + { + private readonly IEmplyeeService _workerService; + public EmployeeController(IEmplyeeService workerService) + { + _workerService = workerService; + } + [HttpGet] + public async Task>> GetAllWorkers() + { + var workers = await _workerService.GetAllWorkers(); + + return Ok(workers); + } + + [HttpGet("{id}")] + public async Task> GetWorkerById(int id) + { + Employee? worker = await _workerService.GetWorkerById(id); + + if (worker is null) return NotFound(); + + return Ok(worker); + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Controllers/ShiftController.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Controllers/ShiftController.cs new file mode 100644 index 00000000..7f9fa8e5 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Controllers/ShiftController.cs @@ -0,0 +1,100 @@ +using Microsoft.AspNetCore.Mvc; +using ShiftsLoggerAPI.Ledana.DTOs; +using ShiftsLoggerAPI.Ledana.Models; +using ShiftsLoggerAPI.Ledana.Services; + +namespace ShiftsLoggerAPI.Ledana.Controllers +{ + [ApiController] + [Route("[controller]")] + public class ShiftController : ControllerBase + { + private readonly IShiftService _shiftService; + public ShiftController(IShiftService shiftService) + { + _shiftService = shiftService; + } + + [HttpGet] + public async Task>>> GetAllShifts(ShiftOptions shiftOptions) + { + var shifts = await _shiftService.GetShifts(shiftOptions); + return Ok(shifts); + } + [HttpGet("all")] + public async Task>> GetJustShifts() + { + var shifts = await _shiftService.GetJustShifts(); + + if (shifts is null) return BadRequest(); + + return Ok(shifts); + } + + + [HttpGet("{id}")] + public async Task>> GetShift(int id) + { + var shift = await _shiftService.GetShiftById(id); + if (shift is null) return NotFound(); + + return Ok(shift); + } + + [HttpPost] + public async Task>> Post(ShiftDto shift) + { + + if (!ModelState.IsValid) + return BadRequest(); + + var newShift = await _shiftService.CreateShift(shift); + + if (newShift.Data is null) return BadRequest(newShift); + + return new ObjectResult(newShift) { StatusCode = 201 }; + } + + [HttpDelete("{id}")] + public async Task>> Delete(int id) + { + var result = await _shiftService.DeleteShift(id); + + if (result is null) return NotFound(result); + if (result.RequestFailed == true) return BadRequest(result); + + return Ok(result); + } + + [HttpPut("{id}")] + public async Task>> UpdateShift(int id, [FromBody] ShiftDto shift) + { + if (!ModelState.IsValid) + { + return BadRequest(); + } + + var newShift = await _shiftService.UpdateShift(id, shift); + + if (newShift.Data is null) return NotFound(newShift); + + return Ok(newShift); + } + + [HttpPatch("{id}")] + public async Task>> UpdatePartialShift(int id, [FromBody] PartialShiftDto shift) + { + if (!ModelState.IsValid) + { + return BadRequest(); + } + + var newShift = await _shiftService.UpdatePartialShift(id, shift); + + if (newShift.RequestFailed == true) return NotFound(newShift); + + return Ok(newShift); + } + + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ApiResponseDto.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ApiResponseDto.cs new file mode 100644 index 00000000..2867193a --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ApiResponseDto.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace ShiftsLoggerAPI.Ledana.DTOs +{ + public class ApiResponseDto + { + public bool RequestFailed { get; set; } = false; + public HttpStatusCode ResponseCode { get; set; } + public string ErrorMessage { get; set; } = string.Empty; + public T? Data { get; set; } + + public int TotalCount { get; set; } + public int CurrentPage { get; set; } + public bool HasNext { get; set; } + + public bool HasPrevious { get; set; } + public int PageSize { get; set; } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/PartialShiftDto.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/PartialShiftDto.cs new file mode 100644 index 00000000..c8ae073f --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/PartialShiftDto.cs @@ -0,0 +1,10 @@ +namespace ShiftsLoggerAPI.Ledana.DTOs +{ + //this dto is only used to patch, when the user wants to only change some of the props + public class PartialShiftDto + { + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public int? EmployeeId { get; set; } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ShiftDto.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ShiftDto.cs new file mode 100644 index 00000000..7f482e71 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ShiftDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace ShiftsLoggerAPI.Ledana.DTOs +{ + public class ShiftDto + { + [Required] + public DateTime? StartTime { get; set; } + [Required] + public DateTime? EndTime { get; set; } + [Required] + public int? EmployeeId { get; set; } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ShiftOptions.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ShiftOptions.cs new file mode 100644 index 00000000..d6daf5cf --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/DTOs/ShiftOptions.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ShiftsLoggerAPI.Ledana.DTOs +{ + public class ShiftOptions + { + [FromQuery(Name = "date")] + public DateTime? Date { get; set; } + [FromQuery(Name = "duration")] + public string? Duration { get; set; } + [FromQuery(Name = "employee_id")] + public int? EmployeeId { get; set; } + + [FromQuery(Name = "sort_by")] + public string SortBy { get; set; } = "id"; + [FromQuery(Name = "sort_order")] + public string SortOrder { get; set; } = "ASC"; + [FromQuery(Name = "search")] + public string Search { get; set; } = string.Empty; + [FromQuery(Name = "page_size")] + public int PageSize { get; set; } = 10; + [FromQuery(Name = "page_number")] + public int PageNumber { get; set; } = 1; + + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Data/ShiftContext.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Data/ShiftContext.cs new file mode 100644 index 00000000..6a88e3d2 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Data/ShiftContext.cs @@ -0,0 +1,207 @@ +using Microsoft.EntityFrameworkCore; +using ShiftsLoggerAPI.Ledana.Models; + +namespace ShiftsLoggerAPI.Ledana.Data +{ + public class ShiftContext : DbContext + { + public DbSet Shifts { get; set; } + public DbSet Employees { get; set; } + + public ShiftContext(DbContextOptions options) + : base (options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData( + new() { Id = 1, FirstName = "Amelia", LastName = "Aster" }, + new() { Id = 2, FirstName = "Lory", LastName = "Marti" }, + new() { Id = 3, FirstName = "Laila", LastName = "Martini" }, + new() { Id = 4, FirstName = "Vivian", LastName = "Scott" }, + new() { Id = 5, FirstName = "Luiza", LastName = "Griffin" }, + new() { Id = 6, FirstName = "Violet", LastName = "Jinx" } + ); + + modelBuilder.Entity() + .Property(s => s.Duration) + .HasComputedColumnSql( + "CONVERT(varchar(5), DATEADD(MINUTE, DATEDIFF(minute, [StartTime], [EndTime]), 0),108)", + stored: true); + + + modelBuilder.Entity().HasData( + new() + { + Id = 1, + StartTime = new(2026, 05, 01, 09, 30, 00), + EndTime = new(2026, 05, 01, 14, 35, 00), + EmployeeId = 2 + }, + new() + { + Id = 2, + StartTime = new(2026, 05, 01, 10, 30, 00), + EndTime = new(2026, 05, 01, 16, 00, 00), + EmployeeId = 3 + }, + new() + { + Id = 3, + StartTime = new(2026, 05, 01, 09, 00, 00), + EndTime = new(2026, 05, 01, 14, 55, 00), + EmployeeId = 4 + }, + new() + { + Id = 4, + StartTime = new(2026, 05, 01, 09, 30, 00), + EndTime = new(2026, 05, 01, 17, 30, 00), + EmployeeId = 5 + }, + new() + { + Id = 5, + StartTime = new(2026, 05, 01, 23, 30, 00), + EndTime = new(2026, 06, 02, 05, 35, 00), + EmployeeId = 2 + }, + new() + { + Id = 6, + StartTime = new(2026, 05, 02, 06, 30, 00), + EndTime = new(2026, 05, 02, 16, 00, 00), + EmployeeId = 3 + }, + new() + { + Id = 7, + StartTime = new(2026, 05, 02, 09, 00, 00), + EndTime = new(2026, 05, 02, 14, 55, 00), + EmployeeId = 4 + }, + new() + { + Id = 8, + StartTime = new(2026, 05, 02, 12, 30, 00), + EndTime = new(2026, 05, 02, 17, 30, 00), + EmployeeId = 5 + }, + new() + { + Id = 9, + StartTime = new(2026, 05, 02, 22, 30, 00), + EndTime = new(2026, 06, 03, 06, 00, 00), + EmployeeId = 6 + }, + new() + { + Id = 10, + StartTime = new(2026, 05, 03, 12, 30, 00), + EndTime = new(2026, 05, 03, 19, 00, 00), + EmployeeId = 1 + }, + new() + { + Id = 11, + StartTime = new(2026, 05, 03, 19, 00, 00), + EndTime = new(2026, 05, 03, 23, 55, 00), + EmployeeId = 2 + }, + new() + { + Id = 12, + StartTime = new(2026, 05, 04, 09, 30, 00), + EndTime = new(2026, 05, 04, 17, 30, 00), + EmployeeId = 5 + }, + new() + { + Id = 13, + StartTime = new(2026, 05, 06, 09, 30, 00), + EndTime = new(2026, 05, 06, 14, 35, 00), + EmployeeId = 2 + }, + new() + { + Id = 14, + StartTime = new(2026, 05, 06, 10, 30, 00), + EndTime = new(2026, 05, 06, 16, 00, 00), + EmployeeId = 3 + }, + new() + { + Id = 15, + StartTime = new(2026, 05, 07, 09, 00, 00), + EndTime = new(2026, 05, 07, 14, 55, 00), + EmployeeId = 4 + }, + new() + { + Id = 16, + StartTime = new(2026, 05, 08, 09, 30, 00), + EndTime = new(2026, 05, 08, 17, 30, 00), + EmployeeId = 5 + }, + new() + { + Id = 17, + StartTime = new(2026, 05, 08, 23, 30, 00), + EndTime = new(2026, 06, 09, 05, 35, 00), + EmployeeId = 2 + }, + new() + { + Id = 18, + StartTime = new(2026, 05, 08, 06, 30, 00), + EndTime = new(2026, 05, 08, 16, 00, 00), + EmployeeId = 3 + }, + new() + { + Id = 19, + StartTime = new(2026, 05, 08, 09, 00, 00), + EndTime = new(2026, 05, 09, 14, 55, 00), + EmployeeId = 4 + }, + new() + { + Id = 20, + StartTime = new(2026, 05, 09, 12, 30, 00), + EndTime = new(2026, 05, 09, 17, 30, 00), + EmployeeId = 3 + }, + new() + { + Id = 21, + StartTime = new(2026, 05, 09, 22, 30, 00), + EndTime = new(2026, 06, 10, 06, 00, 00), + EmployeeId = 6 + }, + new() + { + Id = 22, + StartTime = new(2026, 05, 09, 12, 30, 00), + EndTime = new(2026, 05, 09, 19, 00, 00), + EmployeeId = 1 + }, + new() + { + Id = 23, + StartTime = new(2026, 05, 09, 17, 00, 00), + EndTime = new(2026, 05, 09, 23, 25, 00), + EmployeeId = 2 + }, + new() + { + Id = 24, + StartTime = new(2026, 05, 09, 21, 30, 00), + EndTime = new(2026, 05, 10, 05, 30, 00), + EmployeeId = 5 + } + ); + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Mappings/MappingProfile.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Mappings/MappingProfile.cs new file mode 100644 index 00000000..f0f863b0 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Mappings/MappingProfile.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using ShiftsLoggerAPI.Ledana.DTOs; +using ShiftsLoggerAPI.Ledana.Models; + +namespace ShiftsLoggerAPI.Ledana.Mappings +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/20260513112302_InitialCreate.Designer.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/20260513112302_InitialCreate.Designer.cs new file mode 100644 index 00000000..ba2a24b1 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/20260513112302_InitialCreate.Designer.cs @@ -0,0 +1,305 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ShiftsLoggerAPI.Ledana.Data; + +#nullable disable + +namespace ShiftsLoggerAPI.Ledana.Migrations +{ + [DbContext(typeof(ShiftContext))] + [Migration("20260513112302_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Employees"); + + b.HasData( + new + { + Id = 1, + FirstName = "Amelia", + LastName = "Aster" + }, + new + { + Id = 2, + FirstName = "Lory", + LastName = "Marti" + }, + new + { + Id = 3, + FirstName = "Laila", + LastName = "Martini" + }, + new + { + Id = 4, + FirstName = "Vivian", + LastName = "Scott" + }, + new + { + Id = 5, + FirstName = "Luiza", + LastName = "Griffin" + }, + new + { + Id = 6, + FirstName = "Violet", + LastName = "Jinx" + }); + }); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Duration") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("nvarchar(max)") + .HasComputedColumnSql("CONVERT(varchar(5), DATEADD(MINUTE, DATEDIFF(minute, [StartTime], [EndTime]), 0),108)", true); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Shifts"); + + b.HasData( + new + { + Id = 1, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 1, 14, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 1, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 10, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 1, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 1, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + EmployeeId = 2, + EndTime = new DateTime(2026, 6, 2, 5, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 23, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 6, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 2, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 6, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 7, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 2, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 8, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 2, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 9, + EmployeeId = 6, + EndTime = new DateTime(2026, 6, 3, 6, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 22, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 10, + EmployeeId = 1, + EndTime = new DateTime(2026, 5, 3, 19, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 3, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 11, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 3, 23, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 3, 19, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 12, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 4, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 4, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 13, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 6, 14, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 6, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 14, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 6, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 6, 10, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 15, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 7, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 7, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 16, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 8, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 17, + EmployeeId = 2, + EndTime = new DateTime(2026, 6, 9, 5, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 23, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 18, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 8, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 6, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 19, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 9, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 20, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 9, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 21, + EmployeeId = 6, + EndTime = new DateTime(2026, 6, 10, 6, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 22, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 22, + EmployeeId = 1, + EndTime = new DateTime(2026, 5, 9, 19, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 23, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 9, 23, 25, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 17, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 24, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 10, 5, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 21, 30, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Shift", b => + { + b.HasOne("ShiftsLoggerAPI.Ledana.Models.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Employee", b => + { + b.Navigation("Shifts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/20260513112302_InitialCreate.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/20260513112302_InitialCreate.cs new file mode 100644 index 00000000..22325a2b --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/20260513112302_InitialCreate.cs @@ -0,0 +1,112 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace ShiftsLoggerAPI.Ledana.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Employees", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Employees", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Shifts", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + StartTime = table.Column(type: "datetime2", nullable: false), + EndTime = table.Column(type: "datetime2", nullable: false), + Duration = table.Column(type: "nvarchar(max)", nullable: false, computedColumnSql: "CONVERT(varchar(5), DATEADD(MINUTE, DATEDIFF(minute, [StartTime], [EndTime]), 0),108)", stored: true), + EmployeeId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Shifts", x => x.Id); + table.ForeignKey( + name: "FK_Shifts_Employees_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Employees", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Employees", + columns: new[] { "Id", "FirstName", "LastName" }, + values: new object[,] + { + { 1, "Amelia", "Aster" }, + { 2, "Lory", "Marti" }, + { 3, "Laila", "Martini" }, + { 4, "Vivian", "Scott" }, + { 5, "Luiza", "Griffin" }, + { 6, "Violet", "Jinx" } + }); + + migrationBuilder.InsertData( + table: "Shifts", + columns: new[] { "Id", "EmployeeId", "EndTime", "StartTime" }, + values: new object[,] + { + { 1, 2, new DateTime(2026, 5, 1, 14, 35, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 1, 9, 30, 0, 0, DateTimeKind.Unspecified) }, + { 2, 3, new DateTime(2026, 5, 1, 16, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 1, 10, 30, 0, 0, DateTimeKind.Unspecified) }, + { 3, 4, new DateTime(2026, 5, 1, 14, 55, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 1, 9, 0, 0, 0, DateTimeKind.Unspecified) }, + { 4, 5, new DateTime(2026, 5, 1, 17, 30, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 1, 9, 30, 0, 0, DateTimeKind.Unspecified) }, + { 5, 2, new DateTime(2026, 6, 2, 5, 35, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 1, 23, 30, 0, 0, DateTimeKind.Unspecified) }, + { 6, 3, new DateTime(2026, 5, 2, 16, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 2, 6, 30, 0, 0, DateTimeKind.Unspecified) }, + { 7, 4, new DateTime(2026, 5, 2, 14, 55, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 2, 9, 0, 0, 0, DateTimeKind.Unspecified) }, + { 8, 5, new DateTime(2026, 5, 2, 17, 30, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 2, 12, 30, 0, 0, DateTimeKind.Unspecified) }, + { 9, 6, new DateTime(2026, 6, 3, 6, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 2, 22, 30, 0, 0, DateTimeKind.Unspecified) }, + { 10, 1, new DateTime(2026, 5, 3, 19, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 3, 12, 30, 0, 0, DateTimeKind.Unspecified) }, + { 11, 2, new DateTime(2026, 5, 3, 23, 55, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 3, 19, 0, 0, 0, DateTimeKind.Unspecified) }, + { 12, 5, new DateTime(2026, 5, 4, 17, 30, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 4, 9, 30, 0, 0, DateTimeKind.Unspecified) }, + { 13, 2, new DateTime(2026, 5, 6, 14, 35, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 6, 9, 30, 0, 0, DateTimeKind.Unspecified) }, + { 14, 3, new DateTime(2026, 5, 6, 16, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 6, 10, 30, 0, 0, DateTimeKind.Unspecified) }, + { 15, 4, new DateTime(2026, 5, 7, 14, 55, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 7, 9, 0, 0, 0, DateTimeKind.Unspecified) }, + { 16, 5, new DateTime(2026, 5, 8, 17, 30, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 8, 9, 30, 0, 0, DateTimeKind.Unspecified) }, + { 17, 2, new DateTime(2026, 6, 9, 5, 35, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 8, 23, 30, 0, 0, DateTimeKind.Unspecified) }, + { 18, 3, new DateTime(2026, 5, 8, 16, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 8, 6, 30, 0, 0, DateTimeKind.Unspecified) }, + { 19, 4, new DateTime(2026, 5, 9, 14, 55, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 8, 9, 0, 0, 0, DateTimeKind.Unspecified) }, + { 20, 3, new DateTime(2026, 5, 9, 17, 30, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 9, 12, 30, 0, 0, DateTimeKind.Unspecified) }, + { 21, 6, new DateTime(2026, 6, 10, 6, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 9, 22, 30, 0, 0, DateTimeKind.Unspecified) }, + { 22, 1, new DateTime(2026, 5, 9, 19, 0, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 9, 12, 30, 0, 0, DateTimeKind.Unspecified) }, + { 23, 2, new DateTime(2026, 5, 9, 23, 25, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 9, 17, 0, 0, 0, DateTimeKind.Unspecified) }, + { 24, 5, new DateTime(2026, 5, 10, 5, 30, 0, 0, DateTimeKind.Unspecified), new DateTime(2026, 5, 9, 21, 30, 0, 0, DateTimeKind.Unspecified) } + }); + + migrationBuilder.CreateIndex( + name: "IX_Shifts_EmployeeId", + table: "Shifts", + column: "EmployeeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Shifts"); + + migrationBuilder.DropTable( + name: "Employees"); + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/ShiftContextModelSnapshot.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/ShiftContextModelSnapshot.cs new file mode 100644 index 00000000..7ea17941 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Migrations/ShiftContextModelSnapshot.cs @@ -0,0 +1,302 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using ShiftsLoggerAPI.Ledana.Data; + +#nullable disable + +namespace ShiftsLoggerAPI.Ledana.Migrations +{ + [DbContext(typeof(ShiftContext))] + partial class ShiftContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Employees"); + + b.HasData( + new + { + Id = 1, + FirstName = "Amelia", + LastName = "Aster" + }, + new + { + Id = 2, + FirstName = "Lory", + LastName = "Marti" + }, + new + { + Id = 3, + FirstName = "Laila", + LastName = "Martini" + }, + new + { + Id = 4, + FirstName = "Vivian", + LastName = "Scott" + }, + new + { + Id = 5, + FirstName = "Luiza", + LastName = "Griffin" + }, + new + { + Id = 6, + FirstName = "Violet", + LastName = "Jinx" + }); + }); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Shift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Duration") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("nvarchar(max)") + .HasComputedColumnSql("CONVERT(varchar(5), DATEADD(MINUTE, DATEDIFF(minute, [StartTime], [EndTime]), 0),108)", true); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Shifts"); + + b.HasData( + new + { + Id = 1, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 1, 14, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 2, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 1, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 10, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 3, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 1, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 4, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 1, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 5, + EmployeeId = 2, + EndTime = new DateTime(2026, 6, 2, 5, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 1, 23, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 6, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 2, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 6, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 7, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 2, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 8, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 2, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 9, + EmployeeId = 6, + EndTime = new DateTime(2026, 6, 3, 6, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 2, 22, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 10, + EmployeeId = 1, + EndTime = new DateTime(2026, 5, 3, 19, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 3, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 11, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 3, 23, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 3, 19, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 12, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 4, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 4, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 13, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 6, 14, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 6, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 14, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 6, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 6, 10, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 15, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 7, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 7, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 16, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 8, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 9, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 17, + EmployeeId = 2, + EndTime = new DateTime(2026, 6, 9, 5, 35, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 23, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 18, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 8, 16, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 6, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 19, + EmployeeId = 4, + EndTime = new DateTime(2026, 5, 9, 14, 55, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 8, 9, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 20, + EmployeeId = 3, + EndTime = new DateTime(2026, 5, 9, 17, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 21, + EmployeeId = 6, + EndTime = new DateTime(2026, 6, 10, 6, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 22, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 22, + EmployeeId = 1, + EndTime = new DateTime(2026, 5, 9, 19, 0, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 12, 30, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 23, + EmployeeId = 2, + EndTime = new DateTime(2026, 5, 9, 23, 25, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 17, 0, 0, 0, DateTimeKind.Unspecified) + }, + new + { + Id = 24, + EmployeeId = 5, + EndTime = new DateTime(2026, 5, 10, 5, 30, 0, 0, DateTimeKind.Unspecified), + StartTime = new DateTime(2026, 5, 9, 21, 30, 0, 0, DateTimeKind.Unspecified) + }); + }); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Shift", b => + { + b.HasOne("ShiftsLoggerAPI.Ledana.Models.Employee", "Employee") + .WithMany("Shifts") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ShiftsLoggerAPI.Ledana.Models.Employee", b => + { + b.Navigation("Shifts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Models/Employee.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Models/Employee.cs new file mode 100644 index 00000000..a8da0b3a --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Models/Employee.cs @@ -0,0 +1,11 @@ +namespace ShiftsLoggerAPI.Ledana.Models +{ + public class Employee + { + public int Id { get; set; } + public string FirstName { get; set; } = null!; + public string LastName { get; set; } = null!; + public List Shifts { get; set; } = []; + + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Models/Shift.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Models/Shift.cs new file mode 100644 index 00000000..8c0c6cb7 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Models/Shift.cs @@ -0,0 +1,12 @@ +namespace ShiftsLoggerAPI.Ledana.Models +{ + public class Shift + { + public int Id { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string Duration { get; set; } = null!; + public int EmployeeId { get; set; } + public Employee Employee { get; set; } = null!; + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Program.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Program.cs new file mode 100644 index 00000000..bbfce68b --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Program.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using ShiftsLoggerAPI.Ledana.Data; +using ShiftsLoggerAPI.Ledana.Services; + +var builder = WebApplication.CreateBuilder(args); + +//1configuring the connection to the database using sql server +var connectionString = builder.Configuration.GetConnectionString("ShiftsLoggerDb") + ?? throw new InvalidOperationException("Connection string 'ShiftsLoggerDb' not found!"); +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); + +//2injecting shift service +builder.Services.AddScoped(); +//5adding the worker service +builder.Services.AddScoped(); + +// Add services to the container. +//3configuring json serializer to handle cycles +builder.Services.AddControllers().AddJsonOptions(opt => + opt.JsonSerializerOptions.ReferenceHandler = + System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles); + +//4adding the mapper +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + + +} + +app.MapControllers(); + +app.Run(); diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Properties/launchSettings.json b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Properties/launchSettings.json new file mode 100644 index 00000000..7b64c3e1 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5188", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7264;http://localhost:5188", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/EmployeeService.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/EmployeeService.cs new file mode 100644 index 00000000..b5471e1c --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/EmployeeService.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using ShiftsLoggerAPI.Ledana.Data; +using ShiftsLoggerAPI.Ledana.Models; + +namespace ShiftsLoggerAPI.Ledana.Services +{ + public class EmployeeService : IEmplyeeService + { + private readonly ShiftContext _dbContext; + + public EmployeeService(ShiftContext dbContext) + { + _dbContext = dbContext; + } + + public async Task> GetAllWorkers() + { + var workers = await _dbContext.Employees.ToListAsync(); + return workers; + } + + public async Task GetWorkerById(int id) + { + var worker = await _dbContext.Employees.FindAsync(id); + return worker; + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/IEmplyeeService.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/IEmplyeeService.cs new file mode 100644 index 00000000..2343a85c --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/IEmplyeeService.cs @@ -0,0 +1,10 @@ +using ShiftsLoggerAPI.Ledana.Models; + +namespace ShiftsLoggerAPI.Ledana.Services +{ + public interface IEmplyeeService + { + public Task> GetAllWorkers(); + public Task GetWorkerById(int id); + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/IShiftService.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/IShiftService.cs new file mode 100644 index 00000000..b2412e92 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/IShiftService.cs @@ -0,0 +1,16 @@ +using ShiftsLoggerAPI.Ledana.DTOs; +using ShiftsLoggerAPI.Ledana.Models; + +namespace ShiftsLoggerAPI.Ledana.Services +{ + public interface IShiftService + { + public Task> CreateShift(ShiftDto shift); + public Task> DeleteShift(int id); + public Task> GetShiftById(int id); + public Task>> GetShifts(ShiftOptions shiftOptions); + public Task> UpdatePartialShift(int id, PartialShiftDto shift); + public Task> UpdateShift(int id, ShiftDto shift); + public Task> GetJustShifts(); + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/ShiftService.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/ShiftService.cs new file mode 100644 index 00000000..c974f4f7 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Services/ShiftService.cs @@ -0,0 +1,284 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using ShiftsLoggerAPI.Ledana.Data; +using ShiftsLoggerAPI.Ledana.DTOs; +using ShiftsLoggerAPI.Ledana.Models; +using System.Net; + +namespace ShiftsLoggerAPI.Ledana.Services +{ + public class ShiftService : IShiftService + { + private readonly ShiftContext _dbContext; + private readonly IMapper _mapper; + + public ShiftService(ShiftContext shiftContext, IMapper mapper) + { + _dbContext = shiftContext; + _mapper = mapper; + } + public async Task> CreateShift(ShiftDto shiftDto) + { + Shift shift = _mapper.Map(shiftDto); + + if (!Validator.ValidateEndTime(shift.StartTime, shift.EndTime)) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.BadRequest, + ErrorMessage = "End Time of the shift should be after the Start Time" + }; + } + + //shift.Duration = shift.EndTime - shift.StartTime; + + var savedShift = await _dbContext.Shifts.AddAsync(shift); + await _dbContext.SaveChangesAsync(); + + return new ApiResponseDto + { + Data = savedShift.Entity, + ResponseCode = HttpStatusCode.Created + }; + } + + public async Task> DeleteShift(int id) + { + var shift = await _dbContext.Shifts.Include(s => s.Employee) + .FirstOrDefaultAsync(s => s.Id == id); + + if (shift is null) + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Resource with id {id} was not found" + }; + + _dbContext.Remove(shift); + + await _dbContext.SaveChangesAsync(); + + return new ApiResponseDto + { + Data = $"Shift with id {id} deleted successfully!", + ResponseCode = HttpStatusCode.NoContent + }; + } + + public async Task> GetShiftById(int id) + { + var shift = await _dbContext.Shifts.Include(s => s.Employee) + .FirstOrDefaultAsync(s => s.Id == id); + + if (shift is null) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Resource with id {id} was not found" + }; + } + + return new ApiResponseDto + { + Data = shift, + ResponseCode = HttpStatusCode.OK + }; + } + + public async Task> GetJustShifts() + { + return await _dbContext.Shifts.Include(s => s.Employee).ToListAsync(); + } + + public async Task>> GetShifts(ShiftOptions shiftOptions) + { + var query = _dbContext.Shifts.Include(s => s.Employee).AsQueryable(); + + var totalShifts = await query.CountAsync(); + List? shifts; + + //when user wants to view shifts per date, it will be shifts that started or ended on that date + if (shiftOptions.Date.HasValue) + { + var start = shiftOptions.Date.Value.Date; + var end = start.AddDays(1); + query = query + .Where(s => + s.EndTime >= shiftOptions.Date && s.EndTime < end + || s.StartTime >= shiftOptions.Date && s.StartTime < end); + } + if (shiftOptions.Duration is not null) + { + query = query.Where(s => s.Duration == shiftOptions.Duration); + } + if (shiftOptions.EmployeeId.HasValue) + { + query = query.Where(s => s.EmployeeId == shiftOptions.EmployeeId); + } + + if (shiftOptions.SortBy == "id" || !string.IsNullOrEmpty(shiftOptions.SortBy)) + { + switch (shiftOptions.SortBy) + { + case "date": + query = shiftOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(s => s.StartTime) + : query.OrderByDescending(s => s.StartTime); + break; + case "duration": + query = shiftOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(s => s.Duration) + : query.OrderByDescending(s => s.Duration); + break; + case "employee_id": + query = shiftOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(s => s.EmployeeId) + : query.OrderByDescending(s => s.EmployeeId); + break; + default: + query = shiftOptions.SortOrder.ToUpper() == "ASC" + ? query.OrderBy(s => s.Id) + : query.OrderByDescending(s => s.Id); + break; + } + } + query = query.Skip((shiftOptions.PageNumber - 1) * shiftOptions.PageSize) + .Take(shiftOptions.PageSize); + shifts = await query.ToListAsync(); + + bool hasPrevious = shiftOptions.PageNumber > 1; + bool hasNext = (shiftOptions.PageNumber * shiftOptions.PageSize) < totalShifts; + + return new ApiResponseDto> + { + Data = shifts, + ResponseCode = HttpStatusCode.OK, + TotalCount = totalShifts, + CurrentPage = shiftOptions.PageNumber, + PageSize = shiftOptions.PageSize, + HasPrevious = hasPrevious, + HasNext = hasNext + }; + } + + public async Task> UpdatePartialShift(int id, PartialShiftDto shift) + { + var shiftToUpdate = await _dbContext.Shifts.Include(s => s.Employee) + .FirstOrDefaultAsync(s => s.Id == id); + + if (shiftToUpdate is null) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Resource with id {id} was not found" + }; + } + + if (!ValidateEmployeeId(shiftToUpdate.EmployeeId)) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Employee Id {shiftToUpdate.EmployeeId} is not valid!" + }; + } + + if (shift.StartTime.HasValue) + shiftToUpdate.StartTime = shift.StartTime.Value; + if (shift.EndTime.HasValue) + shiftToUpdate.EndTime = shift.EndTime.Value; + if (shift.EmployeeId.HasValue) + shiftToUpdate.EmployeeId = shift.EmployeeId.Value; + + if (!Validator.ValidateEndTime(shiftToUpdate.StartTime, shiftToUpdate.EndTime)) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.BadRequest, + ErrorMessage = "End Time of the shift should be after the Start Time" + }; + } + + //shiftToUpdate.Duration = shiftToUpdate.EndTime - shiftToUpdate.StartTime; + + await _dbContext.SaveChangesAsync(); + + return new ApiResponseDto + { + Data = shiftToUpdate, + ResponseCode = HttpStatusCode.OK + }; + } + + public async Task> UpdateShift(int id, ShiftDto shift) + { + var shiftToUpdate = await _dbContext.Shifts.Include(s => s.Employee) + .FirstOrDefaultAsync(s => s.Id == id); + + if (shiftToUpdate is null) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Resource with id {id} was not found" + }; + } + + if (!ValidateEmployeeId(shiftToUpdate.EmployeeId)) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = $"Employee Id {shiftToUpdate.EmployeeId} is not valid!" + }; + } + + shiftToUpdate = _mapper.Map(shift, shiftToUpdate); + + + if (!Validator.ValidateEndTime(shiftToUpdate.StartTime, shiftToUpdate.EndTime)) + { + return new ApiResponseDto + { + RequestFailed = true, + Data = null, + ResponseCode = HttpStatusCode.NotFound, + ErrorMessage = "End Time of the shift should be after the Start Time" + }; + } + + //shiftToUpdate.Duration = shiftToUpdate.EndTime - shiftToUpdate.StartTime; + + _dbContext.Shifts.Update(shiftToUpdate); + await _dbContext.SaveChangesAsync(); + + return new ApiResponseDto + { + Data = shiftToUpdate, + ResponseCode = HttpStatusCode.OK + }; + } + private bool ValidateEmployeeId(int EmployeeId) + { + return _dbContext.Employees.Any(w => w.Id == EmployeeId); + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLogger.Ledana.http b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLogger.Ledana.http new file mode 100644 index 00000000..b2cdb995 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLogger.Ledana.http @@ -0,0 +1,47 @@ +@ShiftsLogger.Ledana_HostAddress = https://localhost:7264 + + +### + +POST {{ShiftsLogger.Ledana_HostAddress}}/shift +Content-Type: application/json + +{ + "StartTime": "2026-04-30T09:30:00", + "EndTime": "2026-04-30T17:30:00", + "EmployeeId": 3 +} + +### + +GET {{ShiftsLogger.Ledana_HostAddress}}/shift/all + + +### + +DELETE {{ShiftsLogger.Ledana_HostAddress}}/shift/26 + +### + +PUT {{ShiftsLogger.Ledana_HostAddress}}/shift/15 +Content-Type: application/json + +{ + "startTime": "2026-04-29T10:30:00", + "endTime": "2026-04-29T17:30:00", + "workerId": 6 +} + +### + +PATCH {{ShiftsLogger.Ledana_HostAddress}}/shift/18 +Content-Type: application/json + +{ + "startTime": "2026-05-11T09:30:00" +} + +### + +GET {{ShiftsLogger.Ledana_HostAddress}}/shift/14 +### diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLoggerAPI.Ledana.csproj b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLoggerAPI.Ledana.csproj new file mode 100644 index 00000000..860b930c --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLoggerAPI.Ledana.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLoggerAPI.Ledana.http b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLoggerAPI.Ledana.http new file mode 100644 index 00000000..220ea067 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/ShiftsLoggerAPI.Ledana.http @@ -0,0 +1,5 @@ +@ShiftsLoggerAPI.Ledana_HostAddress = https://localhost:7264 + +GET {{ShiftsLoggerAPI.Ledana_HostAddress}}/employee + +### diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Validator.cs b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Validator.cs new file mode 100644 index 00000000..e31f1498 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/Validator.cs @@ -0,0 +1,10 @@ +namespace ShiftsLoggerAPI.Ledana +{ + public static class Validator + { + public static bool ValidateEndTime(DateTime start, DateTime end) + { + return end > start; + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/appsettings.json b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/appsettings.json new file mode 100644 index 00000000..5e2597e0 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerAPI.Ledana/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "ShiftsLoggerDb": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=ShiftsLoggerDb;Trusted_Connection=True;" + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Clients/ShiftsLoggerApiClient.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Clients/ShiftsLoggerApiClient.cs new file mode 100644 index 00000000..3f84f4f3 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Clients/ShiftsLoggerApiClient.cs @@ -0,0 +1,373 @@ +using ShiftsLoggerAPI.Ledana.DTOs; +using ShiftsLoggerAPI.Ledana.Models; +using System.Globalization; +using System.Net.Http.Headers; +using System.Net.Http.Json; + +internal class ShiftsLoggerApiClient +{ + internal async Task IsEmployeeIdCorrect(int id) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var employees = await client.GetFromJsonAsync>("https://localhost:7264/employee"); + + if (employees is null) return false; + + return employees.Any(w => w.Id == id); + } + catch (Exception e) + { + Console.WriteLine("Getting employees didn't work " + e.Message); + return false; + } + } + + internal async Task?> GetAllEmployees() + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var employees = await client.GetFromJsonAsync>("https://localhost:7264/employee"); + + return employees; + } + catch (Exception e) + { + Console.WriteLine("Getting employees didn't work " + e.Message); + return null; + } + } + + internal async Task CreateShift(ShiftDto shift) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.PostAsJsonAsync($"https://localhost:7264/shift/", shift); + var result = await response.Content.ReadFromJsonAsync>(); + + if (response.IsSuccessStatusCode) + return "Shift created successfully!"; + else + return result?.ErrorMessage; + } + catch (Exception e) + { + return "Creating shift went wrong " + e.Message; + } + } + + internal async Task>?> GetAllShifts(int pageNumber, int pageSize) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + return await client.GetFromJsonAsync>>($"https://localhost:7264/shift?page_size={pageSize}&page_number={pageNumber}"); + + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } + + //when the user wants to view a shift, they can choose from all the shifts available + internal async Task IsShiftIdCorrectFromAllShifts(int id) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.GetFromJsonAsync>("https://localhost:7264/shift/all"); + if (response is null) return false; + + return response.Any(s => s.Id == id); + + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return false; + } + } + + //getting all shifts without pagination + internal async Task?> GetAllShifts() + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.GetFromJsonAsync>("https://localhost:7264/shift/all"); + + return response; + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } + + //check if shift is correct against shifts tht ended today + internal async Task IsShiftIdCorrectForToday(int id) + { + + var shifts = await GetShiftsPerDate(DateTime.Today); + + if (shifts is null) return false; + + return shifts.Any(w => w.Id == id); + } + + + internal async Task DeleteShift(int id) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.DeleteAsync($"https://localhost:7264/shift/{id}"); + var result = await response.Content.ReadFromJsonAsync>(); + + if (response.IsSuccessStatusCode) + return $"Shift with id {id} is deleted!"; + return result?.ErrorMessage; + + } + catch (Exception e) + { + return "Deleting shift went wrong " + e.Message; + } + } + + internal async Task UpdateWholeShift(int id, ShiftDto shift) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.PutAsJsonAsync($"https://localhost:7264/shift/{id}", shift); + var result = await response.Content.ReadFromJsonAsync>(); + + if (response.IsSuccessStatusCode) + return "Shift updated successfully!"; + else + return result?.ErrorMessage; + + } + catch (Exception e) + { + return "Updating shift went wrong " + e.Message; + } + } + + internal async Task UpdatePartialShift(int id, PartialShiftDto shift) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.PatchAsJsonAsync($"https://localhost:7264/shift/{id}", shift); + var result = await response.Content.ReadFromJsonAsync>(); + + if (response.IsSuccessStatusCode) + return "Shift updated successfully!"; + else + return result?.ErrorMessage; + + } + catch (Exception e) + { + return "Updating shift went wrong " + e.Message; + } + } + + internal async Task GetShiftById(int id) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.GetFromJsonAsync>($"https://localhost:7264/shift/{id}"); + + return response?.Data; + } + catch (Exception e) + { + Console.WriteLine("Updating shift went wrong " + e.Message); + return null; + } + } + + //this method gets the shifts that ended on the date provided into it + internal async Task?> GetShiftsPerDate(DateTime date) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + //convert date in the format the get is expecting + var formattedDate = date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + + var response = await client.GetFromJsonAsync>>($"https://localhost:7264/shift?date={formattedDate}&sort_by=date"); + + return response?.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } + + internal async Task?> GetShiftsPerEmployeeId(int id) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.GetFromJsonAsync>>($"https://localhost:7264/shift?employee_id={id}"); + + return response?.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } + + internal async Task?> GetShiftsPerDuration(string duration) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await client.GetFromJsonAsync>>($"https://localhost:7264/shift?duration={duration}"); + + return response?.Data; + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } + + internal async Task>?> GetShiftsSortedByDate(int pageNumber, int pageSize) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + return await client.GetFromJsonAsync>>($"https://localhost:7264/shift?sort_by=date&page_size={pageSize}&page_number={pageNumber}"); + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } + + internal async Task>?> GetShiftsSortedByDuration(int pageNumber, int pageSize) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + return await client.GetFromJsonAsync>>($"https://localhost:7264/shift?sort_by=duration&page_size={pageSize}&page_number={pageNumber}"); + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } + + internal async Task>?> GetShiftsSortedByEmployeeId(int pageNumber, int pageSize) + { + try + { + using HttpClient client = new(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + return await client.GetFromJsonAsync>>($"https://localhost:7264/shift?sort_by=employee_id&page_size={pageSize}&page_number={pageNumber}"); + } + catch (Exception e) + { + Console.WriteLine("Getting shifts went wrong " + e.Message); + return null; + } + } +} \ No newline at end of file diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Enums.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Enums.cs new file mode 100644 index 00000000..aea6d558 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Enums.cs @@ -0,0 +1,51 @@ + +namespace ShiftsLoggerUI.Ledana +{ + internal class Enums + { + internal enum MainMenuOptions + { + AddANewShift, + AddNewNightShift, + DeleteAShift, + UpdateAWholeShift, + UpdatePartOfAShift, + ViewingShiftsMenu, + Quit + } + + internal enum ViewingShiftsOptions + { + ViewAShift, + ViewAllShifts, + ViewTodaysShifts, + ViewShiftsPerFilter, + ViewShiftsSorted, + GoBack + } + + internal enum ViewShiftsPerFilter + { + ViewShiftsPerDate, + ViewShiftsPerEmployeeId, + ViewShiftsPerDuration, + GoBack + } + + internal enum ViewShiftsSorted + { + ViewShiftsSortedByDate, + ViewShiftsSortedByEmployeeId, + ViewShiftsSortedByDuration, + GoBack + } + + internal enum ShiftsPerDurationOptions + { + ViewShiftsOnThatDuration, + ViewShiftsBelowThatDuration, + ViewShiftsAboveThatDuration, + GoBack + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Helper.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Helper.cs new file mode 100644 index 00000000..ba41ace7 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Helper.cs @@ -0,0 +1,198 @@ +using ShiftsLoggerUI.Ledana.UI; + +namespace ShiftsLoggerUI.Ledana +{ + internal class Helper + { + internal static readonly ShiftsLoggerApiClient shiftsLoggerApiClient = new(); + + //getting employee id when creating a new shift + internal async Task GetEmployeeId() + { + Console.WriteLine("Please insert employee id '1 - 6'"); + + string? input = Console.ReadLine(); + int id; + + while (!int.TryParse(input, out id) || !await shiftsLoggerApiClient.IsEmployeeIdCorrect(id)) + { + Console.WriteLine("Please put a valid id or type 'x' to go back"); + input = Console.ReadLine(); + if (input is not null && input.ToLower() == "x") return 0; + } + + return id; + } + + //getting shift id when the user wants to view a shift + //they can choose from all the shifts in the db + internal async Task GetShiftIdFromAllShifts(string message) + { + Console.WriteLine(message); + + string? input = Console.ReadLine(); + int id; + + while (!int.TryParse(input, out id) || !await shiftsLoggerApiClient.IsShiftIdCorrectFromAllShifts(id)) + { + Console.WriteLine("Please put a valid id or type 'x' to go back"); + input = Console.ReadLine(); + if (input is not null && input.ToLower() == "x") return 0; + } + + return id; + } + //getting shift id when user wants to delete or update a shift + //they can choose only from shifts that end today + internal async Task GetShiftIdFromTodaysShifts(string message) + { + Console.WriteLine(message); + + string? input = Console.ReadLine(); + int id; + + while (!int.TryParse(input, out id) || !await shiftsLoggerApiClient.IsShiftIdCorrectForToday(id)) + { + Console.WriteLine("Please put a valid id or type 'x' to go back"); + input = Console.ReadLine(); + if (input is not null && input.ToLower() == "x") return 0; + } + + return id; + } + + //getting date input for get shifts per date in the right format + internal DateTime GetDate(string message) + { + Console.WriteLine(message); + string? timeString = Console.ReadLine(); + DateTime time; + + while (!Validator.ValidateDate(timeString, out time)) + { + Console.WriteLine("Date Time format is not correct. Try again or type 'x' to go back"); + timeString = Console.ReadLine(); + if (timeString is not null && timeString.ToLower() == "x") return DateTime.MinValue; + } + return time; + } + + //ask the user for a duration in format "hh:mm" to filter shifts in that duration + internal async Task GetDuration(string message) + { + Console.WriteLine(message); + string? time = Console.ReadLine(); + TimeSpan duration; + + while (!Validator.ValidateTimeSpan(time, out duration)) + { + Console.WriteLine("Duration format is not correct. Try again or type 'x' to go back"); + time = Console.ReadLine(); + if (time is not null && time.ToLower() == "x") return TimeSpan.Zero; + } + return duration; + } + + //checking for over lapping shift when creating one + internal async Task CheckForOverlappingShifts(int employeeId, DateTime startTime, DateTime endTime) + { + var shiftsPerDate = await shiftsLoggerApiClient.GetShiftsPerDate(DateTime.Today); + if (shiftsPerDate is null) return true; + + //check if employee already has a shift for the date + if (shiftsPerDate.Any(s => s.EmployeeId == employeeId)) + { + var existingShifts = shiftsPerDate.Where(s => s.EmployeeId == employeeId).ToList(); + if (existingShifts.Count == 2) + { + Console.WriteLine("Employee cannot have more then 2 shifts per day"); + TableVisualisation.ShowShifts(existingShifts); + return true; + } + var existingShift = existingShifts.First(s => s.EmployeeId == employeeId); + + if (Validator.ShiftOverLaps(startTime, endTime, existingShift)) + { + Console.WriteLine("The new shift cannot over lap the existing one"); + TableVisualisation.ShowShift(existingShift); + return true; + } + } + return false; + } + + //checking for over lapping shift when updating one + internal async Task CheckForOverlappingShifts(int employeeId, DateTime start, DateTime end, int shiftIdToUpdate) + { + var shiftsPerDate = await shiftsLoggerApiClient.GetShiftsPerDate(DateTime.Today); + if (shiftsPerDate is null) return true; + + //skip the one we are updating + var updatingShift = shiftsPerDate.First(s => s.Id == shiftIdToUpdate); + + if (shiftsPerDate.Remove(updatingShift)) + { + //check if employee already has a shift for the date + if (shiftsPerDate.Any(s => s.EmployeeId == employeeId)) + { + var existingShifts = shiftsPerDate.Where(s => s.EmployeeId == employeeId).ToList(); + if (existingShifts.Count == 2) + { + Console.WriteLine("Employee cannot have more then 2 shifts per day"); + TableVisualisation.ShowShifts(existingShifts); + return true; + } + var existingShift = existingShifts.First(s => s.EmployeeId == employeeId); + + if (Validator.ShiftOverLaps(start, end, existingShift)) + { + Console.WriteLine("The new shift cannot over lap the existing one"); + TableVisualisation.ShowShift(existingShift); + return true; + } + } + } + + return false; + } + + internal DateTime GetDateTime(string message) + { + Console.WriteLine(message); + string? timeString = Console.ReadLine(); + if (timeString is not null && timeString.ToLower() == "x") return DateTime.MinValue; + DateTime time; + + while (!Validator.ValidateDateTime(timeString, out time)) + { + Console.WriteLine("Date time format is not correct, try again or type 'x' to go back"); + timeString = Console.ReadLine(); + if (timeString is not null && timeString.ToLower() == "x") return DateTime.MinValue; + } + return time; + } + + internal bool ValidateDateInput(DateTime startTime, DateTime endTime) + { + //end date should be today + if (DateOnly.FromDateTime(endTime) != DateOnly.FromDateTime(DateTime.Today)) return false; + + //start day can be yesterday or today + if (DateOnly.FromDateTime(startTime) != DateOnly.FromDateTime(DateTime.Today) && DateOnly.FromDateTime(startTime) != DateOnly.FromDateTime(DateTime.Today.AddDays(-1))) return false; + + //end day can be yesterday only if end time is between 00:00 and 11:59 + //and start time is between 12:00 and 23:59 + if (TimeOnly.FromDateTime(startTime) >= new TimeOnly(12, 00, 00) + && TimeOnly.FromDateTime(startTime) <= new TimeOnly(23, 59, 59) + && TimeOnly.FromDateTime(endTime) >= new TimeOnly(00, 00, 00) + && TimeOnly.FromDateTime(endTime) <= new TimeOnly(11, 59, 59) + && DateOnly.FromDateTime(startTime) == DateOnly.FromDateTime(DateTime.Today.AddDays(-1)) + ) + { + return true; + } + + return true; + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Program.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Program.cs new file mode 100644 index 00000000..ccaee2b5 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Program.cs @@ -0,0 +1,5 @@ +using ShiftsLoggerUI.Ledana.UI; + +UserInterface userInterface = new(); + +await userInterface.MainMenu(); \ No newline at end of file diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Services/ShiftService.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Services/ShiftService.cs new file mode 100644 index 00000000..1f986d61 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Services/ShiftService.cs @@ -0,0 +1,455 @@ +using ShiftsLoggerAPI.Ledana.DTOs; +using ShiftsLoggerAPI.Ledana.Models; +using ShiftsLoggerUI.Ledana.UI; +using Spectre.Console; +using System.Globalization; + +namespace ShiftsLoggerUI.Ledana.Services +{ + internal class ShiftService + { + internal static readonly ShiftsLoggerApiClient shiftsLoggerApiClient = new(); + internal static readonly Helper helper = new(); + internal static Random random = new(); + + internal async Task ViewAllEmployees() + { + var employees = await shiftsLoggerApiClient.GetAllEmployees(); + + TableVisualisation.ShowEmployees(employees); + } + + //user can add a new shift with an employee id. start of shift will be now + //and end randomly chosen from a 6 hour to a 12 hour shift + internal async Task CreateNewShift() + { + int employeeId = await helper.GetEmployeeId(); + if (employeeId == 0) return; + + DateTime startTime = DateTime.Now; + Console.WriteLine($"Shift start at {startTime:HH:mm}"); + + Thread.Sleep(2000); + + DateTime endTime = DateTime.Now.AddHours(random.Next(4, 12)).AddMinutes(random.Next(1, 60)); + Console.WriteLine($"Shift end at {endTime:HH:mm}\n"); + + if (await helper.CheckForOverlappingShifts(employeeId, startTime, endTime)) + return; + + ShiftDto shift = new() + { + EmployeeId = employeeId, + StartTime = startTime, + EndTime = endTime + }; + + Console.WriteLine(); + Console.WriteLine(await shiftsLoggerApiClient.CreateShift(shift)); + Console.WriteLine(); + } + + //user can delete or update shifts only from shifts that ended today + internal async Task DeleteShift() + { + var shifts = await shiftsLoggerApiClient.GetShiftsPerDate(DateTime.Today); + if (shifts is not null && shifts.Count != 0) TableVisualisation.ShowShifts(shifts); + else + { + Console.WriteLine("No shifts for today yet"); + return; + } + + int id = await helper.GetShiftIdFromTodaysShifts("Please put the id of the shift you want to delete"); + if (id == 0) return; + + Console.WriteLine(await shiftsLoggerApiClient.DeleteShift(id)); + } + + //user can update a shift and if the shift goes on over night the start should be + //the day before and end today + internal async Task UpdateWholeShift() + { + Console.WriteLine("If you're trying to update into a night shift, please have in mind that the start time should be on the day before and end time today"); + + var shifts = await shiftsLoggerApiClient.GetShiftsPerDate(DateTime.Today); + if (shifts is not null && shifts.Count != 0) TableVisualisation.ShowShifts(shifts); + else + { + Console.WriteLine("No shifts for today yet"); + return; + } + + int shiftIdToUpdate = await helper.GetShiftIdFromTodaysShifts("Please put the id of the shift you want to update"); + if (shiftIdToUpdate == 0) return; + + bool isDateRight = false; + DateTime startTime = new(); + DateTime endTime = new(); + int employeeId = 0; + + while (!isDateRight) + { + startTime = helper.GetDateTime("Please put the new Start Time (yyyy-MM-ddTHH:mm:ss)"); + endTime = helper.GetDateTime("Please put the new End Time (yyyy-MM-ddTHH:mm:ss)"); + employeeId = await helper.GetEmployeeId(); + + isDateRight = helper.ValidateDateInput(startTime, endTime); + if (!isDateRight) Console.WriteLine("Incorrect input in dates. Try again or type 'x' to exit"); + } + Console.WriteLine($"Shift start at {startTime:HH:mm}"); + Console.WriteLine($"Shift end at {endTime:HH:mm}\n"); + + if (await helper.CheckForOverlappingShifts(employeeId, startTime, endTime, shiftIdToUpdate)) + return; + + if (endTime - startTime > new TimeSpan(12, 00, 00)) + { + Console.WriteLine("Shift can not be longer then 12 hours"); + return; + } + + ShiftDto shift = new() + { + EmployeeId = employeeId, + StartTime = startTime, + EndTime = endTime + }; + + Console.WriteLine("\n" + await shiftsLoggerApiClient.UpdateWholeShift(shiftIdToUpdate, shift) + "\n"); + } + + internal async Task UpdatePartialShift() + { + Console.WriteLine("If you're trying to update into a night shift, please have in mind that the start time should be on the day before and end time today"); + + var shifts = await shiftsLoggerApiClient.GetShiftsPerDate(DateTime.Today); + if (shifts is not null && shifts.Count != 0) TableVisualisation.ShowShifts(shifts); + else + { + Console.WriteLine("No shifts for today yet"); + return; + } + + int shiftIdToUpdate = await helper.GetShiftIdFromTodaysShifts("Please put the id of the shift you want to update"); + if (shiftIdToUpdate == 0) return; + + var existingShift = await shiftsLoggerApiClient.GetShiftById(shiftIdToUpdate); + + if (existingShift is null) + { + Console.WriteLine("Couldn't find shift"); + return; + } + //keeping existing values to validate over lapping + DateTime existingStart = existingShift.StartTime; + DateTime existingEnd = existingShift.EndTime; + int existingEmployeeId = existingShift.EmployeeId; + + //passing null if the prop is not changed + DateTime? startTime = null; + DateTime? endTime = null; + int? employeeId = null; + + bool isDateRight = false; + bool shiftIsChanging = false; + + while (!isDateRight) + { + if (AnsiConsole.Confirm("Do you want to update start time?")) + { + startTime = helper.GetDateTime("Please put the new Start Time (yyyy-MM-ddTHH:mm:ss)"); + if (startTime == DateTime.MinValue) return; + existingStart = (DateTime)startTime; + shiftIsChanging = true; + } + + if (AnsiConsole.Confirm("Do you want to update end time?")) + { + endTime = helper.GetDateTime("Please put the new End Time (yyyy-MM-ddTHH:mm:ss)"); + if (endTime == DateTime.MinValue) return; + existingEnd = (DateTime)endTime; + shiftIsChanging = true; + } + + if (AnsiConsole.Confirm("Do you want to update employee id?")) + { + employeeId = await helper.GetEmployeeId(); + if (employeeId == 0) return; + existingEmployeeId = (int)employeeId; + shiftIsChanging = true; + } + + isDateRight = helper.ValidateDateInput(existingStart, existingEnd); + if (!isDateRight) Console.WriteLine("Incorrect input in dates. Try again or type 'x' to exit"); + } + + Console.WriteLine($"Shift start at {existingStart:HH:mm}"); + Console.WriteLine($"Shift end at {existingEnd:HH:mm}\n"); + + if (await helper.CheckForOverlappingShifts(existingEmployeeId, existingStart, existingEnd, shiftIdToUpdate)) + return; + + + if (existingEnd - existingStart > new TimeSpan(12, 00, 00)) + { + Console.WriteLine("Shift can not be longer then 12 hours"); + return; + } + + if (shiftIsChanging) + { + PartialShiftDto shift = new() + { + EmployeeId = employeeId, + StartTime = startTime, + EndTime = endTime + }; + + Console.WriteLine(); + Console.WriteLine(await shiftsLoggerApiClient.UpdatePartialShift(shiftIdToUpdate, shift)); + Console.WriteLine(); + } + else + Console.WriteLine("Shift stayed the same!"); + } + + internal async Task ViewShift() + { + await GetAllShifts(); + int id = await helper.GetShiftIdFromAllShifts("Please put the id of the shift you want to see"); + if (id == 0) return; + + var shift = await shiftsLoggerApiClient.GetShiftById(id); + + if (shift is null) + { + Console.WriteLine("Not found"); + return; + } + + TableVisualisation.ShowShift(shift); + } + internal async Task GetAllShifts() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await shiftsLoggerApiClient.GetAllShifts(pageNumber, pageSize); + + ViewAllShifts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + } + } + + private void ViewAllShifts(ref ApiResponseDto>? response, ref int pageNumber, ref int pageSize, ref bool keepRunning) + { + if (response is null) { Console.WriteLine("No shifts found!"); return; } + + Console.Clear(); + TableVisualisation.ShowShifts(response.Data); + Console.WriteLine($"Total count: {response.TotalCount}"); + Console.WriteLine($"Current page: {response.CurrentPage}"); + Console.WriteLine($"Page size: {response.PageSize}"); + Console.WriteLine($"Has next: {response.HasNext}"); + Console.WriteLine($"Has previous: {response.HasPrevious}"); + + var choice = AnsiConsole.Prompt(new SelectionPrompt() + .AddChoices("Next", "Previous", "First", "Last", "Go Back")); + + switch (choice) + { + case "Next": + if (!response.HasNext) return; + pageNumber++; + break; + case "Previous": + if (!response.HasPrevious) return; + if (pageNumber > 1) pageNumber--; + break; + case "First": + pageNumber = 1; + break; + case "Last": + pageNumber = response.TotalCount % pageSize == 0 ? response.TotalCount / pageSize : response.TotalCount / pageSize + 1; + break; + case "Go Back": + keepRunning = false; + break; + } + } + + internal async Task ViewShiftsPerDate() + { + var date = helper.GetDate("Please put the date in format: \"yyyy-mm-dd\""); + if (date == DateTime.MinValue) return; + + var shifts = await shiftsLoggerApiClient.GetShiftsPerDate(date); + if (shifts is null || shifts.Count == 0) Console.WriteLine("No shifts found!"); + + else + TableVisualisation.ShowShifts(shifts); + + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + Console.Clear(); + } + + internal async Task ViewShiftsPerEmployeeId() + { + var id = await helper.GetEmployeeId(); + if (id == 0) return; + + var shifts = await shiftsLoggerApiClient.GetShiftsPerEmployeeId(id); + + if (shifts is null || shifts.Count == 0) Console.WriteLine("No shifts found!"); + + else + TableVisualisation.ShowShifts(shifts); + + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + Console.Clear(); + } + + internal async Task ViewShiftsPerDuration() + { + TimeSpan duration = await helper.GetDuration("Please put the duration in format: \"HH:mm\""); + if (duration == TimeSpan.Zero) return; + + var shifts = await shiftsLoggerApiClient.GetShiftsPerDuration(duration.ToString(@"hh\:mm")); + if (shifts is null || shifts.Count == 0) Console.WriteLine("No shifts found!"); + + else + TableVisualisation.ShowShifts(shifts); + + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + Console.Clear(); + } + + internal async Task ViewShiftsSortedByDate() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await shiftsLoggerApiClient.GetShiftsSortedByDate(pageNumber, pageSize); + + ViewAllShifts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + + } + } + + internal async Task ViewShiftsSortedByDuration() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await shiftsLoggerApiClient.GetShiftsSortedByDuration(pageNumber, pageSize); + + ViewAllShifts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + + } + } + + internal async Task ViewShiftsSortedByEmployeeId() + { + int pageNumber = 1; + int pageSize = 5; + bool keepRunning = true; + + while (keepRunning) + { + var response = await shiftsLoggerApiClient.GetShiftsSortedByEmployeeId(pageNumber, pageSize); + + ViewAllShifts(ref response, ref pageNumber, ref pageSize, ref keepRunning); + + } + } + + internal async Task GetTodaysShifts() + { + var shifts = await shiftsLoggerApiClient.GetShiftsPerDate(DateTime.Today); + if (shifts is null || shifts.Count == 0) Console.WriteLine("No shifts found!"); + + else + TableVisualisation.ShowShifts(shifts); + + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + Console.Clear(); + } + + internal async Task ViewShiftsBelowDuration() + { + TimeSpan duration = await helper.GetDuration("Please put the duration in format: \"HH:mm\""); + if (duration == TimeSpan.Zero) return; + + var shifts = await shiftsLoggerApiClient.GetAllShifts(); + if (shifts is null || shifts.Count == 0) + { + Console.WriteLine("No shifts found!"); + return; + } + + var shiftsBelowDuration = + shifts + .Where(s => + StringToTimespan(s.Duration) + < + duration).ToList(); + + + TableVisualisation.ShowShifts(shiftsBelowDuration); + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + Console.Clear(); + } + + internal TimeSpan StringToTimespan(string timeSpan) + { + if (TimeSpan.TryParseExact(timeSpan, + @"hh\:mm", + CultureInfo.InvariantCulture, + TimeSpanStyles.None, + out TimeSpan time)) + return time; + + return new TimeSpan(0, 0, 0); + } + + internal async Task ViewShiftsAboveDuration() + { + TimeSpan duration = await helper.GetDuration("Please put the duration in format: \"HH:mm\""); + if (duration == TimeSpan.Zero) return; + + var shifts = await shiftsLoggerApiClient.GetAllShifts(); + if (shifts is null || shifts.Count == 0) + { + Console.WriteLine("No shifts found!"); + return; + } + + var shiftsAboveDuration = + shifts + .Where(s => + StringToTimespan(s.Duration) + > + duration).ToList(); + + + TableVisualisation.ShowShifts(shiftsAboveDuration); + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + Console.Clear(); + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/ShiftsLoggerUI.Ledana.csproj b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/ShiftsLoggerUI.Ledana.csproj new file mode 100644 index 00000000..f19c837e --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/ShiftsLoggerUI.Ledana.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/UI/TableVisualisation.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/UI/TableVisualisation.cs new file mode 100644 index 00000000..d35299ad --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/UI/TableVisualisation.cs @@ -0,0 +1,80 @@ +using ShiftsLoggerAPI.Ledana.Models; +using Spectre.Console; + +namespace ShiftsLoggerUI.Ledana.UI +{ + internal class TableVisualisation + { + internal static void ShowShifts(List? shifts) + { + if(shifts is null) + { + Console.WriteLine("Couldn't fetch shifts"); + return; + } + + var table = new Table(); + table.ShowRowSeparators(); + + table.AddColumn("Shift Id"); + table.AddColumn("Start Time"); + table.AddColumn("End Time"); + table.AddColumn("Duration"); + table.AddColumn("Employee Id"); + table.AddColumn("First Name"); + table.AddColumn("Last Name"); + + foreach (var item in shifts) + { + table.AddRow(item.Id.ToString(), + item.StartTime.ToString(), + item.EndTime.ToString(), + item.Duration.ToString(), + item.EmployeeId.ToString(), + item.Employee.FirstName, + item.Employee.LastName); + } + AnsiConsole.Write(table); + } + + internal static void ShowShift(Shift shift) + { + var panel = new Panel($@"Id: {shift.Id} +Start time: {shift.StartTime} +End time: {shift.EndTime} +Duration: {shift.Duration} +Employee Id: {shift.EmployeeId} +First Name: {shift.Employee.FirstName} +Last Name: {shift.Employee.LastName}") + { + Header = new PanelHeader("Shift's info"), + Padding = new Padding(2, 2, 2, 2) + }; + + AnsiConsole.Write(panel); + Console.WriteLine("Press any key to continue"); + Console.ReadKey(); + Console.Clear(); + } + + internal static void ShowEmployees(List? employees) + { + + if (employees is null) + { + Console.WriteLine("Employees could not be loaded"); + return; + } + var table = new Table(); + table.AddColumn("Id"); + table.AddColumn("First Name"); + table.AddColumn("Last Name"); + + foreach (var item in employees) + { + table.AddRow(item.Id.ToString(), item.FirstName, item.LastName); + } + AnsiConsole.Write(table); + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/UI/UserInterface.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/UI/UserInterface.cs new file mode 100644 index 00000000..ff25d716 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/UI/UserInterface.cs @@ -0,0 +1,201 @@ +using ShiftsLoggerUI.Ledana.Services; +using Spectre.Console; +using static ShiftsLoggerUI.Ledana.Enums; + +namespace ShiftsLoggerUI.Ledana.UI +{ + internal class UserInterface + { + static ShiftService shiftService = new(); + internal async Task MainMenu() + { + bool flag = true; + Console.WriteLine("Welcome to our app"); + await shiftService.ViewAllEmployees(); + while (flag) + { + Console.WriteLine("Press any key to view the main menu"); + Console.ReadKey(); + Console.Clear(); + + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices( + MainMenuOptions.AddANewShift, + MainMenuOptions.DeleteAShift, + MainMenuOptions.UpdateAWholeShift, + MainMenuOptions.UpdatePartOfAShift, + MainMenuOptions.ViewingShiftsMenu, + MainMenuOptions.Quit + )); + + switch (option) + { + case MainMenuOptions.AddANewShift: + await shiftService.CreateNewShift(); + break; + case MainMenuOptions.DeleteAShift: + await shiftService.DeleteShift(); + break; + case MainMenuOptions.UpdateAWholeShift: + await shiftService.UpdateWholeShift(); + break; + case MainMenuOptions.UpdatePartOfAShift: + await shiftService.UpdatePartialShift(); + break; + case MainMenuOptions.ViewingShiftsMenu: + await ViewingShiftsMenu(); + break; + + case MainMenuOptions.Quit: + Console.WriteLine("Good bye!"); + Console.ReadKey(); + flag = false; + break; + } + } + } + + private async Task ViewingShiftsMenu() + { + bool flag = true; + + while (flag) + { + Console.Clear(); + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices(ViewingShiftsOptions.ViewAShift, + ViewingShiftsOptions.ViewAllShifts, + ViewingShiftsOptions.ViewTodaysShifts, + ViewingShiftsOptions.ViewShiftsPerFilter, + ViewingShiftsOptions.ViewShiftsSorted, + ViewingShiftsOptions.GoBack + )); + switch (option) + { + case ViewingShiftsOptions.ViewAShift: + await shiftService.ViewShift(); + break; + case ViewingShiftsOptions.ViewAllShifts: + await shiftService.GetAllShifts(); + break; + case ViewingShiftsOptions.ViewTodaysShifts: + await shiftService.GetTodaysShifts(); + break; + case ViewingShiftsOptions.ViewShiftsPerFilter: + await ShiftsPerFilterMenu(); + break; + case ViewingShiftsOptions.ViewShiftsSorted: + await ShiftsSortedMenu(); + break; + case ViewingShiftsOptions.GoBack: + flag = false; + break; + } + } + } + + + private async Task ShiftsSortedMenu() + { + bool flag = true; + + while (flag) + { + Console.Clear(); + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices(ViewShiftsSorted.ViewShiftsSortedByDate, + ViewShiftsSorted.ViewShiftsSortedByDuration, + ViewShiftsSorted.ViewShiftsSortedByEmployeeId, + ViewShiftsSorted.GoBack + )); + switch (option) + { + case ViewShiftsSorted.ViewShiftsSortedByDate: + await shiftService.ViewShiftsSortedByDate(); + break; + case ViewShiftsSorted.ViewShiftsSortedByDuration: + await shiftService.ViewShiftsSortedByDuration(); + break; + case ViewShiftsSorted.ViewShiftsSortedByEmployeeId: + await shiftService.ViewShiftsSortedByEmployeeId(); + break; + case ViewShiftsSorted.GoBack: + flag = false; + break; + } + } + } + + private async Task ShiftsPerFilterMenu() + { + bool flag = true; + + while (flag) + { + Console.Clear(); + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices(ViewShiftsPerFilter.ViewShiftsPerDate, + ViewShiftsPerFilter.ViewShiftsPerDuration, + ViewShiftsPerFilter.ViewShiftsPerEmployeeId, + ViewShiftsPerFilter.GoBack + )); + switch (option) + { + case ViewShiftsPerFilter.ViewShiftsPerDate: + await shiftService.ViewShiftsPerDate(); + break; + case ViewShiftsPerFilter.ViewShiftsPerEmployeeId: + await shiftService.ViewShiftsPerEmployeeId(); + break; + case ViewShiftsPerFilter.ViewShiftsPerDuration: + await ShiftsPerDurationMenu(); + break; + case ViewShiftsPerFilter.GoBack: + flag = false; + break; + } + } + } + + private async Task ShiftsPerDurationMenu() + { + bool flag = true; + + while (flag) + { + Console.Clear(); + var option = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What do you want to do?") + .AddChoices(ShiftsPerDurationOptions.ViewShiftsOnThatDuration, + ShiftsPerDurationOptions.ViewShiftsBelowThatDuration, + ShiftsPerDurationOptions.ViewShiftsAboveThatDuration, + ShiftsPerDurationOptions.GoBack + )); + switch (option) + { + case ShiftsPerDurationOptions.ViewShiftsOnThatDuration: + await shiftService.ViewShiftsPerDuration(); + break; + case ShiftsPerDurationOptions.ViewShiftsBelowThatDuration: + await shiftService.ViewShiftsBelowDuration(); + break; + case ShiftsPerDurationOptions.ViewShiftsAboveThatDuration: + await shiftService.ViewShiftsAboveDuration(); + break; + case ShiftsPerDurationOptions.GoBack: + flag = false; + break; + } + } + } + } +} diff --git a/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Validator.cs b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Validator.cs new file mode 100644 index 00000000..558edef7 --- /dev/null +++ b/ShiftsLogger.Ledana/ShiftsLoggerUI.Ledana/Validator.cs @@ -0,0 +1,45 @@ +using ShiftsLoggerAPI.Ledana.Models; +using System.Globalization; + +namespace ShiftsLoggerUI.Ledana +{ + internal static class Validator + { + internal static bool ValidateDate(string? date, out DateTime start) + { + return DateTime.TryParseExact(date, + "yyyy-MM-dd", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out start); + } + + internal static bool ValidateTimeSpan(string? time, out TimeSpan end) + { + return TimeSpan.TryParseExact(time, + @"hh\:mm", + CultureInfo.InvariantCulture, + TimeSpanStyles.None, + out end); + } + + internal static bool ShiftOverLaps(DateTime start, DateTime end, Shift existingShift) + { + return (end >= existingShift.StartTime && end <= existingShift.EndTime + || + start >= existingShift.StartTime && start <= existingShift.EndTime + || + start <= existingShift.StartTime && end >= existingShift.EndTime + ); + } + + internal static bool ValidateDateTime(string? timeString, out DateTime time) + { + return DateTime.TryParseExact(timeString, + "yyyy-MM-ddTHH:mm:ss", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out time); + } + } +}