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);
+ }
+ }
+}