Reupload
This commit is contained in:
2025-07-08 10:32:34 +02:00
commit a5a8c3410d
59 changed files with 4451 additions and 0 deletions

567
.gitignore vendored Normal file
View File

@@ -0,0 +1,567 @@
## RedirectedFrom: https://github.com/sch-seiryu/SharpenTheCSharp/blob/main/.gitignore
##
## From 'Jetbrains.gitignore'
## URL: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
## # Related issue: https://rider-support.jetbrains.com/hc/en-us/articles/207097529-What-is-the-idea-folder-
## Note that following three files are excluded: 'indexLayout.xml', 'vcs.xml', and 'runConfigurations'
## Pasted the snippet below:
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
## [gitignore covers JetBrains IDEs above]
##
## [dotnet generated gitignore below]
## 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/main/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/
[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
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# 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
*.tlog
*.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 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# 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/
# Visual Studio History (VSHistory) files
.vshistory/
# 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
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
/.idea/

16
LeanderShiftPlannerV2.sln Normal file
View File

@@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LeanderShiftPlannerV2", "LeanderShiftPlannerV2\LeanderShiftPlannerV2.csproj", "{0B74CD01-D9F1-4A9B-BDF4-903BBE3768E1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0B74CD01-D9F1-4A9B-BDF4-903BBE3768E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B74CD01-D9F1-4A9B-BDF4-903BBE3768E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B74CD01-D9F1-4A9B-BDF4-903BBE3768E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B74CD01-D9F1-4A9B-BDF4-903BBE3768E1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="LeanderShiftPlannerV2.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@@ -0,0 +1,27 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using LeanderShiftPlannerV2.Service;
using LeanderShiftPlannerV2.View.LeanderShiftPlannerView;
namespace LeanderShiftPlannerV2;
public class App : Application
{
public AppService AppService { get; } = new AppService();
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new LeanderShiftPlannerMainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

View File

@@ -0,0 +1,82 @@
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
using LeanderShiftPlannerV2.Util;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace LeanderShiftPlannerV2.FileIO
{
public static class LoadDataFromFile
{
private static void CheckFileSystem()
{
if (!Directory.Exists(Constants.DataPath))
Directory.CreateDirectory(Constants.DataPath);
}
public static List<Person> LoadPersons()
{
CheckFileSystem();
if (!File.Exists(Constants.PersonsPath))
{
return new List<Person>();
}
XmlSerializer deserializer = new XmlSerializer(typeof(List<Person>));
using (TextReader xmlReader = new StreamReader(Constants.PersonsPath))
{
return (List<Person>)deserializer.Deserialize(xmlReader)!;
}
}
public static List<Shift> LoadShifts()
{
CheckFileSystem();
if (!File.Exists(Constants.ShiftsPath))
{
return new List<Shift>();
}
XmlSerializer deserializer = new XmlSerializer(typeof(List<Shift>));
using (TextReader xmlReader = new StreamReader(Constants.ShiftsPath))
{
return (List<Shift>)deserializer.Deserialize(xmlReader)!;
}
}
public static void LoadPersonShiftConnections(List<Person> persons, List<Shift> shifts, AppService appService)
{
CheckFileSystem();
if (!File.Exists(Constants.ConnectionsPath))
{
return;
}
List<PersonShiftConnection> connections;
XmlSerializer deserializer = new XmlSerializer(typeof(List<PersonShiftConnection>));
using (TextReader xmlReader = new StreamReader(Constants.ConnectionsPath))
{
connections = (List<PersonShiftConnection>)deserializer.Deserialize(xmlReader)!;
}
foreach (PersonShiftConnection connection in connections)
{
foreach (Person person in persons)
{
if (!connection.FirstName.Equals(person.FirstName)) continue;
foreach (Shift shift in shifts)
{
if (connection.Shifts.Contains(shift.ToString()))
{
appService.PersonService.AddShiftToPerson(person, shift);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,94 @@
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
namespace LeanderShiftPlannerV2.FileIO
{
public static class SaveDataToFile
{
[Serializable]
public class DictionaryEntry
{
public required string Key { get; set; }
public required List<Person> Value { get; set; }
}
[Serializable]
public class SerializableDictionary
{
public List<DictionaryEntry> Entries { get; set; }
public SerializableDictionary()
{
Entries = new List<DictionaryEntry>();
}
}
private static void CheckFileSystem()
{
if (!Directory.Exists(Constants.DataPath))
Directory.CreateDirectory(Constants.DataPath);
if (!Directory.Exists(Constants.SavedVariablesPath))
Directory.CreateDirectory(Constants.SavedVariablesPath);
}
public static void SavePersons(List<Person> persons)
{
if (persons.Count == 0) return;
CheckFileSystem();
XmlSerializer serializer = new XmlSerializer(typeof(List<Person>));
using (TextWriter xmlWriter = new StreamWriter(Constants.PersonsPath))
{
serializer.Serialize(xmlWriter, persons);
}
}
public static void SaveShifts(List<Shift> shifts)
{
if (shifts.Count == 0) return;
CheckFileSystem();
XmlSerializer serializer = new XmlSerializer(typeof(List<Shift>));
using (TextWriter xmlWriter = new StreamWriter(Constants.ShiftsPath))
{
serializer.Serialize(xmlWriter, shifts);
}
}
public static void SavePersonShiftConnections(List<Person> persons)
{
CheckFileSystem();
List<PersonShiftConnection> connections = new List<PersonShiftConnection>();
foreach (Person person in persons)
{
if (person.Shifts.Count == 0) continue;
PersonShiftConnection connection = new PersonShiftConnection();
connection.FirstName = person.FirstName;
foreach (Shift shift in person.Shifts)
{
connection.Shifts.Add(shift.ToString());
}
connections.Add(connection);
}
if (connections.Count == 0) return;
XmlSerializer serializer = new XmlSerializer(typeof(List<PersonShiftConnection>));
using (TextWriter xmlWriter = new StreamWriter(Constants.ConnectionsPath))
{
serializer.Serialize(xmlWriter, connections);
}
}
}
}

View File

@@ -0,0 +1,176 @@
using System.Collections.Generic;
using LeanderShiftPlannerV2.Model;
using SkiaSharp;
using System.IO;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using LeanderShiftPlannerV2.Util;
namespace LeanderShiftPlannerV2.FileIO
{
public static class ShiftPlanIO
{
private static void CheckFileSystem()
{
if (!Directory.Exists(Constants.DataPath)) Directory.CreateDirectory(Constants.DataPath);
if (!Directory.Exists(Constants.ExportPath)) Directory.CreateDirectory(Constants.ExportPath);
if (!Directory.Exists(Constants.ShiftPlanPath)) Directory.CreateDirectory(Constants.ShiftPlanPath);
}
public static SKImage GenerateShiftPlanPng(ShiftPlan shiftPlan)
{
Dictionary<(int, int), List<string>> names = GetNamesDict(shiftPlan);
const int rows = 4;
const int columns = 6;
const int cellWidth = 150;
const int cellHeight = 200;
using (SKBitmap bitmap = new SKBitmap(1000, 850))
using (SKCanvas canvas = new SKCanvas(bitmap))
{
SKFont smallTextFont = new SKFont(SKTypeface.FromFile(Constants.FontPath)) { Size = 24 };
SKPaint rectColorGray = new SKPaint
{ Color = SKColors.LightGray, Style = SKPaintStyle.Stroke, StrokeWidth = 5 };
SKPaint textColorGray = new SKPaint { Color = SKColors.Gray };
SKPaint textColorBlack = new SKPaint { Color = SKColors.Black };
// Fill Background
canvas.Clear(SKColors.White);
// Draw Topic
using (SKFont topic = new SKFont(SKTypeface.FromFile(Constants.FontPath)))
{
topic.Size = 40;
string text1 = "Schichtplan ";
string text2 = "IT-Service";
float text1Width = topic.MeasureText(text1);
float textX = 50;
float textY = 100;
canvas.DrawText(text1, textX, textY, topic, textColorBlack);
canvas.DrawText(text2, textX + text1Width, textY, topic, textColorGray);
}
// Draw Headers
string[] headers = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"];
for (int j = 1; j < columns; j++)
{
string text = headers[j - 1];
float textWidth = smallTextFont.MeasureText(text);
float textX = j * cellWidth + (cellWidth - textWidth) / 2;
float textY = (cellHeight - smallTextFont.Size);
canvas.DrawText(text, textX, textY, smallTextFont, textColorBlack);
}
// Draw Start-times
string[] time = [" ", "Ab 9 Uhr", "Ab 14 Uhr", "Ab 16 Uhr"];
for (int i = 1; i < rows; i++)
{
string text = time[i];
float textWidth = smallTextFont.MeasureText(text);
float textX = (cellWidth - textWidth) / 2;
float textY = i * cellHeight + (cellHeight + smallTextFont.Size) / 2;
canvas.DrawText(text, textX, textY, smallTextFont, textColorBlack);
}
// Draw ShiftPlan
for (int i = 1; i < rows; i++)
{
for (int j = 1; j < columns; j++)
{
int x = j * cellWidth;
int y = i * cellHeight;
// Draw Rectangles
canvas.DrawRect(new SKRect(x, y, x + cellWidth, y + cellHeight), rectColorGray);
// Draw Text
int k = 0;
foreach (string name in names[(i, j)])
{
k++;
string text = name;
float textWidth = smallTextFont.MeasureText(text);
float textX = x + (cellWidth - textWidth) / 2;
float textY = y + 100 - (names[(i, j)].Count * smallTextFont.Size / 2) +
k * smallTextFont.Size;
//float textY = y + k * textColor.TextSize;
canvas.DrawText(text, textX, textY, smallTextFont, textColorBlack);
}
}
}
SKImage image = SKImage.FromBitmap(bitmap);
return image;
}
}
public static void ExportShiftPlan(SKImage image, int id)
{
CheckFileSystem();
using SKData data = image.Encode(SKEncodedImageFormat.Png, 100);
using FileStream dataStream = File.OpenWrite(Constants.ShiftPlanPath + $"ShiftPlan_{id}.png");
data.SaveTo(dataStream);
}
public static IImage ConvertToIImage(SKImage image)
{
SKBitmap skBitmap = SKBitmap.FromImage(image);
// Convert SKBitmap to a byte array
MemoryStream ms = new MemoryStream();
skBitmap.Encode(ms, SKEncodedImageFormat.Png, 100);
ms.Seek(0, SeekOrigin.Begin);
// Create a Bitmap from the byte array
Bitmap bitmap = new Bitmap(ms);
// Convert Bitmap to IImage
return bitmap;
}
private static Dictionary<(int, int), List<string>> GetNamesDict(ShiftPlan shiftPlan)
{
Dictionary<(int, int), List<string>> result = new Dictionary<(int, int), List<string>>();
int i = 0, j = 1;
foreach ((Shift, Shift, Shift) shift in shiftPlan.Shifts)
{
i++;
result[(j, i)] = new List<string>();
if (shift.Item1 is Shift)
{
foreach (Person person in shift.Item1.Persons)
{
result[(j, i)].Add(person.FirstName);
}
}
j++;
result[(j, i)] = new List<string>();
if (shift.Item2 is Shift)
{
foreach (Person person in shift.Item2.Persons)
{
result[(j, i)].Add(person.FirstName);
}
}
j++;
result[(j, i)] = new List<string>();
if (shift.Item3 is Shift)
{
foreach (Person person in shift.Item3.Persons)
{
result[(j, i)].Add(person.FirstName);
}
}
j = 1;
}
return result;
}
}
}

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Util;
using OfficeOpenXml;
namespace LeanderShiftPlannerV2.FileIO;
public static class TimesheetIO
{
private static int CheckFileSystem(List<Person> persons)
{
if (!Directory.Exists(Constants.DataPath))
Directory.CreateDirectory(Constants.DataPath);
if (!Directory.Exists(Constants.ExportPath))
Directory.CreateDirectory(Constants.ExportPath);
if (!Directory.Exists(Constants.TimesheetPath))
Directory.CreateDirectory(Constants.TimesheetPath);
foreach (Person person in persons)
if (!Directory.Exists(Constants.TimesheetPath + $"{person.FirstName}"))
Directory.CreateDirectory(Constants.TimesheetPath + $"{person.FirstName}");
foreach (string file in Directory.GetFiles(Constants.TimesheetPath))
{
int error = CheckFileLock(file);
if (error != 0) return error;
}
return 0;
}
private static int CheckFileLock(string filePath)
{
try
{
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
return 0;
}
catch (IOException)
{
return 1;
}
catch (Exception ex)
{
return 2;
}
}
public static int ExportTimesheets(List<Person> persons)
{
int error = CheckFileSystem(persons);
if (error != 0) return error;
string year = DateTime.Today.Year.ToString();
foreach (Person person in persons)
{
// Pfad zur Ausgabedatei
string filePath = Constants.TimesheetPath + $"{person.FirstName}";
// Erstellen eines neuen Pakets und einer neuen Arbeitsmappe
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
foreach (Timesheet timesheet in person.Timesheets)
{
using (ExcelPackage package = new ExcelPackage(new FileInfo(Constants.TimesheetResource)))
{
ExcelWorksheet? worksheet = package.Workbook.Worksheets[0];
int monthInt = Constants.Months.IndexOf(timesheet.Month);
int dayInt = DateTime.DaysInMonth(int.Parse(year), monthInt);
string month = Constants.MonthsGerman[monthInt];
worksheet.Cells[4, 4].Value = person.Surname;
worksheet.Cells[4, 5].Value = person.FirstName;
worksheet.Cells[10, 4].Value = month;
worksheet.Cells[10, 6].Value = year;
string dayIntStr = (dayInt < 10 ? $"0{dayInt}" : dayInt.ToString());
string monthIntStr = (monthInt < 10 ? $"0{monthInt}" : monthInt.ToString());
worksheet.Cells[52, 2].Value = $"{dayIntStr}.{monthIntStr}.{year}";
foreach ((DateTime, DateTime, bool) tuple in timesheet.Value)
{
worksheet.Cells[17 + tuple.Item1.Day, 2].Value = tuple.Item1.Hour + (tuple.Item1.Minute == 0 ? ":00" : tuple.Item1.Minute.ToString());
worksheet.Cells[17 + tuple.Item1.Day, 3].Value = tuple.Item2.Hour + (tuple.Item2.Minute == 0 ? ":00" : tuple.Item1.Minute.ToString());
if (tuple.Item3) worksheet.Cells[17 + tuple.Item1.Day, 5].Value = "1:00";
}
for (int i = 17; i <= 47; i++)
{
worksheet.Cells[i, 4].Formula = $"=SUM(C{i}-B{i})";
worksheet.Cells[i, 6].Formula = $"=SUM(D{i}-E{i})";
}
worksheet.Cells[48, 6].Formula = "SUM(F17:F47)";
package.SaveAs(new FileInfo(@$"{filePath}/{month}.xlsx"));
}
}
}
return 0;
}
}

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Resources/icon/ShiftPlannerIcon.ico</ApplicationIcon>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<PackageIcon>Resources\icon\ShiftPlannerIcon.png</PackageIcon>
<AssemblyVersion>1.0.4</AssemblyVersion>
<FileVersion>1.0.4</FileVersion>
<NeutralLanguage>en</NeutralLanguage>
<Version>1.0.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.1"/>
<PackageReference Include="Avalonia.Desktop" Version="11.2.1"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1"/>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="EPPlus" Version="7.5.3" />
<PackageReference Include="SkiaSharp" Version="3.118.0-preview.2.3" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="3.118.0-preview.2.3" />
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="3.118.0-preview.2.3" />
<PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="3.118.0-preview.2.3" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\font\Emblem.ttf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="Resources\templates\timesheet.xlsx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\icon\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace LeanderShiftPlannerV2.Model;
public class Filter
{
public Filter(string filterString, List<ShiftPlan> affectedShiftPlans)
{
FilterString = filterString;
AffectedShiftPlans = affectedShiftPlans;
}
public List<ShiftPlan> AffectedShiftPlans { get; set; }
public string FilterString { get; set; }
}

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Xml.Serialization;
namespace LeanderShiftPlannerV2.Model
{
[Serializable]
public class Person : INotifyPropertyChanged
{
private string _firstFirstName = null!;
private string _surname = null!;
private int _hours;
private ObservableCollection<Shift> _shifts;
[XmlIgnore]
private List<Timesheet> _timesheets;
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Person()
{
_shifts = new ObservableCollection<Shift>();
_timesheets = new List<Timesheet>();
}
public Person(string firstFirstName, string surname, int hours)
{
this._firstFirstName = firstFirstName;
this._surname = surname;
this._hours = hours;
_shifts = new ObservableCollection<Shift>();
_timesheets = new List<Timesheet>();
}
public string FirstName
{
get => _firstFirstName;
set
{
_firstFirstName = value;
OnPropertyChanged(nameof(FirstName));
}
}
public string Surname
{
get => _surname;
set
{
_surname = value;
OnPropertyChanged(nameof(Surname));
}
}
public int Hours
{
get => _hours;
set
{
_hours = value;
OnPropertyChanged(nameof(Hours));
}
}
[XmlIgnore]
public string ShiftsString
{
get
{
string result = "";
foreach (Shift shift in _shifts)
result += shift.Day + " " + shift.ShiftInDay + "; ";
return result;
}
}
[XmlIgnore]
public List<Timesheet> Timesheets
{
get => _timesheets;
set => _timesheets = value;
}
[XmlIgnore] public ObservableCollection<Shift> Shifts => _shifts;
public void AddShift(Shift shift)
{
_shifts.Add(shift);
OnPropertyChanged(nameof(ShiftsString));
}
public void RemoveShift(Shift shift)
{
_shifts.Remove(shift);
OnPropertyChanged(nameof(ShiftsString));
}
public void ClearShifts()
{
_shifts.Clear();
OnPropertyChanged(nameof(ShiftsString));
}
override
public string ToString()
{
return _firstFirstName;
}
}
}

View File

@@ -0,0 +1,168 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Xml.Serialization;
namespace LeanderShiftPlannerV2.Model
{
[Serializable]
public class Shift : INotifyPropertyChanged
{
private string _day = null!;
private string _startTime = null!;
private string _endTime = null!;
private int _duration;
private int _shiftInDay;
private int _maxPersonCount;
private int _minPersonCount;
private ObservableCollection<Person> _persons;
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Shift()
{
_persons = new ObservableCollection<Person>();
}
public Shift(string day, string startTime, string endTime, int duration, int shiftInDay, int maxPersonCount,
int minPersonCount)
{
this._day = day;
this._startTime = startTime;
this._endTime = endTime;
this._duration = duration;
this._shiftInDay = shiftInDay;
this._maxPersonCount = maxPersonCount;
this._minPersonCount = minPersonCount;
_persons = new ObservableCollection<Person>();
}
public string Day
{
get => _day;
set
{
_day = value;
OnPropertyChanged(nameof(Day));
}
}
[XmlIgnore]
public string Time
{
get
{
string[] startTimeParts = _startTime.Split(':');
string[] endTimeParts = _endTime.Split(':');
return startTimeParts[0].PadLeft(2, '0') + ":" + startTimeParts[1].PadLeft(2, '0')
+ " - " + endTimeParts[0].PadLeft(2, '0') + ":" + endTimeParts[1].PadLeft(2, '0');
}
}
public string StartTime
{
get => _startTime;
set
{
_startTime = value;
OnPropertyChanged(nameof(StartTime));
}
}
public string EndTime
{
get => _endTime;
set
{
_endTime = value;
OnPropertyChanged(nameof(EndTime));
}
}
public int Duration
{
get => _duration;
set
{
_duration = value;
OnPropertyChanged(nameof(Duration));
}
}
public int ShiftInDay
{
get => _shiftInDay;
set
{
_shiftInDay = value;
OnPropertyChanged(nameof(ShiftInDay));
}
}
public int MaxPersonCount
{
get => _maxPersonCount;
set
{
_maxPersonCount = value;
OnPropertyChanged(nameof(MaxPersonCount));
}
}
public int MinPersonCount
{
get => _minPersonCount;
set
{
_minPersonCount = value;
OnPropertyChanged(nameof(MinPersonCount));
}
}
[XmlIgnore]
public string PersonsString
{
get
{
string result = "";
foreach (Person person in _persons)
result += person.FirstName + "; ";
return result;
}
}
[XmlIgnore] public ObservableCollection<Person> Persons => _persons;
public void AddPerson(Person person)
{
_persons.Add(person);
OnPropertyChanged(nameof(PersonsString));
}
public void RemovePerson(Person person)
{
_persons.Remove(person);
OnPropertyChanged(nameof(PersonsString));
}
public void ClearPersons()
{
_persons.Clear();
OnPropertyChanged(nameof(PersonsString));
}
override
public string ToString()
{
return Day + " " + Time;
}
}
}

View File

@@ -0,0 +1,69 @@
using LeanderShiftPlannerV2.Service;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace LeanderShiftPlannerV2.Model
{
public class ShiftPlan
{
private readonly AppService _appService;
private readonly ObservableCollection<(Shift, Shift, Shift)> _shifts;
public ShiftPlan(AppService appService, List<(Shift, Shift, Shift)> shifts)
{
this._appService = appService;
this._shifts = new ObservableCollection<(Shift, Shift, Shift)>();
foreach ((Shift, Shift, Shift) shift in shifts)
_shifts.Add(shift);
}
public ObservableCollection<(Shift, Shift, Shift)> Shifts
{
get { return _shifts; }
}
override
public string ToString()
{
string result = string.Empty;
foreach (ValueTuple<Shift, Shift, Shift> day in _shifts)
{
if (day.Item1 is Shift)
{
result += day.Item1 + " (";
foreach (Person person in day.Item1.Persons)
result += person + ", ";
result = result.Remove(result.Length - 2);
result += "); ";
}
if (day.Item2 is Shift)
{
result += day.Item2 + " (";
foreach (Person person in day.Item2.Persons)
result += person + ", ";
result = result.Remove(result.Length - 2);
result += "); ";
}
if (day.Item3 is Shift)
{
result += day.Item3 + " (";
foreach (Person person in day.Item3.Persons)
result += person + ", ";
result = result.Remove(result.Length - 2);
result += "); ";
}
}
if (result != string.Empty)
{
result = result.Remove(result.Length - 2);
}
return result;
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace LeanderShiftPlannerV2.Model;
public class Timesheet
{
private List<(DateTime, DateTime, bool)> _value = new List<(DateTime, DateTime, bool)>();
public Timesheet(string month, List<(DateTime, DateTime, bool)> value)
{
Month = month;
_value.AddRange(value);
}
public List<(DateTime, DateTime, bool)> Value
{
get => _value;
set => _value = value;
}
public string Month { get; }
}

View File

@@ -0,0 +1,21 @@
using Avalonia;
using System;
namespace LeanderShiftPlannerV2;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View File

@@ -0,0 +1,24 @@
namespace LeanderShiftPlannerV2.Service
{
public class AppService
{
public AppService()
{
PersonService = new PersonService(this);
ShiftService = new ShiftService(this);
ShiftPlanService = new ShiftPlanService(this);
FilterService = new FilterService(this);
TimesheetService = new TimesheetService(this);
}
public PersonService PersonService { get; }
public ShiftService ShiftService { get; }
public ShiftPlanService ShiftPlanService { get; }
public FilterService FilterService { get; }
public TimesheetService TimesheetService { get; }
}
}

View File

@@ -0,0 +1,341 @@
using LeanderShiftPlannerV2.Model;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using LeanderShiftPlannerV2.Util;
namespace LeanderShiftPlannerV2.Service
{
public class FilterService
{
private readonly AppService _appService;
// Parameters
private IEnumerable<Person> _persons = new List<Person>();
private IEnumerable<Person> _personsParameter = new List<Person>();
private int _hours = 0;
private int _shift1 = 0;
private int _shift2 = 0;
private int _maxDays = 0;
public FilterService(AppService appService)
{
this._appService = appService;
Filters = new ObservableCollection<Filter>();
}
public ObservableCollection<Filter> Filters;
public void SetParameters(IEnumerable<Person>? persons, IEnumerable<Person>? personsParameter, int hours = 0,
int shift1 = 0, int shift2 = 0, int maxDays = 0)
{
this._persons = persons ?? new List<Person>();
this._personsParameter = personsParameter ?? new List<Person>();
this._hours = hours;
this._shift1 = shift1;
this._shift2 = shift2;
this._maxDays = maxDays;
}
public void FilterMinHoursInRow()
{
List<ShiftPlan> shiftPlans = [.. _appService.ShiftPlanService.GetAllShiftPlans()];
ConcurrentBag<ShiftPlan> removeItems = new ConcurrentBag<ShiftPlan>();
Parallel.ForEach(_persons, () => new List<ShiftPlan>(),
(person, state, localList) =>
{
foreach (ShiftPlan shiftPlan in shiftPlans)
{
if (CheckMinHoursInRow(person, shiftPlan, _hours))
{
localList.Add(shiftPlan);
}
}
return localList;
},
localList =>
{
foreach (ShiftPlan shiftPlan in localList)
{
removeItems.Add(shiftPlan);
}
});
RemoveFromList($"{ViewUtil.GetEnumerableAsString(_persons)} with min {_hours} in row", [.. removeItems]);
}
private static bool CheckMinHoursInRow(Person person, ShiftPlan shiftPlan, int hours)
{
int personHours = 0;
foreach ((Shift, Shift, Shift) shift in shiftPlan.Shifts)
{
if (shift.Item1 is Shift shift1 && shift1.Persons.Contains(person))
{
personHours += shift1.Duration;
}
if (shift.Item2 is Shift shift2 && shift2.Persons.Contains(person))
{
personHours += shift2.Duration;
}
if (shift.Item2 is Shift shift3 && shift3.Persons.Contains(person))
{
personHours += shift3.Duration;
}
}
return (personHours <= hours);
}
public void FilterIfShiftThenShift()
{
List<ShiftPlan> shiftPlans = [.. _appService.ShiftPlanService.GetAllShiftPlans()];
ConcurrentBag<ShiftPlan> removeItems = new ConcurrentBag<ShiftPlan>();
Parallel.ForEach(_persons, () => new List<ShiftPlan>(),
(person, state, localList) =>
{
foreach (ShiftPlan shiftPlan in shiftPlans)
{
if (CheckIfShiftThenShift(person, shiftPlan, _shift1 - 1, _shift2 - 1))
{
localList.Add(shiftPlan);
}
}
return localList;
},
localList =>
{
foreach (ShiftPlan shiftPlan in localList)
{
removeItems.Add(shiftPlan);
}
});
RemoveFromList($"{ViewUtil.GetEnumerableAsString(_persons)} if shift {_shift1} then shift {_shift2}", [.. removeItems]);
}
private static bool CheckIfShiftThenShift(Person person, ShiftPlan shiftPlan, int inputShift1, int inputShift2)
{
foreach ((Shift, Shift, Shift) shift in shiftPlan.Shifts)
{
List<object> shifts = new List<object>() { shift.Item1, shift.Item2, shift.Item3 };
if (shifts[inputShift1] is Shift shift1 && shifts[inputShift2] is Shift shift2)
{
if (shift1.Persons.Contains(person) && !shift2.Persons.Contains(person)) return true;
}
}
return false;
}
public void FilterMaxDays()
{
List<ShiftPlan> shiftPlans = [.. _appService.ShiftPlanService.GetAllShiftPlans()];
ConcurrentBag<ShiftPlan> removeItems = new ConcurrentBag<ShiftPlan>();
Parallel.ForEach(_persons, () => new List<ShiftPlan>(),
(person, state, localList) =>
{
foreach (ShiftPlan shiftPlan in shiftPlans)
{
if (CheckIfHoursThenMaxDays(person, shiftPlan, _hours, _maxDays))
{
localList.Add(shiftPlan);
}
}
return localList;
},
localList =>
{
foreach (ShiftPlan shiftPlan in localList)
{
removeItems.Add(shiftPlan);
}
});
RemoveFromList($"{ViewUtil.GetEnumerableAsString(_persons)} with {_maxDays} max days", [.. removeItems]);
}
private static bool CheckIfHoursThenMaxDays(Person person, ShiftPlan shiftPlan, int hours, int maxDays)
{
int personDays = 0;
foreach ((Shift, Shift, Shift) shift in shiftPlan.Shifts)
{
if (shift.Item1 is Shift shift1 && shift1.Persons.Contains(person)
|| (shift.Item2 is Shift shift2 && shift2.Persons.Contains(person))
|| (shift.Item3 is Shift shift3 && shift3.Persons.Contains(person))) personDays++;
}
return (personDays > maxDays);
}
public void FilterPersonWithPersons()
{
List<ShiftPlan> shiftPlans = [.. _appService.ShiftPlanService.GetAllShiftPlans()];
ConcurrentBag<ShiftPlan> removeItems = new ConcurrentBag<ShiftPlan>();
Parallel.ForEach(_persons, () => new List<ShiftPlan>(),
(person, state, localList) =>
{
foreach (ShiftPlan shiftPlan in shiftPlans)
{
if (CheckPersonWithPersons(person, shiftPlan, _personsParameter))
{
localList.Add(shiftPlan);
}
}
return localList;
},
localList =>
{
foreach (ShiftPlan shiftPlan in localList)
{
removeItems.Add(shiftPlan);
}
});
RemoveFromList($"{ViewUtil.GetEnumerableAsString(_persons)} with {ViewUtil.GetEnumerableAsString(_personsParameter)}", [.. removeItems]);
}
private static bool CheckPersonWithPersons(Person person, ShiftPlan shiftPlan,
IEnumerable<Person> personsParameter)
{
bool result1 = true;
bool result2 = true;
bool result3 = true;
foreach ((Shift, Shift, Shift) shift in shiftPlan.Shifts)
{
if (shift.Item1 is Shift shift1 && shift1.Persons.Contains(person))
{
foreach (Person paramterPerson in personsParameter)
result1 = (result1 && !shift1.Persons.Contains(paramterPerson));
}
else result1 = false;
if (shift.Item2 is Shift shift2 && shift2.Persons.Contains(person))
{
foreach (Person paramterPerson in personsParameter)
result2 = (result2 && !shift2.Persons.Contains(paramterPerson));
}
else result2 = false;
if (shift.Item3 is Shift shift3 && shift3.Persons.Contains(person))
{
foreach (Person paramterPerson in personsParameter)
result3 = (result3 && !shift3.Persons.Contains(paramterPerson));
}
else result3 = false;
}
return (result1 || result2 || result3);
}
public void FilterPersonWithoutPersons()
{
List<ShiftPlan> shiftPlans = [.. _appService.ShiftPlanService.GetAllShiftPlans()];
ConcurrentBag<ShiftPlan> removeItems = new ConcurrentBag<ShiftPlan>();
Parallel.ForEach(_persons, () => new List<ShiftPlan>(),
(person, state, localList) =>
{
foreach (ShiftPlan shiftPlan in shiftPlans)
{
if (CheckPersonWithoutPersons(person, shiftPlan, _personsParameter))
{
localList.Add(shiftPlan);
}
}
return localList;
},
localList =>
{
foreach (ShiftPlan shiftPlan in localList)
{
removeItems.Add(shiftPlan);
}
});
RemoveFromList($"{ViewUtil.GetEnumerableAsString(_persons)} without {ViewUtil.GetEnumerableAsString(_personsParameter)}", [.. removeItems]);
}
private static bool CheckPersonWithoutPersons(Person person, ShiftPlan shiftPlan,
IEnumerable<Person> personsParameter)
{
foreach ((Shift, Shift, Shift) shift in shiftPlan.Shifts)
{
if (shift.Item1 is Shift shift1 && shift1.Persons.Contains(person))
{
foreach (Person parameterPerson in personsParameter)
if (shift1.Persons.Contains(parameterPerson))
return true;
}
if (shift.Item2 is Shift shift2 && shift2.Persons.Contains(person))
{
foreach (Person paramterPerson in personsParameter)
if (shift2.Persons.Contains(paramterPerson))
return true;
}
if (shift.Item3 is Shift shift3 && shift3.Persons.Contains(person))
{
foreach (Person paramterPerson in personsParameter)
if (shift3.Persons.Contains(paramterPerson))
return true;
}
}
return false;
}
public void UndoFilter(Filter inputFilter)
{
List<ShiftPlan> notReturningShiftPlans = new List<ShiftPlan>();
List<ShiftPlan> returningShiftPlans = inputFilter.AffectedShiftPlans;
foreach (ShiftPlan affectedShiftPlan in inputFilter.AffectedShiftPlans)
{
bool foundShiftPlan = false;
foreach (Filter filter in Filters)
{
if (filter != inputFilter)
{
foreach (ShiftPlan shiftPlan in filter.AffectedShiftPlans)
{
if (affectedShiftPlan == shiftPlan)
{
notReturningShiftPlans.Add(affectedShiftPlan);
foundShiftPlan = true;
break;
}
}
}
if (foundShiftPlan) break;
}
}
foreach (ShiftPlan shiftPlan in notReturningShiftPlans) returningShiftPlans.Remove(shiftPlan);
foreach (ShiftPlan shiftPlan in returningShiftPlans) _appService.ShiftPlanService.GetShiftPlans().Add(shiftPlan);
Filters.Remove(inputFilter);
}
private void RemoveFromList(string sender, List<ShiftPlan> removeItems)
{
Filters.Add(new Filter(sender, removeItems));
foreach (ShiftPlan shiftPlan in removeItems)
{
_appService.ShiftPlanService.GetShiftPlans().Remove(shiftPlan);
}
}
public ObservableCollection<Filter> ResetAllFilters()
{
return Filters = new ObservableCollection<Filter>();
}
}
}

View File

@@ -0,0 +1,108 @@
using LeanderShiftPlannerV2.FileIO;
using LeanderShiftPlannerV2.Model;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace LeanderShiftPlannerV2.Service
{
public class PersonService
{
private readonly AppService _appService;
private readonly ObservableCollection<Person> _persons;
public PersonService(AppService appService)
{
this._appService = appService;
_persons = new ObservableCollection<Person>();
LoadPersonsFromFile();
}
private void LoadPersonsFromFile()
{
foreach (Person person in LoadDataFromFile.LoadPersons())
{
_persons.Add(person);
}
}
public ObservableCollection<Person> GetPersonList()
{
return _persons;
}
public Person AddNewPerson(string firstName, string surname, string hours)
{
Person newPerson = new Person(firstName, surname, int.Parse(hours));
_persons.Add(newPerson);
return newPerson;
}
public void EditPerson(Person person, string? firstName, string? surname, int? hours)
{
if (firstName != null)
{
person.FirstName = firstName;
}
if (surname != null)
{
person.Surname = surname;
}
if (hours != null)
{
person.Hours = (int)hours;
}
}
public void DeletePerson(Person person)
{
_appService.ShiftService.RemovePerson(person);
_persons.Remove(person);
}
public void SetShiftsForPerson(Person person, IEnumerable<Shift> shifts)
{
foreach (Shift shift in person.Shifts)
shift.RemovePerson(person);
person.ClearShifts();
AddShiftsToPerson(person, shifts);
}
public void AddShiftToPerson(Person person, Shift shift)
{
person.AddShift(shift);
if (!shift.Persons.Contains(person))
{
_appService.ShiftService.AddPersonToShift(shift, person);
}
}
public void AddShiftsToPerson(Person person, IEnumerable<Shift> shifts)
{
foreach (Shift shift in shifts)
{
AddShiftToPerson(person, shift);
}
}
public void RemoveShiftFromPerson(Person person, Shift shift)
{
person.RemoveShift(shift);
if (shift.Persons.Contains(person))
{
_appService.ShiftService.RemovePersonFromShift(shift, person);
}
}
public void RemoveShiftsFromPerson(Person person, IEnumerable<Shift> shifts)
{
foreach (Shift shift in shifts)
{
RemoveShiftFromPerson(person, shift);
}
}
}
}

View File

@@ -0,0 +1,371 @@
using LeanderShiftPlannerV2.Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace LeanderShiftPlannerV2.Service
{
public class ShiftPlanService
{
private int _id = 0;
private readonly AppService _appService;
private List<ShiftPlan> _allShiftPlans;
private ObservableCollection<ShiftPlan> _shiftPlans;
private List<Shift> _plannedShifts;
private List<string> _days;
public ShiftPlanService(AppService appService)
{
this._appService = appService;
this._allShiftPlans = new List<ShiftPlan>();
this._shiftPlans = new ObservableCollection<ShiftPlan>();
InitLists();
}
private void InitLists()
{
_allShiftPlans.Clear();
_shiftPlans.Clear();
this._plannedShifts = new List<Shift>();
this._days = new List<string>();
}
public int GetNextID()
{
int result = _id++;
return result;
}
public void StartCalculation(CancellationToken token)
{
InitLists();
GeneratePlannedShifts();
CalculateShiftPlans(token);
_allShiftPlans = [.. _shiftPlans];
}
public ObservableCollection<ShiftPlan> GetShiftPlans()
{
return this._shiftPlans;
}
public List<ShiftPlan> GetAllShiftPlans()
{
return this._allShiftPlans;
}
public void ResetShiftPlanList()
{
_shiftPlans = [.. _allShiftPlans];
}
public void RestartShiftPlanList()
{
List<ShiftPlan> tmp = _shiftPlans.ToList();
foreach (ShiftPlan shiftPlan in tmp)
{
_shiftPlans.Remove(shiftPlan);
_allShiftPlans.Remove(shiftPlan);
}
}
/*
* Generate all possible shifts for each shift in each day
* that could occour within the given data by generating all
* permutations for each personCount per shift, starting with
* MinPersonCount and going up to MaxPersonCount
*/
private void GeneratePlannedShifts()
{
InitDaysList();
foreach (Shift shift in _appService.ShiftService.GetShiftList())
{
for (int personCount = shift.MinPersonCount; personCount <= shift.MaxPersonCount; personCount++)
{
if (personCount == 1)
{
List<Person> personPermutations = new List<Person>();
for (int i = 0; i < shift.Persons.Count; i++)
{
personPermutations.Add((shift.Persons[i]));
}
_plannedShifts.AddRange(GeneratePlannedShifts(personPermutations, shift));
}
else if (personCount == 2)
{
List<(Person, Person)> personPermutations = new List<(Person, Person)>();
for (int i = 0; i < shift.Persons.Count; i++)
{
for (int j = i + 1; j < shift.Persons.Count; j++)
{
personPermutations.Add((shift.Persons[i], shift.Persons[j]));
}
}
_plannedShifts.AddRange(GeneratePlannedShifts(personPermutations, shift));
}
else if (personCount == 3)
{
List<(Person, Person, Person)> personPermutations = new List<(Person, Person, Person)>();
for (int i = 0; i < shift.Persons.Count; i++)
{
for (int j = i + 1; j < shift.Persons.Count; j++)
{
for (int k = j + 1; k < shift.Persons.Count; k++)
{
personPermutations.Add((shift.Persons[i], shift.Persons[j], shift.Persons[k]));
}
}
}
_plannedShifts.AddRange(GeneratePlannedShifts(personPermutations, shift));
}
}
}
}
/*
* Plan a shift with the given persons, where
* persons are given as single Person if PersonCount == 1
* and going up to (Person, Person, Person) - triple
* if PersonCount = 3
*/
public List<Shift> GeneratePlannedShifts<T>(List<T> personPermutations, Shift shift)
{
List<Shift> shifts = new List<Shift>();
foreach (T personPermutation in personPermutations)
{
Shift newShift = new Shift(shift.Day, shift.StartTime, shift.EndTime,
shift.Duration, shift.ShiftInDay, shift.MaxPersonCount, shift.MinPersonCount);
if (personPermutation is Person person)
{
newShift.Persons.Add(person);
}
else if (personPermutation is ValueTuple<Person, Person> tuple)
{
newShift.Persons.Add(tuple.Item1);
newShift.Persons.Add(tuple.Item2);
}
else if (personPermutation is ValueTuple<Person, Person, Person> triple)
{
newShift.Persons.Add(triple.Item1);
newShift.Persons.Add(triple.Item2);
newShift.Persons.Add(triple.Item3);
}
shifts.Add(newShift);
}
return shifts;
}
private void InitDaysList()
{
foreach (Shift shift in _appService.ShiftService.GetShiftList())
{
if (!_days.Contains(shift.Day))
{
_days.Add(shift.Day);
}
}
}
/*
* Calculation shiftPlans by permuting of all plannedShifts
* generated in GeneratePlannedShifts - method.
* Every Shift gets its own Task, so we get a dynamic parallelization
* for better performance in calculation
* Important: In plannedShifts there is no Check if persons over-
* or undercount their weekly hours, this check is made here
* for each ShiftPlan that got generated!
* So effectively: Every shiftPlan gets calculated by its own Task,
* which also checks if it's a valid shiftPlan for the given
* persons and their weekly hours!
*/
private void CalculateShiftPlans(CancellationToken token)
{
ParallelOptions parallelOptions = new ParallelOptions() { CancellationToken = token };
Dictionary<string, List<Shift>> plannedShiftsByDay = SplitShiftsByDay();
Dictionary<string, List<(Shift, Shift, Shift)>> plannedShiftsByDayAndTime =
GenerateDayShifts(plannedShiftsByDay);
try
{
Parallel.ForEach(plannedShiftsByDayAndTime[_days[0]], parallelOptions, monday =>
{
try
{
Parallel.ForEach(plannedShiftsByDayAndTime[_days[1]], parallelOptions, tuesday =>
{
try
{
Parallel.ForEach(plannedShiftsByDayAndTime[_days[2]], parallelOptions, wednesday =>
{
try
{
Parallel.ForEach(plannedShiftsByDayAndTime[_days[3]], parallelOptions,
thursday =>
{
try
{
Parallel.ForEach(plannedShiftsByDayAndTime[_days[4]],
parallelOptions, (friday, state) =>
{
List<(Shift, Shift, Shift)> shiftPlan =
new List<(Shift, Shift, Shift)>()
{ monday, tuesday, wednesday, thursday, friday };
if (CheckShift(shiftPlan))
_shiftPlans.Add(new ShiftPlan(_appService, shiftPlan));
});
}
catch (OperationCanceledException)
{
}
});
}
catch (OperationCanceledException)
{
}
});
}
catch (OperationCanceledException)
{
}
});
}
catch (OperationCanceledException)
{
}
});
}
catch (OperationCanceledException)
{
}
}
/*
* Check if some people in shiftPlan got more
* or less than the expected hours to work
*/
private bool CheckShift(List<(Shift, Shift, Shift)> shiftPlan)
{
Dictionary<string, int> calculatedPersonHours = new Dictionary<string, int>();
foreach (Person person in _appService.PersonService.GetPersonList())
{
calculatedPersonHours[person.FirstName] = 0;
}
// calc hours and check if hours are more than expected
foreach ((Shift, Shift, Shift) shift in shiftPlan)
{
if (shift.Item1 is Shift)
{
foreach (Person person in shift.Item1.Persons)
{
calculatedPersonHours[person.FirstName] += shift.Item1.Duration;
if (person.Hours < calculatedPersonHours[person.FirstName])
return false;
}
}
if (shift.Item2 is Shift)
{
foreach (Person person in shift.Item2.Persons)
{
calculatedPersonHours[person.FirstName] += shift.Item2.Duration;
if (person.Hours < calculatedPersonHours[person.FirstName])
return false;
}
}
if (shift.Item3 is Shift)
{
foreach (Person person in shift.Item3.Persons)
{
calculatedPersonHours[person.FirstName] += shift.Item3.Duration;
if (person.Hours < calculatedPersonHours[person.FirstName])
return false;
}
}
}
// check if hours in plan are less than expected
foreach (Person person in _appService.PersonService.GetPersonList())
{
if (person.Hours > calculatedPersonHours[person.FirstName])
return false;
}
return true;
}
private Dictionary<string, List<Shift>> SplitShiftsByDay()
{
Dictionary<string, List<Shift>> plannedShiftsByDay = new Dictionary<string, List<Shift>>();
foreach (Shift shift in _plannedShifts)
{
if (!plannedShiftsByDay.ContainsKey(shift.Day))
{
plannedShiftsByDay.Add(shift.Day, new List<Shift>());
}
plannedShiftsByDay[shift.Day].Add(shift);
}
return plannedShiftsByDay;
}
private Dictionary<string, List<(Shift, Shift, Shift)>> GenerateDayShifts(
Dictionary<string, List<Shift>> plannedShiftsByDay)
{
Dictionary<string, List<(Shift, Shift, Shift)>> plannedShiftsByDayAndTime =
new Dictionary<string, List<(Shift, Shift, Shift)>>();
foreach (string day in _days)
plannedShiftsByDayAndTime.Add(day, new List<(Shift, Shift, Shift)>());
foreach (string day in plannedShiftsByDay.Keys)
{
List<Shift> shifts = plannedShiftsByDay[day];
List<Shift> shifts1 = new List<Shift>();
List<Shift> shifts2 = new List<Shift>();
List<Shift> shifts3 = new List<Shift>();
foreach (Shift shift in shifts)
{
if (shift.ShiftInDay == 1)
shifts1.Add(shift);
else if (shift.ShiftInDay == 2)
shifts2.Add(shift);
else if (shift.ShiftInDay == 3)
shifts3.Add(shift);
}
for (int i = 0; i < shifts1.Count; i++)
{
if (shifts2.Count == 0)
{
plannedShiftsByDayAndTime[day].Add((shifts1[i], null, null));
}
for (int j = 0; j < shifts2.Count; j++)
{
if (shifts3.Count == 0)
{
plannedShiftsByDayAndTime[day].Add((shifts1[i], shifts2[j], null));
}
for (int k = 0; k < shifts3.Count; k++)
{
plannedShiftsByDayAndTime[day].Add((shifts1[i], shifts2[j], shifts3[k]));
}
}
}
}
return plannedShiftsByDayAndTime;
}
}
}

View File

@@ -0,0 +1,130 @@
using LeanderShiftPlannerV2.FileIO;
using LeanderShiftPlannerV2.Model;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
namespace LeanderShiftPlannerV2.Service
{
public class ShiftService
{
private readonly AppService _appService;
private readonly ObservableCollection<Shift> _shifts;
public ShiftService(AppService appService)
{
this._appService = appService;
_shifts = new ObservableCollection<Shift>();
LoadShiftsFromFile();
}
private void LoadShiftsFromFile()
{
foreach (Shift shift in LoadDataFromFile.LoadShifts())
{
_shifts.Add(shift);
}
}
internal ObservableCollection<Shift> GetShiftList()
{
return _shifts;
}
public Shift AddShift(string day, DateTime startTime, DateTime endTime, int shiftNumber, int maxPersonCount,
int minPersonCount)
{
string startTimeString = startTime.Hour.ToString() + ":" + startTime.Minute.ToString();
string endTimeString = endTime.Hour.ToString() + ":" + endTime.Minute.ToString();
TimeSpan duration = endTime - startTime;
Shift newShift = new Shift(day, startTimeString, endTimeString, duration.Hours, shiftNumber, maxPersonCount,
minPersonCount);
_shifts.Add(newShift);
return newShift;
}
public void EditShift(Shift shift, string? day, DateTime? startTime, DateTime? endTime, int shiftNumber,
int maxPersonCount, int minPersonCount)
{
if (day != null)
{
shift.Day = day;
}
if (startTime != null && endTime != null)
{
string startTimeString = startTime.Value.Hour.ToString() + ":" + startTime.Value.Minute.ToString();
string endTimeString = endTime.Value.Hour.ToString() + ":" + endTime.Value.Minute.ToString();
TimeSpan duration = endTime.Value - startTime.Value;
shift.StartTime = startTimeString;
shift.EndTime = endTimeString;
shift.Duration = duration.Hours;
shift.ShiftInDay = shiftNumber;
shift.MaxPersonCount = maxPersonCount;
shift.MinPersonCount = minPersonCount;
}
}
public void RemoveShift(Shift shift)
{
_shifts.Remove(shift);
}
public void SetPeronssForShift(Shift shift, IEnumerable<Person> persons)
{
foreach (Person person in shift.Persons)
person.RemoveShift(shift);
shift.ClearPersons();
AddPersonsToShift(shift, persons);
}
public void AddPersonToShift(Shift shift, Person person)
{
shift.AddPerson(person);
if (!person.Shifts.Contains(shift))
{
_appService.PersonService.AddShiftToPerson(person, shift);
}
}
public void AddPersonsToShift(Shift shift, IEnumerable<Person> persons)
{
foreach (Person person in persons)
{
AddPersonToShift(shift, person);
}
}
public void RemovePersonFromShift(Shift shift, Person person)
{
shift.RemovePerson(person);
if (person.Shifts.Contains(shift))
{
_appService.PersonService.RemoveShiftFromPerson(person, shift);
}
}
public void RemovePersonsFromShift(Shift shift, IEnumerable<Person> persons)
{
foreach (Person person in persons)
{
RemovePersonFromShift(shift, person);
}
}
public void RemovePerson(Person person)
{
foreach (Shift shift in _shifts)
{
shift.Persons.Remove(person);
}
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Util;
namespace LeanderShiftPlannerV2.Service;
public class TimesheetService
{
private readonly AppService _appService;
private const string HolidayApiUriPart1 = "https://feiertage-api.de/api/?jahr=";
private const string HolidayApiUriPart2 = "&nur_land=";
public TimesheetService(AppService appService)
{
_appService = appService;
}
public async Task GenerateTimeSheets()
{
List<Person> persons = _appService.PersonService.GetPersonList().ToList();
foreach (Person person in persons)
{
List<int> pattern = GetPattern(person);
List<DateTime> holidays = await GetHolidays();
List<DateTime> weekends = GetWeekends();
int monthHours = person.Hours * 4;
foreach (string month in Constants.Months)
{
if (month == Constants.Months[0]) continue;
List<(DateTime, DateTime, bool)> timesheetValue = new List<(DateTime, DateTime, bool)>();
int currentPersonHours = 0;
int currentPatternIndex = 0;
int dayOfMonth = 1;
while (currentPersonHours != monthHours)
{
DateTime currentDay = new DateTime(DateTime.Now.Year, Constants.Months.IndexOf(month), dayOfMonth);
if (holidays.Contains(currentDay) || weekends.Contains(currentDay))
{
dayOfMonth++;
continue;
}
switch (pattern[currentPatternIndex])
{
case 6:
{
DateTime dayStart = new DateTime(currentDay.Year, currentDay.Month, currentDay.Day, 9, 0, 0);
DateTime dayEnd = new DateTime(currentDay.Year, currentDay.Month, currentDay.Day, 16, 0, 0);
timesheetValue.Add((dayStart, dayEnd, true));
currentPersonHours += 6;
break;
}
case 4:
{
DateTime dayStart = new DateTime(currentDay.Year, currentDay.Month, currentDay.Day, 9, 0, 0);
DateTime dayEnd = new DateTime(currentDay.Year, currentDay.Month, currentDay.Day, 13, 0, 0);
timesheetValue.Add((dayStart, dayEnd, false));
currentPersonHours += 4;
break;
}
case 2:
{
DateTime dayStart = new DateTime(currentDay.Year, currentDay.Month, currentDay.Day, 14, 0, 0);
DateTime dayEnd = new DateTime(currentDay.Year, currentDay.Month, currentDay.Day, 16, 0, 0);
timesheetValue.Add((dayStart, dayEnd, false));
currentPersonHours += 2;
break;
}
}
dayOfMonth++;
currentPatternIndex = (currentPatternIndex + 1) % pattern.Count;
}
person.Timesheets.Add(new Timesheet(month, timesheetValue));
}
}
}
private List<DateTime> GetWeekends()
{
List<DateTime> weekends = new List<DateTime>();
DateTime firstDayOfYear = new DateTime(DateTime.Now.Year, 1, 1);
DateTime lastDayOfYear = new DateTime(DateTime.Now.Year, 12, 31);
int daysInYear = lastDayOfYear.DayOfYear;
for (int i = 0; i <= daysInYear; i++)
{
DateTime currentDay = firstDayOfYear.AddDays(i);
if (currentDay.DayOfWeek == DayOfWeek.Saturday || currentDay.DayOfWeek == DayOfWeek.Sunday) weekends.Add(currentDay);
}
return weekends;
}
private async Task<List<DateTime>> GetHolidays()
{
List<DateTime> holidays = new List<DateTime>();
string url = $"{HolidayApiUriPart1}{DateTime.Now.Year}{HolidayApiUriPart2}HE";
using HttpClient client = new HttpClient();
try
{
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string jsonResponse = await response.Content.ReadAsStringAsync();
JsonSerializerOptions options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
JsonObject? data = JsonSerializer.Deserialize<JsonObject>(jsonResponse, options);
foreach (KeyValuePair<string, JsonNode> item in data)
{
string test = item.Value.Deserialize<JsonObject>().First().Value.ToString();
string[] dateSplit = test.Split("-");
holidays.Add(new DateTime(int.Parse(dateSplit[0]),
int.Parse(dateSplit[1]), int.Parse(dateSplit[2])));
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
return holidays;
}
private List<int> GetPattern(Person person)
{
int hours = 0;
List<int> pattern = new List<int>();
while (hours < person.Hours)
{
if (hours + 6 <= person.Hours)
{
pattern.Add(6);
hours += 6;
}
else if (hours + 4 <= person.Hours)
{
pattern.Add(4);
hours += 4;
}
else if (hours + 2 <= person.Hours)
{
pattern.Add(2);
hours += 2;
}
else if (hours + 2 > person.Hours)
{
// Error! Could not find pattern!
return null;
}
}
return pattern;
}
}

View File

@@ -0,0 +1,86 @@
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.View.FilterView;
using LeanderShiftPlannerV2.View.PersonView;
using LeanderShiftPlannerV2.View.ShiftPlanView;
using LeanderShiftPlannerV2.View.ShiftView;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using LeanderShiftPlannerV2.View.ErrorView;
namespace LeanderShiftPlannerV2.Service
{
public static class ViewService
{
public static PersonCreator ShowPersonCreator(Window sender, AppService appService)
{
PersonCreator personCreator = new PersonCreator(appService);
personCreator.Show(sender);
return personCreator;
}
public static PersonEditor ShowPersonEditor(Window sender, AppService appService, Person person)
{
PersonEditor personEditor = new PersonEditor(appService, person);
personEditor.Show(sender);
return personEditor;
}
public static PersonShiftSelector ShowPersonShiftSelector(Window sender, AppService appService, Person person,
ObservableCollection<Shift> shifts)
{
PersonShiftSelector personShiftSelector = new PersonShiftSelector(appService, person, shifts);
personShiftSelector.Show(sender);
return personShiftSelector;
}
public static ShiftCreator ShowShiftCreator(Window sender, AppService appService)
{
ShiftCreator shiftCreator = new ShiftCreator(appService);
shiftCreator.Show(sender);
return shiftCreator;
}
public static ShiftEditor ShowShiftEditor(Window sender, AppService appService, Shift shift)
{
ShiftEditor shiftEditor = new ShiftEditor(appService, shift);
shiftEditor.Show(sender);
return shiftEditor;
}
public static ShiftPersonSelector ShowShiftPersonSelector(Window sender, AppService appService, Shift shift,
ObservableCollection<Person> persons)
{
ShiftPersonSelector shiftPersonSelector = new ShiftPersonSelector(appService, shift, persons);
shiftPersonSelector.Show(sender);
return shiftPersonSelector;
}
public static ShiftPlanViewer ShowShiftPlanViewer(Window sender, AppService appService, ShiftPlan shiftPlan)
{
ShiftPlanViewer shiftPlanViewer = new ShiftPlanViewer(appService, shiftPlan);
shiftPlanViewer.Show(sender);
return shiftPlanViewer;
}
public static FilterSelector ShowFilterSelector(Window sender, AppService appService)
{
FilterSelector filterSelector = new FilterSelector(appService);
filterSelector.Show(sender);
return filterSelector;
}
public static NoInputError ShowNoInputError(Window sender)
{
NoInputError noInputError = new NoInputError();
noInputError.Show(sender);
return noInputError;
}
public static AreYouSure ShowAreYouSure(Window sender)
{
AreYouSure areYouSure = new AreYouSure();
areYouSure.Show(sender);
return areYouSure;
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace LeanderShiftPlannerV2.Util
{
public static class Constants
{
// FileIO
public const string DataPath = @"Data";
public const string SavedVariablesPath = DataPath + @"/SavedVariables";
public const string PersonsPath = SavedVariablesPath + @"/Persons.xml";
public const string ShiftsPath = SavedVariablesPath + @"/Shifts.xml";
public const string ConnectionsPath = SavedVariablesPath + @"/Conections.xml";
public const string ExportPath = DataPath + @"/Output";
public const string ShiftPlanPath = ExportPath + @"/ShiftPlans/";
public const string TimesheetPath = ExportPath + "/Timesheets/";
public const string TimesheetResource = @"Resources/templates/timesheet.xlsx";
public const string FontPath = @"Resources/font/Emblem.ttf";
// TimeSheets
public static readonly List<string> Months = new List<string>
{
"ERROR!",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
public static readonly List<string> MonthsGerman = new List<string>
{
"ERROR!",
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
};
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace LeanderShiftPlannerV2.Util
{
[Serializable]
public class PersonShiftConnection
{
private string _firstName;
private List<string> _shifts;
public PersonShiftConnection()
{
_shifts = new List<string>();
}
public string FirstName
{
get => _firstName;
set => _firstName = value;
}
public List<string> Shifts
{
get => _shifts;
set => _shifts = value;
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using LeanderShiftPlannerV2.Model;
namespace LeanderShiftPlannerV2.Util
{
public static class ViewUtil
{
public static bool CheckNumericText(string text)
{
Regex regex = new Regex("[^0-9]+");
return regex.IsMatch(text);
}
public static bool CheckPersonCountText(string text)
{
Regex regex = new Regex("[^1-3]");
return regex.IsMatch(text);
}
public static string GetEnumerableAsString(IEnumerable<Object> objects)
{
string result = "";
if (objects is IEnumerable<Person>)
{
foreach (Person person in objects)
{
result += person.FirstName + ", ";
}
result = result.Remove(result.Length - 2, 2);
}
return result;
}
}
}

View File

@@ -0,0 +1,13 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.ErrorView.AreYouSure"
Title="AreYouSure" Height="100" Width="300" CanResize="False" WindowStartupLocation="CenterOwner">
<Grid>
<TextBlock Text="Are you sure?" Height="32" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0, 0, 0, 15"/>
<Button x:Name="ButtonAccept" Content="Accept" Height="32" Width="100" HorizontalAlignment="Right" VerticalAlignment="Bottom" HorizontalContentAlignment="Center" Margin="0, 0, 20, 10" Click="ButtonAccept_OnClick"/>
<Button x:Name="ButtonCancel" Content="Cancel" Height="32" Width="100" HorizontalAlignment="Left" VerticalAlignment="Bottom" HorizontalContentAlignment="Center" Margin="20, 0, 0, 10" Click="ButtonCancel_OnClick"/>
</Grid>
</Window>

View File

@@ -0,0 +1,26 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LeanderShiftPlannerV2.View.ErrorView;
public partial class AreYouSure : Window
{
public AreYouSure()
{
InitializeComponent();
}
public bool DialogResult { get; set; }
private void ButtonAccept_OnClick(object? sender, RoutedEventArgs e)
{
DialogResult = true;
this.Close();
}
private void ButtonCancel_OnClick(object? sender, RoutedEventArgs e)
{
DialogResult = false;
this.Close();
}
}

View File

@@ -0,0 +1,12 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.ErrorView.FileLockedError"
Title="FileLockedError" Height="100" Width="300" CanResize="False" WindowStartupLocation="CenterOwner">
<Grid>
<TextBlock Text="One or more files you tried to write are locked by an other program" Height="32" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0, 0, 0, 15"/>
<Button x:Name="ButtonAccept" Content="Accept" Height="32" Width="100" HorizontalAlignment="Right" VerticalAlignment="Bottom" HorizontalContentAlignment="Center" Margin="0, 0, 0, 10" Click="ButtonAccept_OnClick"/>
</Grid>
</Window>

View File

@@ -0,0 +1,20 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LeanderShiftPlannerV2.View.ErrorView;
public partial class FileLockedError : Window
{
public FileLockedError()
{
InitializeComponent();
}
public bool DialogResult { get; set; }
private void ButtonAccept_OnClick(object? sender, RoutedEventArgs e)
{
DialogResult = true;
this.Close();
}
}

View File

@@ -0,0 +1,12 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.ErrorView.NoInputError"
Title="NoInputError" Height="100" Width="300" CanResize="False" WindowStartupLocation="CenterOwner">
<Grid>
<TextBlock Text="You have to fill all fields with data!" Height="32" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0, 0, 0, 15"/>
<Button x:Name="ButtonAccept" Content="Accept" Height="32" Width="100" HorizontalAlignment="Center" VerticalAlignment="Bottom" HorizontalContentAlignment="Center" Margin="0, 0, 0, 10" Click="ButtonAccept_OnClick"/>
</Grid>
</Window>

View File

@@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LeanderShiftPlannerV2.View.ErrorView;
public partial class NoInputError : Window
{
public NoInputError()
{
InitializeComponent();
}
private void ButtonAccept_OnClick(object? sender, RoutedEventArgs e)
{
this.Close();
}
}

View File

@@ -0,0 +1,49 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:leanderControls="clr-namespace:LeanderShiftPlannerV2.View.LeanderControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.FilterView.FilterSelector"
Title="FilterSelector" Height="425" Width="375" WindowStartupLocation="CenterOwner" CanResize="False">
<Grid>
<Label Content="Filter list using following filter and persons:" Margin="0,15,0,0" Height="25" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<Grid HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,55,0,0" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto">
<Label Grid.Column="0" Grid.Row="0" Content="Select filter:" Height="25" HorizontalContentAlignment="Right"/>
<ComboBox x:Name="ComboBoxFilters" Grid.Column="1" Grid.Row="0" Height="32" Width="150" SelectionChanged="ComboBox_SelectionChanged"/>
<Label Grid.Column="0" Grid.Row="1" Content="Select persons:" Height="25" HorizontalContentAlignment="Right"/>
<leanderControls:CheckComboBox Grid.Column="1" Grid.Row="1" x:Name="CheckComboBoxPersonSelector" Height="32" Width="150"/>
</Grid>
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="Auto" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,75">
<Label Grid.Row="0" Content="Filter parameters:" HorizontalAlignment="Center" VerticalAlignment="Top"/>
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,Auto" VerticalAlignment="Bottom">
<Label Grid.Column="0" Grid.Row="0" Content="Hours in row:" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center"/>
<TextBox Grid.Column="1" Grid.Row="0" x:Name="TextBoxHoursInRow" Height="32" Width="150" TextInput="TextBox_PreviewTextInput"/>
<Label Grid.Column="0" Grid.Row="1" Content="Shift1:" Height="25" HorizontalContentAlignment="Right" HorizontalAlignment="Right" VerticalContentAlignment="Center"/>
<ComboBox x:Name="ComboBoxShift1" Grid.Column="1" Grid.Row="1" Height="32" Width="150">
<ComboBoxItem Content="1"/>
<ComboBoxItem Content="2"/>
<ComboBoxItem Content="3"/>
</ComboBox>
<Label Grid.Column="0" Grid.Row="2" Content="Shift2:" Height="25" HorizontalContentAlignment="Right" HorizontalAlignment="Right" VerticalContentAlignment="Center"/>
<ComboBox x:Name="ComboBoxShift2" Grid.Column="1" Grid.Row="2" Height="32" Width="150">
<ComboBoxItem Content="1"/>
<ComboBoxItem Content="2"/>
<ComboBoxItem Content="3"/>
</ComboBox>
<Label Grid.Column="0" Grid.Row="3" Content="Max days:" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center"/>
<TextBox x:Name="TextBoxMaxDays" Grid.Column="1" Grid.Row="3" Height="32" Width="150" TextInput="TextBox_PreviewTextInput"/>
<Label Grid.Column="0" Grid.Row="4" Content="Select persons:" Height="25" HorizontalContentAlignment="Right"/>
<leanderControls:CheckComboBox Grid.Column="1" Grid.Row="4" x:Name="CheckComboBoxPersonParameterSelector" Height="32" Width="150"/>
</Grid>
</Grid>
<Button x:Name="ButtonApplyFilter" Content="Apply filter" Height="32" Width="100" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,15" Click="ButtonApplyFilter_Click"/>
</Grid>
</Window>

View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
using LeanderShiftPlannerV2.Util;
namespace LeanderShiftPlannerV2.View.FilterView;
public partial class FilterSelector : Window
{
private readonly AppService _appService;
private Dictionary<string, (Action, Action)> _filters = null!;
public FilterSelector(AppService appService)
{
InitializeComponent();
this._appService = appService;
InitFilters();
ComboBoxFilters.ItemsSource = _filters.Keys;
CheckComboBoxPersonSelector.ItemsSource = _appService.PersonService.GetPersonList();
CheckComboBoxPersonParameterSelector.ItemsSource = _appService.PersonService.GetPersonList();
DeactivateParameters();
}
private void InitFilters()
{
_filters = new Dictionary<string, (Action, Action)>
{
{ "IfShiftThenShift", (_appService.FilterService.FilterIfShiftThenShift, IfShiftThenShiftInRowSelected) },
{ "MaxDays", (_appService.FilterService.FilterMaxDays, FilterMaxDaysSelected) },
{
"FilterPersonWithPersons",
(_appService.FilterService.FilterPersonWithPersons, FilterPersonWithPersonsSelected)
},
{
"FilterPersonWithoutPersons",
(_appService.FilterService.FilterPersonWithoutPersons, FilterPersonWithoutPersonsSelected)
}
};
}
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DeactivateParameters();
if (ComboBoxFilters.SelectedItem?.ToString() is { } key) _filters[key].Item2.Invoke();
}
private void TextBox_PreviewTextInput(object sender, TextInputEventArgs e)
{
if (e.Text != null) e.Handled = ViewUtil.CheckNumericText(e.Text);
}
private void ButtonApplyFilter_Click(object sender, RoutedEventArgs e)
{
SetParameters();
if (ComboBoxFilters.SelectedItem?.ToString() is { } key) _filters[key].Item1.Invoke();
}
private void SetParameters()
{
List<Person> persons = (from Person person in CheckComboBoxPersonSelector.SelectedItems select person).ToList();
List<Person> personsParameter =
(from Person person in CheckComboBoxPersonParameterSelector.SelectedItems select person).ToList();
int shift1 = ComboBoxShift1.SelectedIndex;
int shift2 = ComboBoxShift2.SelectedIndex;
int.TryParse(TextBoxHoursInRow.Text, out int hours);
int.TryParse(TextBoxMaxDays.Text, out int maxDays);
_appService.FilterService.SetParameters(persons, personsParameter, hours, shift1 + 1, shift2 + 1, maxDays);
}
private void DeactivateParameters()
{
TextBoxHoursInRow.IsEnabled = false;
ComboBoxShift1.IsEnabled = false;
ComboBoxShift2.IsEnabled = false;
TextBoxMaxDays.IsEnabled = false;
CheckComboBoxPersonParameterSelector.IsEnabled = false;
}
// ReSharper disable once UnusedMember.Local
private void FilterMinHoursInRowSelected()
{
TextBoxHoursInRow.IsEnabled = true;
}
private void IfShiftThenShiftInRowSelected()
{
ComboBoxShift1.IsEnabled = true;
ComboBoxShift2.IsEnabled = true;
}
private void FilterMaxDaysSelected()
{
TextBoxMaxDays.IsEnabled = true;
}
private void FilterPersonWithoutPersonsSelected()
{
CheckComboBoxPersonParameterSelector.IsEnabled = true;
}
private void FilterPersonWithPersonsSelected()
{
CheckComboBoxPersonParameterSelector.IsEnabled = true;
}
}

View File

@@ -0,0 +1,10 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="LeanderShiftPlannerV2.View.LeanderControls.CheckComboBox">
<ComboBox x:Name="ThisCheckComboBox" SelectionChanged="ThisCheckComboBox_OnSelectionChanged"
Width="{Binding Width, RelativeSource={RelativeSource AncestorType=UserControl}}"
Height="{Binding Height, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</UserControl>

View File

@@ -0,0 +1,70 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LeanderShiftPlannerV2.View.LeanderControls;
public partial class CheckComboBox : UserControl
{
private ObservableCollection<object> _itemsSource = new ObservableCollection<object>();
private readonly ObservableCollection<object> _selectedItems = new ObservableCollection<object>();
private readonly Dictionary<object, object> _items = new Dictionary<object, object>();
public CheckComboBox()
{
InitializeComponent();
}
public IEnumerable ItemsSource
{
get => _itemsSource;
set
{
_itemsSource = new ObservableCollection<object>((IEnumerable<object>)value);
CreateNewItemsList();
}
}
public IList SelectedItems => _selectedItems;
private void CreateNewItemsList()
{
ThisCheckComboBox.Items.Clear();
foreach (object item in ItemsSource)
{
CheckBox newItemCheckBox = new CheckBox();
newItemCheckBox.Content = item;
newItemCheckBox.IsCheckedChanged += ItemCheckedUnchecked!;
ThisCheckComboBox.Items.Add(newItemCheckBox);
_items.Add(newItemCheckBox, item);
}
}
private void ItemCheckedUnchecked(object sender, RoutedEventArgs e)
{
if (!_selectedItems.Contains(_items[sender]))
{
_selectedItems.Add(_items[sender]);
}
else
{
_selectedItems.Remove(_items[sender]);
}
string text = "";
foreach (object selectedItem in _selectedItems)
{
text += selectedItem + ", ";
}
if (_selectedItems.Count > 0) text = text.Remove(text.Length - 2);
ThisCheckComboBox.PlaceholderText = text;
}
private void ThisCheckComboBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
ThisCheckComboBox.SelectedIndex = -1;
}
}

View File

@@ -0,0 +1,13 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.LeanderControls.FilterElement">
<StackPanel Orientation="Horizontal">
<Label x:Name="LabelBoxFilterText" VerticalAlignment="Center" Padding="5, 0"/>
<Button x:Name="ButtonRemoveFilter" Content="X" VerticalAlignment="Center" Click="ButtonRemoveFilter_OnClick"/>
<Grid MinWidth="10" MaxWidth="10"/>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,26 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
namespace LeanderShiftPlannerV2.View.LeanderControls;
public partial class FilterElement : UserControl
{
private readonly AppService _appService;
private readonly Filter _model;
public FilterElement(AppService appService, Filter model)
{
InitializeComponent();
_model = model;
_appService = appService;
LabelBoxFilterText.Content = _model.FilterString;
}
private void ButtonRemoveFilter_OnClick(object? sender, RoutedEventArgs e)
{
_appService.FilterService.UndoFilter(_model);
}
}

View File

@@ -0,0 +1,140 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:model="clr-namespace:LeanderShiftPlannerV2.Model"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.LeanderShiftPlannerView.LeanderShiftPlannerMainWindow"
Title="LeanderShiftPlanner" MinHeight="800" MinWidth="1280" Height="800" Width="1280"
WindowStartupLocation="CenterScreen" Closing="Window_Closing">
<Grid>
<TabControl x:Name="TabControlRoot" Margin="0,0,0,200" SelectionChanged="TabControlRoot_SelectionChanged">
<TabItem x:Name="TabItemPersons" Header="Persons">
<ListBox x:Name="ListViewPersons" SelectionMode="Single">
<ListBox.Template>
<ControlTemplate>
<DockPanel>
<Grid DockPanel.Dock="Top" Height="30" ColumnDefinitions="100,70,1030" Margin="10,0, 0, 0">
<TextBlock Grid.Column="0" Text="Name" TextAlignment="Center" Width="100"/>
<TextBlock Grid.Column="1" Text="Hours" TextAlignment="Center" Width="70"/>
<TextBlock Grid.Column="2" Text="Shifts" TextAlignment="Center" Width="1030"/>
</Grid>
<ItemsPresenter/>
</DockPanel>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="100,70,1030">
<TextBlock Grid.Column="0" Text="{Binding FirstName, Mode=OneWay}" TextAlignment="Center" Width="100" x:DataType="model:Person"/>
<TextBlock Grid.Column="1" Text="{Binding Hours, Mode=OneWay}" TextAlignment="Center" Width="70" x:DataType="model:Person"/>
<TextBlock Grid.Column="2" Text="{Binding ShiftsString, Mode=OneWay}" TextAlignment="Center" Width="1030" x:DataType="model:Person"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</TabItem>
<TabItem x:Name="TabItemShifts" Header="Shifts">
<ListBox x:Name="ListViewShifts">
<ListBox.Template>
<ControlTemplate>
<DockPanel>
<Grid DockPanel.Dock="Top" Height="30" ColumnDefinitions="100,100,80, 125, 125, 70, 650" Margin="10,0, 0, 0">
<TextBlock Grid.Column="0" Text="Day" TextAlignment="Center" Width="100"/>
<TextBlock Grid.Column="1" Text="Time" TextAlignment="Center" Width="100"/>
<TextBlock Grid.Column="2" Text="ShiftInDay" TextAlignment="Center" Width="80"/>
<TextBlock Grid.Column="3" Text="MinPersonCount" TextAlignment="Center" Width="125"/>
<TextBlock Grid.Column="4" Text="MaxPersonCount" TextAlignment="Center" Width="125"/>
<TextBlock Grid.Column="5" Text="Duration" TextAlignment="Center" Width="70"/>
<TextBlock Grid.Column="6" Text="Persons" TextAlignment="Center" Width="650"/>
</Grid>
<ItemsPresenter/>
</DockPanel>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Day, Mode=OneWay}" Width="100" TextAlignment="Center" x:DataType="model:Shift"/>
<TextBlock Text="{Binding Time, Mode=OneWay}" Width="100" TextAlignment="Center" x:DataType="model:Shift"/>
<TextBlock Text="{Binding ShiftInDay, Mode=OneWay}" Width="80" TextAlignment="Center" x:DataType="model:Shift"/>
<TextBlock Text="{Binding MinPersonCount, Mode=OneWay}" Width="125" TextAlignment="Center" x:DataType="model:Shift"/>
<TextBlock Text="{Binding MaxPersonCount, Mode=OneWay}" Width="125" TextAlignment="Center" x:DataType="model:Shift"/>
<TextBlock Text="{Binding Duration, Mode=OneWay}" Width="70" TextAlignment="Center" x:DataType="model:Shift"/>
<TextBlock Text="{Binding PersonsString, Mode=OneWay}" Width="650" TextAlignment="Center" x:DataType="model:Shift"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</TabItem>
<TabItem x:Name="TabItemShiftPlans" Header="ShiftPlans">
<ScrollViewer>
<StackPanel>
<WrapPanel x:Name="StackPanelFilterList" MinHeight="40"/>
<ListBox x:Name="ListViewShiftPlans"/>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
<Grid MinHeight="200" MaxHeight="200" VerticalAlignment="Bottom">
<Grid HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,50,35"
RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="Auto">
<Button Grid.Column="0" Grid.Row="0" x:Name="ButtonAddPerson" Content="Add new person" Height="32"
Width="175" HorizontalContentAlignment="Center" Click="ButtonAddPerson_Click" />
<Button Grid.Column="0" Grid.Row="1" x:Name="ButtonEditPerson" Content="Edit person" Height="32"
Width="175" HorizontalContentAlignment="Center" Click="ButtonEditPerson_Click" />
<Button Grid.Column="0" Grid.Row="2" x:Name="ButtonDeletePerson" Content="Delete person" Height="32"
Width="175" HorizontalContentAlignment="Center" Click="ButtonDeletePerson_Click" />
<Button Grid.Column="0" Grid.Row="3" x:Name="ButtonAddShiftToPerson" Content="Select shifts for person"
Height="32" Width="175" HorizontalContentAlignment="Center"
Click="ButtonAddShiftToPerson_Click" />
</Grid>
<Grid HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="50,0,0,35"
RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="Auto">
<Button Grid.Column="0" Grid.Row="0" x:Name="ButtonAddShift" Content="Add new shift" Height="32"
Width="175" HorizontalContentAlignment="Center" Click="ButtonAddShift_Click" />
<Button Grid.Column="0" Grid.Row="1" x:Name="ButtonEditShift" Content="Edit shift" Height="32"
Width="175" HorizontalContentAlignment="Center" Click="ButtonEditShift_Click" />
<Button Grid.Column="0" Grid.Row="2" x:Name="ButtonDeleteShift" Content="Delete shift" Height="32"
Width="175" HorizontalContentAlignment="Center" Click="ButtonDeleteShift_Click" />
<Button Grid.Column="0" Grid.Row="3" x:Name="ButtonAddPersonToShift" Content="Select persons for shift"
Height="32" Width="175" HorizontalContentAlignment="Center"
Click="ButtonAddPersonToShift_Click" />
</Grid>
<Grid Margin="500,0,0,50" HorizontalAlignment="Center" VerticalAlignment="Bottom"
RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="Auto">
<Button Grid.Column="0" Grid.Row="1" x:Name="ButtonResetAllFilters" Content="Reset all filters" Height="32"
Width="175" HorizontalContentAlignment="Center" Click="ButtonResetAllFilters_Click" />
<Button Grid.Column="0" Grid.Row="2" x:Name="ButtonFilterShiftPlans" Content="Filter shiftplans"
Height="32" Width="175" HorizontalContentAlignment="Center"
Click="ButtonFilterShiftPlans_Click" />
</Grid>
<Grid HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,500,50"
RowDefinitions="Auto,Auto, Auto" ColumnDefinitions="Auto">
<Button Grid.Column="0" Grid.Row="0" x:Name="ButtonGenerateTimesheets" Content="Generate Timesheets"
Height="32" Width="180" HorizontalContentAlignment="Center" Click="ButtonGenerateTimesheets_OnClick"/>
<Button Grid.Column="0" Grid.Row="1" x:Name="ButtonOpenShiftPlan" Content="Open selected ShiftPlan"
Height="32" Width="180" HorizontalContentAlignment="Center" Click="ButtonOpenShiftPlan_Click" />
<Button Grid.Column="0" Grid.Row="2" x:Name="ButtonSaveShiftPlans" Content="Save all ShifPlans"
Height="32" Width="180" HorizontalContentAlignment="Center" Click="ButtonSaveShiftPlans_Click" />
</Grid>
<Button x:Name="ButtonStartCalculation" Content="Start calculation" Height="32" Width="175"
HorizontalContentAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,80"
Click="ButtonStartCalculation_Click" />
<ProgressBar x:Name="ProgressbarCalculaion" Height="25" Width="300" HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="0,0,0,50" />
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using LeanderShiftPlannerV2.FileIO;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
using LeanderShiftPlannerV2.View.ErrorView;
using LeanderShiftPlannerV2.View.LeanderControls;
namespace LeanderShiftPlannerV2.View.LeanderShiftPlannerView;
public partial class LeanderShiftPlannerMainWindow : Window
{
private readonly AppService _appService = ((App)Application.Current!).AppService;
private CancellationTokenSource _cts = new CancellationTokenSource();
private CancellationToken _cancelCalculation;
private bool _calcRunning;
private readonly Dispatcher _dispatcher = Dispatcher.UIThread;
private readonly Dictionary<Filter, FilterElement> _filters = new Dictionary<Filter, FilterElement>();
public LeanderShiftPlannerMainWindow()
{
InitializeComponent();
LoadData();
DeactivateView();
ViewModePersons();
ListViewPersons.ItemsSource = _appService.PersonService.GetPersonList();
ListViewShifts.ItemsSource = _appService.ShiftService.GetShiftList();
_appService.PersonService.GetPersonList().CollectionChanged += PersonListChanged;
_appService.ShiftService.GetShiftList().CollectionChanged += ShiftListChanged;
_appService.ShiftPlanService.GetShiftPlans().CollectionChanged += ShiftPlansChanged;
_appService.FilterService.Filters.CollectionChanged += FilterListChanged;
}
// ------------------------------------------------------UI-Interactions------------------------------------------------------
private void ButtonAddPerson_Click(object? sender, RoutedEventArgs e)
{
ViewService.ShowPersonCreator(this, _appService);
}
private void ButtonEditPerson_Click(object? sender, RoutedEventArgs e)
{
if (ListViewPersons.SelectedItem is Person selectedPerson)
{
ViewService.ShowPersonEditor(this, _appService, selectedPerson);
}
}
private void ButtonDeletePerson_Click(object? sender, RoutedEventArgs e)
{
AreYouSure confirm = ViewService.ShowAreYouSure(this);
confirm.Closed += (_, _) =>
{
if (!confirm.DialogResult) return;
if (ListViewPersons.SelectedItem is not Person selectedPerson) return;
_appService.PersonService.DeletePerson(selectedPerson);
PersonListChanged(sender, e);
};
}
private void ButtonAddShiftToPerson_Click(object? sender, RoutedEventArgs e)
{
if (ListViewPersons.SelectedItem is Person selectedPerson)
{
ViewService.ShowPersonShiftSelector(this, _appService, selectedPerson,
_appService.ShiftService.GetShiftList());
}
}
private void ButtonAddShift_Click(object? sender, RoutedEventArgs e)
{
ViewService.ShowShiftCreator(this, _appService);
}
private void ButtonEditShift_Click(object? sender, RoutedEventArgs e)
{
if (ListViewShifts.SelectedItem is Shift selectedShift)
{
ViewService.ShowShiftEditor(this, _appService, selectedShift);
}
}
private void ButtonDeleteShift_Click(object? sender, RoutedEventArgs e)
{
AreYouSure confirm = ViewService.ShowAreYouSure(this);
confirm.Closed += (_, _) =>
{
if (!confirm.DialogResult) return;
if (ListViewShifts.SelectedItem is not Shift selectedShift) return;
_appService.ShiftService.RemoveShift(selectedShift);
ShiftListChanged(sender, e);
};
}
private void ButtonAddPersonToShift_Click(object? sender, RoutedEventArgs e)
{
if (ListViewShifts.SelectedItem is Shift selectedShift)
{
ViewService.ShowShiftPersonSelector(this, _appService, selectedShift,
_appService.PersonService.GetPersonList());
}
}
private void ButtonOpenShiftPlan_Click(object? sender, RoutedEventArgs e)
{
if (ListViewShiftPlans.SelectedItem is ShiftPlan selectedShiftPlan)
{
ViewService.ShowShiftPlanViewer(this, _appService, selectedShiftPlan);
}
}
private async void ButtonGenerateTimesheets_OnClick(object? sender, RoutedEventArgs e)
{
try
{
await _appService.TimesheetService.GenerateTimeSheets();
int error = TimesheetIO.ExportTimesheets(_appService.PersonService.GetPersonList().ToList());
if (error == 1) ;
}
catch (Exception exception)
{
throw; // TODO handle exception
}
}
private void ButtonFilterShiftPlans_Click(object? sender, RoutedEventArgs e)
{
ViewService.ShowFilterSelector(this, _appService);
}
private void ButtonResetAllFilters_Click(object? sender, RoutedEventArgs e)
{
_appService.ShiftPlanService.ResetShiftPlanList();
_appService.FilterService.ResetAllFilters();
StackPanelFilterList.Children.Clear();
_filters.Clear();
}
private void ButtonSaveShiftPlans_Click(object? sender, RoutedEventArgs e)
{
foreach (ShiftPlan shiftPlan in _appService.ShiftPlanService.GetShiftPlans())
ShiftPlanIO.ExportShiftPlan(ShiftPlanIO.GenerateShiftPlanPng(shiftPlan),
_appService.ShiftPlanService.GetNextID());
}
private void ButtonStartCalculation_Click(object? sender, RoutedEventArgs e)
{
if (_calcRunning)
{
_calcRunning = false;
ViewModeCalcRunning();
ButtonStartCalculation.Content = "Start calculation";
ProgressbarCalculaion.IsIndeterminate = false;
_cts.Cancel();
}
else
{
StackPanelFilterList.Children.Clear();
_filters.Clear();
_appService.ShiftPlanService.RestartShiftPlanList();
_cts = new CancellationTokenSource();
_calcRunning = true;
ViewModeCalcRunning();
_cancelCalculation = _cts.Token;
ButtonStartCalculation.Content = "Stop calculation";
ProgressbarCalculaion.IsIndeterminate = true;
Task calculation = new Task(() => { _appService.ShiftPlanService.StartCalculation(_cancelCalculation); });
calculation.Start();
calculation.GetAwaiter().OnCompleted(() =>
{
_calcRunning = false;
ViewModeCalcRunning();
ProgressbarCalculaion.IsIndeterminate = false;
ButtonStartCalculation.Content = "Start calculation";
_cts.Dispose();
});
}
}
private void Window_Closing(object? sender, WindowClosingEventArgs e)
{
SaveDataToFile.SavePersons(_appService.PersonService.GetPersonList().ToList());
SaveDataToFile.SaveShifts(_appService.ShiftService.GetShiftList().ToList());
SaveDataToFile.SavePersonShiftConnections(_appService.PersonService.GetPersonList().ToList());
}
private void TabControlRoot_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0) return;
if (TabItemPersons == e.AddedItems[0] && !_calcRunning)
{
ViewModePersons();
}
else if (TabItemShifts == e.AddedItems[0] && !_calcRunning)
{
ViewModeShifts();
}
else if (TabItemShiftPlans == e.AddedItems[0])
{
ViewModeShiftPlans();
}
}
// ------------------------------------------------------Helper------------------------------------------------------
private void PersonListChanged(object? sender, EventArgs e)
{
}
private void ShiftListChanged(object? sender, EventArgs e)
{
}
private void ShiftPlansChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e)
{
case { Action: NotifyCollectionChangedAction.Add, NewItems: not null }:
_dispatcher.InvokeAsync(() =>
{
foreach (object item in e.NewItems)
{
ListViewShiftPlans.Items.Add(item);
}
}, DispatcherPriority.Background);
break;
case { Action: NotifyCollectionChangedAction.Remove, OldItems: not null }:
_dispatcher.InvokeAsync(() =>
{
foreach (object item in e.OldItems)
{
ListViewShiftPlans.Items.Remove(item);
}
});
break;
}
}
private void FilterListChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e)
{
case { Action: NotifyCollectionChangedAction.Add, NewItems: not null }:
{
foreach (Filter filter in e.NewItems)
{
_filters[filter] = new FilterElement(_appService, filter);
StackPanelFilterList.Children.Add(_filters[filter]);
}
break;
}
case { Action: NotifyCollectionChangedAction.Remove, OldItems: not null }:
{
foreach (Filter filter in e.OldItems)
{
StackPanelFilterList.Children.Remove(_filters[filter]);
}
break;
}
}
}
private void LoadData()
{
List<Person> persons = _appService.PersonService.GetPersonList().ToList();
List<Shift> shifts = _appService.ShiftService.GetShiftList().ToList();
LoadDataFromFile.LoadPersonShiftConnections(persons, shifts, _appService);
}
private void DeactivateView()
{
ButtonAddPerson.IsEnabled = false;
ButtonEditPerson.IsEnabled = false;
ButtonDeletePerson.IsEnabled = false;
ButtonAddShiftToPerson.IsEnabled = false;
ButtonOpenShiftPlan.IsEnabled = false;
ButtonSaveShiftPlans.IsEnabled = false;
ButtonFilterShiftPlans.IsEnabled = false;
ButtonResetAllFilters.IsEnabled = false;
ButtonAddShift.IsEnabled = false;
ButtonEditShift.IsEnabled = false;
ButtonDeleteShift.IsEnabled = false;
ButtonAddPersonToShift.IsEnabled = false;
}
private void ViewModePersons()
{
DeactivateView();
ButtonAddPerson.IsEnabled = true;
ButtonEditPerson.IsEnabled = true;
ButtonDeletePerson.IsEnabled = true;
ButtonAddShiftToPerson.IsEnabled = true;
}
private void ViewModeShifts()
{
DeactivateView();
ButtonAddShift.IsEnabled = true;
ButtonEditShift.IsEnabled = true;
ButtonDeleteShift.IsEnabled = true;
ButtonAddPersonToShift.IsEnabled = true;
}
private void ViewModeShiftPlans()
{
if (_calcRunning) return;
DeactivateView();
ButtonOpenShiftPlan.IsEnabled = true;
ButtonSaveShiftPlans.IsEnabled = true;
ButtonFilterShiftPlans.IsEnabled = true;
ButtonResetAllFilters.IsEnabled = true;
}
private void ViewModeCalcRunning()
{
if (_calcRunning)
{
ButtonOpenShiftPlan.IsEnabled = false;
ButtonSaveShiftPlans.IsEnabled = false;
ButtonFilterShiftPlans.IsEnabled = false;
ButtonResetAllFilters.IsEnabled = false;
}
else
{
ButtonOpenShiftPlan.IsEnabled = true;
ButtonSaveShiftPlans.IsEnabled = true;
ButtonFilterShiftPlans.IsEnabled = true;
ButtonResetAllFilters.IsEnabled = true;
}
}
}

View File

@@ -0,0 +1,25 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.PersonView.PersonCreator"
Title="PersonCreator" Height="260" Width="300" WindowStartupLocation="CenterOwner" CanResize="False">
<Grid>
<Label Content="Create new person" Height="25" VerticalAlignment="Top" Margin="0,15,0,0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
<Grid ColumnDefinitions="Auto, Auto" RowDefinitions="Auto, Auto, Auto" HorizontalAlignment="Center" VerticalAlignment="Center">
<Label Grid.Column="0" Grid.Row="0" Content="Enter person name: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" x:Name="TextBoxName" Height="32" Width="100" HorizontalContentAlignment="Center"/>
<Label Grid.Column="0" Grid.Row="1" Content="Enter person surname: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" x:Name="TextBoxSurname" Height="32" Width="100" HorizontalContentAlignment="Center"/>
<Label Grid.Column="0" Grid.Row="2" Content="Enter person hours: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" x:Name="TextBoxHours" Height="32" Width="100" HorizontalContentAlignment="Center" TextInput="TextBoxHours_PreviewTextInput" />
</Grid>
<Button x:Name="ButtonCreatePerson" Content="Create new person" HorizontalContentAlignment="Center" Height="32" Width="125" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,15" Click="ButtonCreatePerson_Click"/>
</Grid>
</Window>

View File

@@ -0,0 +1,38 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Service;
using LeanderShiftPlannerV2.Util;
namespace LeanderShiftPlannerV2.View.PersonView;
public partial class PersonCreator : Window
{
private readonly AppService _appService;
public PersonCreator(AppService appService)
{
InitializeComponent();
this._appService = appService;
}
private void ButtonCreatePerson_Click(object sender, RoutedEventArgs e)
{
if (TextBoxName.Text != "" && TextBoxHours.Text != "" && TextBoxSurname.Text != ""
&& TextBoxName.Text != null && TextBoxHours.Text != null && TextBoxSurname.Text != null)
{
_appService.PersonService.AddNewPerson(TextBoxName.Text, TextBoxSurname.Text, TextBoxHours.Text);
this.Close();
}
else
{
ViewService.ShowNoInputError(this);
}
}
private void TextBoxHours_PreviewTextInput(object sender, TextInputEventArgs e)
{
if (e.Text != null) e.Handled = ViewUtil.CheckNumericText(e.Text);
}
}

View File

@@ -0,0 +1,25 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.PersonView.PersonEditor"
Title="PersonEditor" Height="260" Width="300" WindowStartupLocation="CenterOwner" CanResize="False">
<Grid>
<Label Content="Edit person" Height="25" VerticalAlignment="Top" Margin="0,15,0,0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
<Grid ColumnDefinitions="Auto, Auto" RowDefinitions="Auto, Auto, Auto" HorizontalAlignment="Center" VerticalAlignment="Center">
<Label Grid.Column="0" Grid.Row="0" Content="Enter person name: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="0" x:Name="TextBoxName" Height="32" Width="100" HorizontalContentAlignment="Center"/>
<Label Grid.Column="0" Grid.Row="1" Content="Enter person surname: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" x:Name="TextBoxSurname" Height="32" Width="100" HorizontalContentAlignment="Center"/>
<Label Grid.Column="0" Grid.Row="2" Content="Enter person hours: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" x:Name="TextBoxHours" Height="32" Width="100" HorizontalContentAlignment="Center" TextInput="TextBoxHours_PreviewTextInput" />
</Grid>
<Button x:Name="ButtonEditPerson" Content="Edit person" HorizontalContentAlignment="Center" Height="32" Width="125" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,15" Click="ButtonEditPerson_Click" />
</Grid>
</Window>

View File

@@ -0,0 +1,45 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
using LeanderShiftPlannerV2.Util;
namespace LeanderShiftPlannerV2.View.PersonView;
public partial class PersonEditor : Window
{
private readonly AppService _appService;
private readonly Person _model;
public PersonEditor(AppService appService, Person person)
{
InitializeComponent();
this._model = person;
this._appService = appService;
TextBoxName.Text = _model.FirstName;
TextBoxSurname.Text = _model.Surname;
TextBoxHours.Text = _model.Hours.ToString();
}
private void ButtonEditPerson_Click(object sender, RoutedEventArgs e)
{
if (TextBoxName.Text != "" && TextBoxHours.Text != "" && TextBoxSurname.Text != ""
&& TextBoxName.Text != null && TextBoxHours.Text != null && TextBoxSurname.Text != null)
{
_appService.PersonService.EditPerson(_model, TextBoxName.Text, TextBoxSurname.Text, int.Parse(TextBoxHours.Text));
this.Close();
}
else
{
//TODO: Error, no input!
}
}
private void TextBoxHours_PreviewTextInput(object sender, TextInputEventArgs e)
{
if (e.Text != null) e.Handled = ViewUtil.CheckNumericText(e.Text);
}
}

View File

@@ -0,0 +1,21 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:leanderControls="clr-namespace:LeanderShiftPlannerV2.View.LeanderControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.PersonView.PersonShiftSelector"
Title="PersonShiftSelector" Height="175" Width="350" WindowStartupLocation="CenterOwner" CanResize="False">
<Grid>
<Label Content="Add shift(s) to person" Height="25" VerticalAlignment="Top" Margin="0,15,0,0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
<Grid ColumnDefinitions="Auto, Auto" RowDefinitions="Auto" HorizontalAlignment="Center" VerticalAlignment="Center">
<Label Grid.Column="0" Grid.Row="0" Content="Select shift(s): " Height="25" HorizontalContentAlignment="Right" />
<leanderControls:CheckComboBox Grid.Column="1" Grid.Row="0" x:Name="CheckComboBoxShiftSelector" Height="32" Width="100" />
</Grid>
<Button x:Name="ButtonAddShiftsToPerson" Content="Add shift(s) to person" Height="32" Width="125" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,15" Click="ButtonAddShiftsToPerson_Click"/>
</Grid>
</Window>

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
namespace LeanderShiftPlannerV2.View.PersonView;
public partial class PersonShiftSelector : Window
{
private readonly AppService _appService;
private readonly Person _model;
public PersonShiftSelector(AppService appService, Person person, ObservableCollection<Shift> shifts)
{
InitializeComponent();
this._model = person;
this._appService = appService;
CheckComboBoxShiftSelector.ItemsSource = shifts;
}
private void ButtonAddShiftsToPerson_Click(object sender, RoutedEventArgs e)
{
List<Shift> newShifts = new List<Shift>();
foreach (Shift shift in CheckComboBoxShiftSelector.SelectedItems)
{
newShifts.Add(shift);
}
_appService.PersonService.SetShiftsForPerson(_model, newShifts);
this.Close();
}
}

View File

@@ -0,0 +1,22 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.ShiftPlanView.ShiftPlanViewer"
Title="ShiftPlanViewer" Height="800" Width="1000" WindowStartupLocation="CenterOwner">
<Grid>
<TabControl>
<TabItem Header="ListView">
<ListBox x:Name="ListViewShifts" Margin="0,0,0,100"/>
</TabItem>
<TabItem Header="ImageView">
<Image x:Name="ImageView" Margin="0,0,0,100"/>
</TabItem>
</TabControl>
<Grid MinHeight="100" MaxHeight="100" VerticalAlignment="Bottom">
<Button x:Name="ButtonSaveShiftPlan" Content="Save shiftPlan" HorizontalContentAlignment="Center" Height="32" Width="125" VerticalAlignment="Bottom" Margin="0,0,80,30" HorizontalAlignment="Right" Click="ButtonSaveShiftPlan_Click"/>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,44 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.FileIO;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
using SkiaSharp;
namespace LeanderShiftPlannerV2.View.ShiftPlanView;
public partial class ShiftPlanViewer : Window
{
private readonly AppService _appService;
private readonly ShiftPlan _model;
private readonly SKImage _image;
public ShiftPlanViewer(AppService appService, ShiftPlan shiftPlan)
{
InitializeComponent();
this._appService = appService;
this._model = shiftPlan;
this._image = ShiftPlanIO.GenerateShiftPlanPng(shiftPlan);
InitListView();
InitImageView();
}
private void InitListView()
{
string[] shiftPlanSplitter = _model.ToString().Split("; ");
ListViewShifts.ItemsSource = shiftPlanSplitter;
}
private void InitImageView()
{
ImageView.Source = ShiftPlanIO.ConvertToIImage(_image);
}
private void ButtonSaveShiftPlan_Click(object sender, RoutedEventArgs e)
{
ShiftPlanIO.ExportShiftPlan(_image, _appService.ShiftPlanService.GetNextID());
}
}

View File

@@ -0,0 +1,54 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.ShiftView.ShiftCreator"
Title="ShiftCreator" Height="350" Width="450" WindowStartupLocation="CenterOwner" CanResize="False">
<Grid>
<Label Content="Create new shift" Height="25" VerticalAlignment="Top" Margin="0,10,0,0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,Auto" Margin="0, 50, 0, 0" HorizontalAlignment="Center" VerticalAlignment="Top">
<Label Grid.Column="0" Grid.Row="0" Content="Enter shift day: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<ComboBox Grid.Column="1" Grid.Row="0" x:Name="ComboBoxDay" HorizontalAlignment="Center" Height="32" Width="125" HorizontalContentAlignment="Center">
<ComboBoxItem Content="Monday" />
<ComboBoxItem Content="Tuesday" />
<ComboBoxItem Content="Wednesday" />
<ComboBoxItem Content="Thursday" />
<ComboBoxItem Content="Friday" />
</ComboBox>
<Label Grid.Column="0" Grid.Row="1" Content="Enter shift number: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<ComboBox Grid.Column="1" Grid.Row="1" x:Name="ComboBoxShiftNumber" HorizontalAlignment="Center" Height="32" Width="125" HorizontalContentAlignment="Center" SelectionChanged="AutoFill">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
</ComboBox>
<Label Grid.Column="0" Grid.Row="2" Content="Select shift start: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TimePicker Grid.Column="1" Grid.Row="2" x:Name="TimePickerStartTime" HorizontalAlignment="Center" Height="32" ClockIdentifier="24HourClock" MinuteIncrement="30" />
<Label Grid.Column="0" Grid.Row="3" Content="Select shift end: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TimePicker Grid.Column="1" Grid.Row="3" x:Name="TimePickerEndTime" HorizontalAlignment="Center" Height="32" ClockIdentifier="24HourClock" MinuteIncrement="30" />
</Grid>
<Label Content="Enter person counts" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0, 0 ,0, 120"/>
<Grid RowDefinitions="Auto" ColumnDefinitions="Auto,Auto, Auto, Auto" Margin="0. 0,0, 75" HorizontalAlignment="Center" VerticalAlignment="Bottom">
<Label Grid.Column="0" Grid.Row="0" Content="Min" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<ComboBox Grid.Column="1" Grid.Row="0" x:Name="ComboBoxMinCount" Height="32" Width="75">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
</ComboBox>
<ComboBox Grid.Column="2" Grid.Row="0" x:Name="ComboBoxMaxCount" Height="32" Width="75">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
</ComboBox>
<Label Grid.Column="3" Grid.Row="0" Content="Max" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
</Grid>
<Button x:Name="ButtonCreateShift" Content="Create new shift" Height="32" Width="125" HorizontalContentAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,15" Click="ButtonCreateShift_Click"/>
</Grid>
</Window>

View File

@@ -0,0 +1,93 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Service;
namespace LeanderShiftPlannerV2.View.ShiftView;
public partial class ShiftCreator : Window
{
private readonly AppService _appService;
private readonly DateTime _today = DateTime.Today;
private bool _returnBool = false;
public ShiftCreator(AppService appService)
{
InitializeComponent();
this._appService = appService;
}
private void ButtonCreateShift_Click(object sender, RoutedEventArgs e)
{
if (TimePickerStartTime.SelectedTime == null) _returnBool = true;
if (TimePickerEndTime.SelectedTime == null) _returnBool = true;
if (ComboBoxDay.SelectedItem == null || ComboBoxDay.SelectedItem.ToString() == "") _returnBool = true;
if (ComboBoxShiftNumber.SelectedItem == null || ComboBoxShiftNumber.SelectedItem.ToString() == "")
_returnBool = true;
if (ComboBoxMinCount.SelectedItem == null || ComboBoxMinCount.SelectedItem.ToString() == "") _returnBool = true;
if (ComboBoxMaxCount.SelectedItem == null || ComboBoxMaxCount.SelectedItem.ToString() == "") _returnBool = true;
if (_returnBool)
{
ViewService.ShowNoInputError(this);
_returnBool = false;
return;
}
if (TimePickerStartTime.SelectedTime != null && TimePickerEndTime.SelectedTime != null
&& ComboBoxDay.SelectedItem != null && ComboBoxShiftNumber.SelectedItem != null
&& ComboBoxMaxCount.SelectedItem != null && ComboBoxMinCount.SelectedItem != null)
{
DateTime startTime = new DateTime(_today.Year, _today.Month, _today.Day,
TimePickerStartTime.SelectedTime.Value.Hours, TimePickerStartTime.SelectedTime.Value.Minutes,
_today.Second);
DateTime endTime = new DateTime(_today.Year, _today.Month, _today.Day,
TimePickerEndTime.SelectedTime.Value.Hours, TimePickerEndTime.SelectedTime.Value.Minutes, _today.Second);
string day = ((ComboBoxItem) ComboBoxDay.SelectedItem).Content as string ?? "NAME";
int shiftNumber = int.Parse(((ComboBoxItem) ComboBoxShiftNumber.SelectedItem).Content as string ?? "-1");
int maxPersonCount = int.Parse(((ComboBoxItem) ComboBoxMaxCount.SelectedItem).Content as string ?? "-1");
int minPersonCount = int.Parse(((ComboBoxItem) ComboBoxMinCount.SelectedItem).Content as string ?? "-1");
_appService.ShiftService.AddShift(day, startTime, endTime, shiftNumber, maxPersonCount, minPersonCount);
}
else
{
ViewService.ShowNoInputError(this);
}
this.Close();
}
private void AutoFill(object sender, SelectionChangedEventArgs e)
{
if (ComboBoxShiftNumber.SelectedItem is not ComboBoxItem input) return;
if (input.Content is not string inputText) return;
if (inputText.Contains('1'))
{
ComboBoxMinCount.SelectedIndex = 1;
ComboBoxMaxCount.SelectedIndex = 2;
TimePickerStartTime.SelectedTime = new TimeSpan(0, 9, 0, 0);
TimePickerEndTime.SelectedTime = new TimeSpan(0, 13, 0, 0);
}
else if (inputText.Contains('2'))
{
ComboBoxMinCount.SelectedIndex = 1;
ComboBoxMaxCount.SelectedIndex = 1;
TimePickerStartTime.SelectedTime = new TimeSpan(0, 14, 0, 0);
TimePickerEndTime.SelectedTime = new TimeSpan(0, 16, 0, 0);
}
else if (inputText.Contains('3'))
{
ComboBoxMinCount.SelectedIndex = 0;
ComboBoxMaxCount.SelectedIndex = 1;
TimePickerStartTime.SelectedTime = new TimeSpan(0, 16, 0, 0);
TimePickerEndTime.SelectedTime = new TimeSpan(0, 18, 0, 0);
}
}
}

View File

@@ -0,0 +1,54 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.ShiftView.ShiftEditor"
Title="ShiftEditor" Height="350" Width="450" WindowStartupLocation="CenterOwner" CanResize="False">
<Grid>
<Label Content="Edit shift" Height="25" VerticalAlignment="Top" Margin="0,10,0,0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" />
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,Auto" Margin="0, 50, 0, 0" HorizontalAlignment="Center" VerticalAlignment="Top">
<Label Grid.Column="0" Grid.Row="0" Content="Enter shift day: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<ComboBox Grid.Column="1" Grid.Row="0" x:Name="ComboBoxDay" HorizontalAlignment="Center" Height="32" Width="125" HorizontalContentAlignment="Center">
<ComboBoxItem Content="Monday" />
<ComboBoxItem Content="Tuesday" />
<ComboBoxItem Content="Wednesday" />
<ComboBoxItem Content="Thursday" />
<ComboBoxItem Content="Friday" />
</ComboBox>
<Label Grid.Column="0" Grid.Row="1" Content="Enter shift number: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<ComboBox Grid.Column="1" Grid.Row="1" x:Name="ComboBoxShiftNumber" HorizontalAlignment="Center" Height="32" Width="125" HorizontalContentAlignment="Center">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
</ComboBox>
<Label Grid.Column="0" Grid.Row="2" Content="Select shift start: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TimePicker Grid.Column="1" Grid.Row="2" x:Name="TimePickerStartTime" HorizontalAlignment="Center" Height="32" ClockIdentifier="24HourClock" MinuteIncrement="30" />
<Label Grid.Column="0" Grid.Row="3" Content="Select shift end: " Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<TimePicker Grid.Column="1" Grid.Row="3" x:Name="TimePickerEndTime" HorizontalAlignment="Center" Height="32" ClockIdentifier="24HourClock" MinuteIncrement="30" />
</Grid>
<Label Content="Enter person counts" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0, 0 ,0, 120"/>
<Grid RowDefinitions="Auto" ColumnDefinitions="Auto,Auto, Auto, Auto" Margin="0. 0,0, 75" HorizontalAlignment="Center" VerticalAlignment="Bottom">
<Label Grid.Column="0" Grid.Row="0" Content="Min" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
<ComboBox Grid.Column="1" Grid.Row="0" x:Name="ComboBoxMinCount" Height="32" Width="75">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
</ComboBox>
<ComboBox Grid.Column="2" Grid.Row="0" x:Name="ComboBoxMaxCount" Height="32" Width="75">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
</ComboBox>
<Label Grid.Column="3" Grid.Row="0" Content="Max" Height="25" HorizontalContentAlignment="Right" VerticalContentAlignment="Center" />
</Grid>
<Button x:Name="ButtonEditShift" Content="Edit shift" Height="32" Width="125" HorizontalContentAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,15" Click="ButtonEditShift_Click" />
</Grid>
</Window>

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
namespace LeanderShiftPlannerV2.View.ShiftView;
public partial class ShiftEditor : Window
{
private readonly Dictionary<string, int> _days = new Dictionary<string, int>()
{
{ "Monday", 0 },
{ "Tuesday", 1 },
{ "Wednesday", 2 },
{ "Thursday", 3 },
{ "Friday", 4 }
};
private readonly AppService _appService;
private readonly Shift _model;
private readonly DateTime _today = DateTime.Today;
private bool _returnBool = false;
public ShiftEditor(AppService appService, Shift shift)
{
InitializeComponent();
this._model = shift;
this._appService = appService;
foreach (ComboBoxItem? comboBoxItem in ComboBoxDay.Items)
{
if (comboBoxItem.Content.ToString() == _model.Day) ComboBoxDay.SelectedIndex = ComboBoxDay.Items.IndexOf(comboBoxItem);
}
foreach (ComboBoxItem? comboBoxItem in ComboBoxShiftNumber.Items)
{
if (comboBoxItem.Content.ToString() == _model.ShiftInDay.ToString()) ComboBoxShiftNumber.SelectedIndex = ComboBoxShiftNumber.Items.IndexOf(comboBoxItem);
}
foreach (ComboBoxItem? comboBoxItem in ComboBoxMinCount.Items)
{
if (comboBoxItem.Content.ToString() == _model.MinPersonCount.ToString()) ComboBoxMinCount.SelectedIndex = ComboBoxMinCount.Items.IndexOf(comboBoxItem);
}
foreach (ComboBoxItem? comboBoxItem in ComboBoxMaxCount.Items)
{
if (comboBoxItem.Content.ToString() == _model.MaxPersonCount.ToString()) ComboBoxMaxCount.SelectedIndex = ComboBoxMaxCount.Items.IndexOf(comboBoxItem);
}
string[] startTimeParts = _model.StartTime.Split(':');
string[] endTimeParts = _model.EndTime.Split(':');
TimePickerStartTime.SelectedTime = new TimeSpan(int.Parse(startTimeParts[0]), int.Parse(startTimeParts[1]), 0);
TimePickerEndTime.SelectedTime = new TimeSpan(int.Parse(endTimeParts[0]), int.Parse(endTimeParts[1]), 0);
}
private void ButtonEditShift_Click(object sender, RoutedEventArgs e)
{
if (TimePickerStartTime.SelectedTime == null) _returnBool = true;
if (TimePickerEndTime.SelectedTime == null) _returnBool = true;
if (ComboBoxDay.SelectedItem == null || ComboBoxDay.SelectedItem.ToString() == "") _returnBool = true;
if (ComboBoxShiftNumber.SelectedItem == null || ComboBoxShiftNumber.SelectedItem.ToString() == "")
_returnBool = true;
if (ComboBoxMinCount.SelectedItem == null || ComboBoxMinCount.SelectedItem.ToString() == "") _returnBool = true;
if (ComboBoxMaxCount.SelectedItem == null || ComboBoxMaxCount.SelectedItem.ToString() == "") _returnBool = true;
if (_returnBool)
{
ViewService.ShowNoInputError(this);
_returnBool = false;
return;
}
if (TimePickerStartTime.SelectedTime != null && TimePickerEndTime.SelectedTime != null
&& ComboBoxDay.SelectedItem != null && ComboBoxShiftNumber.SelectedItem != null
&& ComboBoxMaxCount.SelectedItem != null && ComboBoxMinCount.SelectedItem != null)
{
DateTime startTime = new DateTime(_today.Year, _today.Month, _today.Day,
TimePickerStartTime.SelectedTime.Value.Hours, TimePickerStartTime.SelectedTime.Value.Minutes,
_today.Second);
DateTime endTime = new DateTime(_today.Year, _today.Month, _today.Day,
TimePickerEndTime.SelectedTime.Value.Hours, TimePickerEndTime.SelectedTime.Value.Minutes, _today.Second);
_appService.ShiftService.EditShift(_model, ((ComboBoxItem) ComboBoxDay.SelectedItem).Content.ToString(), startTime, endTime,
int.Parse(((ComboBoxItem) ComboBoxShiftNumber.SelectedItem).Content.ToString()), int.Parse(((ComboBoxItem) ComboBoxMaxCount.SelectedItem).Content.ToString()),
int.Parse(((ComboBoxItem) ComboBoxMinCount.SelectedItem).Content.ToString()));
}
else
{
// TODO Error!
}
this.Close();
}
}

View File

@@ -0,0 +1,23 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:leanderControls="clr-namespace:LeanderShiftPlannerV2.View.LeanderControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="LeanderShiftPlannerV2.View.ShiftView.ShiftPersonSelector"
Title="ShiftPersonSelector" Height="175" Width="350" WindowStartupLocation="CenterOwner" CanResize="False">
<Grid>
<Label Content="Add person(s) to shift" Height="25" VerticalAlignment="Top" Margin="0,15,0,0"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center" />
<Grid ColumnDefinitions="Auto, Auto" RowDefinitions="Auto" HorizontalAlignment="Center" VerticalAlignment="Center">
<Label Grid.Column="0" Grid.Row="0" Content="Select person(s): " Height="25" HorizontalContentAlignment="Right" />
<leanderControls:CheckComboBox Grid.Column="1" Grid.Row="0" x:Name="CheckComboBoxPersonSelector" Height="32" Width="100" />
</Grid>
<Button x:Name="ButtonAddPersonsToShift" Content="Add person(s) to shift" Height="32" Width="175"
HorizontalContentAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Margin="0,0,0,15" Click="ButtonAddPersonsToShift_Click" />
</Grid>
</Window>

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Interactivity;
using LeanderShiftPlannerV2.Model;
using LeanderShiftPlannerV2.Service;
namespace LeanderShiftPlannerV2.View.ShiftView;
public partial class ShiftPersonSelector : Window
{
private readonly AppService _appService;
private readonly Shift _model;
public ShiftPersonSelector(AppService appService, Shift shift, ObservableCollection<Person> persons)
{
InitializeComponent();
this._model = shift;
this._appService = appService;
CheckComboBoxPersonSelector.ItemsSource = persons;
}
private void ButtonAddPersonsToShift_Click(object sender, RoutedEventArgs e)
{
List<Person> persons = new List<Person>();
foreach (Person person in CheckComboBoxPersonSelector.SelectedItems)
{
persons.Add(person);
}
_appService.ShiftService.SetPeronssForShift(_model, persons);
this.Close();
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="LeanderShiftPlannerV2.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

46
README.md Normal file
View File

@@ -0,0 +1,46 @@
# LeanderShiftPlanner
## Einleitung
LeanderShiftPlanner ist ein Tool zur automatisierten Erstellung von Schichtplänen. Es ermöglicht das Anlegen von Personen und Schichten, deren Verknüpfung sowie die Berechnung und Filterung von Schichtplänen. Zusätzlich können Stundenzettel automatisch generiert werden.
#### Alle Stundenangaben müssen im Wochenstunden-Format erfolgen!
# Hauptfunktionen
## 1. Personen und Schichten verwalten
Personen hinzufügen: Klicke auf den Tab „Persons“ und nutze die Schaltfläche „Add new person“, um eine neue Person zu erstellen.
Personen bearbeiten/löschen: Über die Buttons „Edit person“ und „Delete person“ kannst du bestehende Einträge ändern oder entfernen.
Schichten hinzufügen: Wechsle in den Tab „Shifts“ und klicke auf „Add new shift“, um eine neue Schicht zu erstellen.
Schichten bearbeiten/löschen: Ändere oder entferne Schichten mit „Edit shift“ und „Delete shift“.
## 2. Personen und Schichten zuweisen
Person einer Schicht zuweisen: Wähle eine Schicht aus und klicke auf „Select persons for shift“.
Schicht einer Person zuweisen: Wähle eine Person aus und nutze „Select shifts for person“.
_Die Verlinkung von Personen und Schichten wird automatisch in beide Richtungen durchgeführt. Eine Schicht, welche per "Select shifts for person" einer Person hinzugefügt wurde, erhält also automatisch auch einen Link zu der Person, ohne dass dies per "Select persons for shift" noch einmal extra erfolgen muss!_
## 3. Schichtpläne berechnen und filtern
Schichtpläne generieren: Drücke „Start calculation“, um eine automatische Planung der Schichten basierend auf den hinterlegten Personen und Schichtzeiten durchzuführen.
Schichtpläne filtern: Wechsle in den Tab „ShiftPlans“ und nutze „Filter shiftplans“, um die Ergebnisse nach bestimmten Kriterien einzugrenzen.
Filter zurücksetzen: Mit „Reset all filters“ werden alle angewendeten Filter entfernt.
_Die Berechnung der Schichtpläne kann je nach Parametern etwas Zeit in Anspruch nehmen. Hier also einfach etwas Geduld mitbringen._
## 4. Schichtpläne anzeigen und speichern
Schichtplan öffnen: Wähle einen erstellten Plan aus und nutze „Open selected ShiftPlan“, um ihn als PNG oder Text einzusehen.
Alle Schichtpläne speichern: Mit „Save all ShiftPlans“ werden alle gefilterten Pläne als PNG im Verzeichnis `Data/Output/ShiftPlans` gespeichert.
## 5. Stundenzettel generieren
Automatisch Stundenzettel erstellen: Über „Generate Timesheets“ können alle Stundenzettel automatisch generiert und im Ordner `Data/Output/Timesheets` abgelegt werden.