├── .gitignore ├── LICENSE ├── README.md ├── Sample.jpg ├── TimeSheet.Tests ├── DepartmentStorageTests.cs ├── LogTests.cs ├── ReportCreatorTests.cs ├── Resource.Designer.cs ├── Resource.resx ├── Resources │ └── department.json ├── TimeSheet.Tests.csproj ├── Usings.cs └── WeekendsProviderTests.cs ├── TimeSheet.sln └── TimeSheet ├── Application ├── Interfaces │ ├── ICommandsHandler.cs │ ├── IDepartmentProvider.cs │ ├── ILogWrapper.cs │ └── IWeekendsProvider.cs ├── Logger │ ├── ConsoleWrapper.cs │ ├── Log.cs │ └── NLogWrapper.cs ├── ProgramCore.cs ├── Structs │ └── ConsoleCommand.cs └── Utils │ └── FileUtils.cs ├── Data ├── AnnualWeekendsInfo.cs ├── DepartmentInfo.cs └── Employee.cs ├── Domain ├── DepartmentStorage.cs ├── Providers │ ├── ConsultantWeekendsProvider.cs │ └── DepartmentFileProvider.cs ├── ReportCreator.cs └── WeekendsStorage.cs ├── Program.cs ├── Resource.Designer.cs ├── Resource.resx ├── Settings.Designer.cs ├── Settings.settings └── TimeSheet.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 kulikov-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### TimeSheet 2 | A small console utility that allows you to automate the creation of timesheets for departments for any Date. Using the Consultant Plus data provider to receive public weekends/holidays. 3 |

4 | -------------------------------------------------------------------------------- /Sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kulikov-dev/TimeSheet/e8edd63468e48b64df54c3f1b62d3660f0928285/Sample.jpg -------------------------------------------------------------------------------- /TimeSheet.Tests/DepartmentStorageTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Castle.Components.DictionaryAdapter; 3 | using Moq; 4 | using TimeSheet.Application.Interfaces; 5 | using TimeSheet.Data; 6 | using TimeSheet.Domain; 7 | 8 | namespace TimeSheet.Tests 9 | { 10 | /// 11 | /// Department info tests 12 | /// 13 | public class DepartmentStorageTests 14 | { 15 | /// 16 | /// Mock department provider 17 | /// 18 | private readonly Mock _mockProvider; 19 | 20 | /// 21 | /// Expected department info 22 | /// 23 | private readonly DepartmentInfo _expectedDepartmentInfo = new() 24 | { 25 | DepartmentTitle = "Отдел разработки ПО", 26 | LeaderName = "Иванов И.И.", 27 | LeaderPosition = "Начальник отдела", 28 | Employees = new EditableList() 29 | { 30 | new(fullName: "Петров П.П.", workHours: 8), 31 | new(fullName: "Васечкин В.В.", workHours: 6) 32 | } 33 | }; 34 | 35 | /// 36 | /// Setup 37 | /// 38 | public DepartmentStorageTests() 39 | { 40 | _mockProvider = new Mock(); 41 | _mockProvider.Setup(item => item.Load()).Returns(Task.FromResult(_expectedDepartmentInfo)!); 42 | } 43 | 44 | [Fact] 45 | public async void TestProvider() 46 | { 47 | var result = await _mockProvider.Object.Load(); 48 | Assert.NotNull(result); 49 | Assert.Equal(_expectedDepartmentInfo, result); 50 | } 51 | 52 | [Fact] 53 | public async void TestLoading() 54 | { 55 | await using var fileStream = new MemoryStream(Resource.department); 56 | var result = await JsonSerializer.DeserializeAsync(fileStream); 57 | 58 | Assert.NotNull(result); 59 | Assert.Equal(_expectedDepartmentInfo, result); 60 | } 61 | 62 | [Fact] 63 | public async void TestStorage() 64 | { 65 | var storage = new DepartmentStorage(_mockProvider.Object); 66 | await storage.Load(); 67 | 68 | Assert.NotNull(storage); 69 | var index = 0; 70 | foreach (var employee in storage) 71 | { 72 | Assert.True(employee.Equals(_expectedDepartmentInfo.Employees[index])); 73 | ++index; 74 | } 75 | 76 | Assert.True(storage.HasEmployees); 77 | Assert.Equal(_expectedDepartmentInfo.Employees.Count, storage.EmployeesCount); 78 | Assert.Equal(_expectedDepartmentInfo.LeaderName, storage.LeaderName); 79 | Assert.Equal(_expectedDepartmentInfo.LeaderPosition, storage.LeaderPosition); 80 | Assert.Equal(_expectedDepartmentInfo.DepartmentTitle, storage.DepartmentTitle); 81 | } 82 | 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /TimeSheet.Tests/LogTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using TimeSheet.Application.Interfaces; 3 | using TimeSheet.Application.Logger; 4 | 5 | namespace TimeSheet.Tests 6 | { 7 | public class LogTests 8 | { 9 | [Fact] 10 | public void TestILogWrapper() 11 | { 12 | var mock = new Mock(); 13 | Log.Attach(mock.Object); 14 | 15 | Log.Info("Info message"); 16 | 17 | mock.Verify(objectItem => objectItem.Info(It.IsAny())); 18 | mock.Verify(objectItem => objectItem.Info("Info message")); 19 | mock.Verify(objectItem => objectItem.Info(It.IsAny()), Times.Once()); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /TimeSheet.Tests/ReportCreatorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Moq; 3 | using OfficeOpenXml; 4 | using TimeSheet.Application.Interfaces; 5 | using TimeSheet.Data; 6 | using TimeSheet.Domain; 7 | 8 | namespace TimeSheet.Tests 9 | { 10 | public class ReportCreatorTests 11 | { 12 | private readonly Mock _departmentProvider; 13 | private readonly Mock _weekendsProvider; 14 | private readonly DateTime _date = new(2021, 01, 01); 15 | 16 | public ReportCreatorTests() 17 | { 18 | _departmentProvider = new Mock(); 19 | _departmentProvider.Setup(item => item.Load()).Returns(async () => 20 | { 21 | await using var fileStream = new MemoryStream(Resource.department); 22 | return await JsonSerializer.DeserializeAsync(fileStream); 23 | }); 24 | 25 | var weekendsInfo = new AnnualWeekendsInfo(_date.Year); 26 | _weekendsProvider = new Mock(); 27 | _weekendsProvider.Setup(item => item.GetAnnualWeekends(_date)).Returns(Task.FromResult(weekendsInfo)); 28 | } 29 | 30 | [Fact] 31 | public async void CheckReport() 32 | { 33 | var resultFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tests", "result.xlsx"); 34 | var expectedFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tests", "expected.xlsx"); 35 | if (File.Exists(resultFilePath)) 36 | { 37 | File.Delete(resultFilePath); 38 | } 39 | 40 | if (!File.Exists(expectedFilePath)) 41 | { 42 | await File.WriteAllBytesAsync(expectedFilePath, Resource.expected); 43 | } 44 | 45 | await ReportCreator.Create(_date, _departmentProvider.Object, _weekendsProvider.Object, exportPath: resultFilePath); 46 | 47 | using var expectedExcel = new ExcelPackage(new FileInfo(expectedFilePath)); 48 | using var resultExcel = new ExcelPackage(new FileInfo(resultFilePath)); 49 | 50 | Assert.True(resultExcel.Workbook.Worksheets.Count > 0); 51 | 52 | var expectedWorksheet = expectedExcel.Workbook.Worksheets[0]; 53 | var resultWorksheet = resultExcel.Workbook.Worksheets[0]; 54 | 55 | Assert.Equal(expectedWorksheet.Name, resultWorksheet.Name); 56 | for (var i = expectedWorksheet.Dimension.Start.Row; i <= expectedWorksheet.Dimension.End.Row; ++i) 57 | { 58 | for (var j = expectedWorksheet.Dimension.Start.Column; j <= expectedWorksheet.Dimension.End.Column; ++j) 59 | { 60 | var expectedCell = expectedWorksheet.Cells[i, j]; 61 | var resultCell = resultWorksheet.Cells[i, j]; 62 | Assert.NotNull(resultCell); 63 | if (expectedCell.Value == null && resultCell.Value == null) 64 | { 65 | continue; 66 | } 67 | 68 | Assert.True(expectedCell.Value?.Equals(resultCell.Value), $"Cell doesn't match: {expectedCell.Address}"); 69 | } 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /TimeSheet.Tests/Resource.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TimeSheet.Tests { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resource { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resource() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TimeSheet.Tests.Resource", typeof(Resource).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Byte[]. 65 | /// 66 | internal static byte[] department { 67 | get { 68 | object obj = ResourceManager.GetObject("department", resourceCulture); 69 | return ((byte[])(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Byte[]. 75 | /// 76 | internal static byte[] expected { 77 | get { 78 | object obj = ResourceManager.GetObject("expected", resourceCulture); 79 | return ((byte[])(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /TimeSheet.Tests/Resource.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | Resources\department.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 123 | 124 | 125 | bin\Debug\net6.0\tests\expected.xlsx;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 126 | 127 | -------------------------------------------------------------------------------- /TimeSheet.Tests/Resources/department.json: -------------------------------------------------------------------------------- 1 | { 2 | "DepartmentTitle": "Отдел разработки ПО", 3 | "LeaderName": "Иванов И.И.", 4 | "LeaderPosition": "Начальник отдела", 5 | "Employees": [ 6 | { 7 | "FullName": "Петров П.П.", 8 | "WorkHours": 8 9 | }, 10 | { 11 | "FullName": "Васечкин В.В.", 12 | "WorkHours": 6 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /TimeSheet.Tests/TimeSheet.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | all 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | True 36 | True 37 | Resource.resx 38 | 39 | 40 | 41 | 42 | 43 | ResXFileCodeGenerator 44 | Resource.Designer.cs 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /TimeSheet.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /TimeSheet.Tests/WeekendsProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using TimeSheet.Application.Interfaces; 3 | using TimeSheet.Data; 4 | using TimeSheet.Domain.Providers; 5 | 6 | namespace TimeSheet.Tests 7 | { 8 | /// 9 | /// Tests for weekends providers 10 | /// 11 | public class WeekendsProviderTests 12 | { 13 | /// 14 | /// Report date 15 | /// 16 | private readonly DateTime _reportDate = new(2021, 01, 01); 17 | 18 | /// 19 | /// List of expected weekends for January 2021 20 | /// 21 | private readonly List _expectedJanWeeekends = new(30) { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 17, 23, 24, 30, 31 }; 22 | 23 | /// 24 | /// Mock weekends provider 25 | /// 26 | private readonly IWeekendsProvider _mockProvider; 27 | 28 | /// 29 | /// Setup 30 | /// 31 | public WeekendsProviderTests() 32 | { 33 | var moq = new Mock(); 34 | moq.Setup(item => item.GetAnnualWeekends(_reportDate)).Returns(() => 35 | { 36 | var result = Task.FromResult(new AnnualWeekendsInfo(_reportDate.Year)); 37 | result.Result.AddMonthWeekends(1, _expectedJanWeeekends); 38 | return result; 39 | }); 40 | 41 | _mockProvider = moq.Object; 42 | } 43 | 44 | [Fact] 45 | public async void TestMock() 46 | { 47 | Assert.NotNull(_mockProvider); 48 | 49 | var annualWeekends = await _mockProvider.GetAnnualWeekends(_reportDate); 50 | Assert.NotNull(annualWeekends); 51 | 52 | Assert.Equal(_reportDate.Year, annualWeekends.Year); 53 | 54 | var resultJanWeekends = annualWeekends.GetMonthWeekends(_reportDate); 55 | Assert.True(resultJanWeekends.SequenceEqual(_expectedJanWeeekends)); 56 | 57 | Assert.True(annualWeekends.GetMonthWeekends(new DateTime(2021, 02, 01)).Count == 0); 58 | } 59 | 60 | [Fact] 61 | public async void TestConsultantProvider() 62 | { 63 | var consultantResult = await new ConsultantWeekendsProvider().GetAnnualWeekends(_reportDate); 64 | var consultantJanWeekends = consultantResult.GetMonthWeekends(_reportDate); 65 | 66 | var expected = _mockProvider.GetAnnualWeekends(_reportDate); 67 | var expectedJanWeekends = expected.Result.GetMonthWeekends(_reportDate); 68 | 69 | Assert.True(expectedJanWeekends.SequenceEqual(consultantJanWeekends)); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /TimeSheet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32630.192 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimeSheet", "TimeSheet\TimeSheet.csproj", "{7FBDDC75-06F0-41EC-988D-D29D3FC4E173}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TimeSheet.Tests", "TimeSheet.Tests\TimeSheet.Tests.csproj", "{CF084C22-6931-4E4D-AB1B-01B06E7A499D}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {7FBDDC75-06F0-41EC-988D-D29D3FC4E173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {7FBDDC75-06F0-41EC-988D-D29D3FC4E173}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {7FBDDC75-06F0-41EC-988D-D29D3FC4E173}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {7FBDDC75-06F0-41EC-988D-D29D3FC4E173}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {CF084C22-6931-4E4D-AB1B-01B06E7A499D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {CF084C22-6931-4E4D-AB1B-01B06E7A499D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {CF084C22-6931-4E4D-AB1B-01B06E7A499D}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {CF084C22-6931-4E4D-AB1B-01B06E7A499D}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {CF8A0CE0-B94F-4C72-98CD-A40C10D4D059} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /TimeSheet/Application/Interfaces/ICommandsHandler.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Application.Structs; 2 | 3 | namespace TimeSheet.Application.Interfaces 4 | { 5 | /// 6 | /// Interface for console commands handler 7 | /// 8 | internal interface ICommandsHandler 9 | { 10 | /// 11 | /// Get console commands 12 | /// 13 | /// List of console commands 14 | List GetCommands(); 15 | 16 | /// 17 | /// Process command 18 | /// 19 | /// A command 20 | /// A command processed 21 | Task Process(string command); 22 | } 23 | } -------------------------------------------------------------------------------- /TimeSheet/Application/Interfaces/IDepartmentProvider.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Data; 2 | 3 | namespace TimeSheet.Application.Interfaces 4 | { 5 | /// 6 | /// Interface for department info data sources 7 | /// 8 | public interface IDepartmentProvider 9 | { 10 | /// 11 | /// Load department information 12 | /// 13 | /// Department information 14 | Task Load(); 15 | } 16 | } -------------------------------------------------------------------------------- /TimeSheet/Application/Interfaces/ILogWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace TimeSheet.Application.Interfaces 2 | { 3 | /// 4 | /// Interface for log wrappers 5 | /// 6 | public interface ILogWrapper 7 | { 8 | /// 9 | /// Echo information message 10 | /// 11 | /// Message 12 | void Info(string message); 13 | 14 | /// 15 | /// Echo warning message 16 | /// 17 | /// Message 18 | void Warning(string message); 19 | 20 | /// 21 | /// Echo error message 22 | /// 23 | /// Message 24 | /// Exception 25 | void Error(string message, Exception? ex = null); 26 | } 27 | } -------------------------------------------------------------------------------- /TimeSheet/Application/Interfaces/IWeekendsProvider.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Data; 2 | 3 | namespace TimeSheet.Application.Interfaces 4 | { 5 | /// 6 | /// Interface for holidays/weekends provider 7 | /// 8 | public interface IWeekendsProvider 9 | { 10 | /// 11 | /// Get annual weekends 12 | /// 13 | /// Date 14 | /// Annual weekends 15 | Task GetAnnualWeekends(DateTime date); 16 | } 17 | } -------------------------------------------------------------------------------- /TimeSheet/Application/Logger/ConsoleWrapper.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Application.Interfaces; 2 | 3 | namespace TimeSheet.Application.Logger 4 | { 5 | /// 6 | /// Log wrapper to output in a Console 7 | /// 8 | internal sealed class ConsoleWrapper : ILogWrapper 9 | { 10 | /// 11 | /// Echo information message 12 | /// 13 | /// Message 14 | public void Info(string message) 15 | { 16 | Console.WriteLine(message); 17 | } 18 | 19 | /// 20 | /// Echo warning message 21 | /// 22 | /// Message 23 | public void Warning(string message) 24 | { 25 | Console.ForegroundColor = ConsoleColor.DarkYellow; 26 | Console.WriteLine($"Предупреждение. {message}"); 27 | Console.ResetColor(); 28 | } 29 | 30 | /// 31 | /// Echo error message 32 | /// 33 | /// Message 34 | /// Exception 35 | public void Error(string message, Exception? ex = null) 36 | { 37 | Console.ForegroundColor = ConsoleColor.DarkRed; 38 | Console.WriteLine($"Ошибка. {message}"); 39 | Console.ResetColor(); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /TimeSheet/Application/Logger/Log.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Application.Interfaces; 2 | 3 | namespace TimeSheet.Application.Logger 4 | { 5 | /// 6 | /// Logger 7 | /// 8 | public static class Log 9 | { 10 | /// 11 | /// List of wrappers for logging 12 | /// 13 | private static readonly List LogWrappers = new(); 14 | 15 | /// 16 | /// Attach a new wrapper 17 | /// 18 | /// Wrapper 19 | public static void Attach(ILogWrapper logWrapper) 20 | { 21 | LogWrappers.Add(logWrapper); 22 | } 23 | 24 | /// 25 | /// Echo information message 26 | /// 27 | /// Message 28 | public static void Info(string message) 29 | { 30 | foreach (var logger in LogWrappers) 31 | { 32 | logger.Info(message); 33 | } 34 | } 35 | 36 | /// 37 | /// Echo warning message 38 | /// 39 | /// Message 40 | public static void Warning(string message) 41 | { 42 | foreach (var logger in LogWrappers) 43 | { 44 | logger.Warning(message); 45 | } 46 | } 47 | 48 | /// 49 | /// Echo error message 50 | /// 51 | /// Message 52 | /// Exception 53 | public static void Error(string message, Exception? ex = null) 54 | { 55 | foreach (var logger in LogWrappers) 56 | { 57 | logger.Error(message, ex); 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /TimeSheet/Application/Logger/NLogWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using NLog; 3 | using NLog.Config; 4 | using NLog.Layouts; 5 | using NLog.Targets; 6 | using TimeSheet.Application.Interfaces; 7 | 8 | namespace TimeSheet.Application.Logger 9 | { 10 | /// 11 | /// Log wrapper to output with NLog 12 | /// 13 | internal sealed class NLogWrapper : ILogWrapper 14 | { 15 | /// 16 | /// NLog logger 17 | /// 18 | private readonly NLog.Logger _logger; 19 | 20 | /// 21 | /// Parameter-less constructor 22 | /// 23 | internal NLogWrapper() 24 | { 25 | LogFolderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log"); 26 | var config = new LoggingConfiguration(); 27 | var csvTarget = new FileTarget 28 | { 29 | Encoding = Encoding.UTF8, 30 | FileName = Path.Combine(LogFolderPath, "csv-${shortdate}.log"), 31 | ArchiveFileName = Path.Combine(LogFolderPath, "log.{#}.txt"), 32 | ArchiveEvery = FileArchivePeriod.Day, 33 | ArchiveNumbering = ArchiveNumberingMode.Rolling, 34 | MaxArchiveFiles = 4, 35 | ConcurrentWrites = false 36 | }; 37 | 38 | var csvLayout = new CsvLayout 39 | { 40 | Delimiter = CsvColumnDelimiterMode.Tab, 41 | WithHeader = true 42 | }; 43 | csvLayout.Columns.Add(new CsvColumn("time", "${longdate}")); 44 | csvLayout.Columns.Add(new CsvColumn("level", "${level:upperCase=true}")); 45 | csvLayout.Columns.Add(new CsvColumn("message", "${message}")); 46 | csvLayout.Columns.Add(new CsvColumn("stacktrace", "${stacktrace:topFrames=10}")); 47 | csvLayout.Columns.Add(new CsvColumn("exception", "${exception:format=ToString}")); 48 | csvTarget.Layout = csvLayout; 49 | 50 | config.AddTarget("csv-file", csvTarget); 51 | LogManager.Configuration = config; 52 | _logger = LogManager.GetCurrentClassLogger(); 53 | } 54 | 55 | /// 56 | /// Path to a log folder 57 | /// 58 | internal string LogFolderPath { get; } 59 | 60 | /// 61 | /// Echo information message 62 | /// 63 | /// Message 64 | public void Info(string message) 65 | { 66 | _logger.Info(message); 67 | } 68 | 69 | /// 70 | /// Echo warning message 71 | /// 72 | /// Message 73 | public void Warning(string message) 74 | { 75 | _logger.Warn(message); 76 | } 77 | 78 | /// 79 | /// Echo error message 80 | /// 81 | /// Message 82 | /// Exception 83 | public void Error(string message, Exception? ex = null) 84 | { 85 | if (ex == null) 86 | { 87 | _logger.Error(message); 88 | } 89 | else 90 | { 91 | _logger.Error(ex, message); 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /TimeSheet/Application/ProgramCore.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Application.Interfaces; 2 | using TimeSheet.Application.Structs; 3 | 4 | namespace TimeSheet.Application 5 | { 6 | /// 7 | /// Core 8 | /// 9 | internal static class ProgramCore 10 | { 11 | /// 12 | /// List of a console command handlers 13 | /// 14 | internal static List ConsoleCommandHandlers { get; } = new(); 15 | 16 | /// 17 | /// Core initialization 18 | /// 19 | /// MUST init at the program startup 20 | internal static void Init() 21 | { 22 | ConsoleCommandHandlers.Clear(); 23 | var handlerType = typeof(ICommandsHandler); 24 | var types = AppDomain.CurrentDomain.GetAssemblies() 25 | .SelectMany(assembly => assembly.GetTypes()) 26 | .Where(type => type != handlerType && handlerType.IsAssignableFrom(type)); 27 | 28 | foreach (var type in types) 29 | { 30 | if (Activator.CreateInstance(type) is ICommandsHandler instance) 31 | { 32 | ConsoleCommandHandlers.Add(instance); 33 | } 34 | } 35 | } 36 | 37 | /// 38 | /// Class command handler 39 | /// 40 | /// Used by reflection. Do not remove 41 | internal sealed class ApplicationCommandHandler : ICommandsHandler 42 | { 43 | /// 44 | /// Show help information 45 | /// 46 | internal const string Help = "?"; 47 | 48 | /// 49 | /// Exit application 50 | /// 51 | private const string Exit = "exit"; 52 | 53 | /// 54 | /// Get console commands 55 | /// 56 | /// List of console commands 57 | public List GetCommands() 58 | { 59 | return new List() 60 | { 61 | new (Help, $" * {Help}: показать справку;", 6), 62 | new (Exit, $" * {Exit}: выйти из приложения;",7) 63 | }; 64 | } 65 | 66 | /// 67 | /// Process command 68 | /// 69 | /// A command 70 | /// A command processed 71 | public Task Process(string command) 72 | { 73 | var commands = GetCommands(); 74 | foreach (var localCommand in commands) 75 | { 76 | if (!localCommand.Name.Equals(command, StringComparison.InvariantCultureIgnoreCase)) 77 | { 78 | continue; 79 | } 80 | 81 | switch (localCommand.Name) 82 | { 83 | case Help: 84 | Console.WriteLine("Список доступных команд:"); 85 | var allCommands = new List(); 86 | foreach (var handler in ConsoleCommandHandlers) 87 | { 88 | allCommands.AddRange(handler.GetCommands()); 89 | } 90 | 91 | allCommands.Sort(); 92 | foreach (var commandInfo in allCommands) 93 | { 94 | Console.WriteLine(commandInfo.Help); 95 | } 96 | 97 | Console.WriteLine(); 98 | break; 99 | case Exit: 100 | Environment.Exit(0); 101 | break; 102 | } 103 | 104 | return Task.FromResult(true); 105 | } 106 | 107 | return Task.FromResult(false); 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /TimeSheet/Application/Structs/ConsoleCommand.cs: -------------------------------------------------------------------------------- 1 | namespace TimeSheet.Application.Structs 2 | { 3 | /// 4 | /// Information about a console command 5 | /// 6 | /// Command name 7 | /// Help description 8 | /// Order in a help chapter 9 | internal record ConsoleCommand(string Name, string Help, int Order) : IComparable 10 | { 11 | /// 12 | /// Command name 13 | /// 14 | internal string Name { get; set; } = Name; 15 | 16 | /// 17 | /// Help description 18 | /// 19 | internal string Help { get; set; } = Help; 20 | 21 | /// 22 | /// 23 | /// 24 | internal int Order { get; set; } = Order; 25 | 26 | /// 27 | /// Compare two console commands by order 28 | /// 29 | /// Second instance 30 | /// Comparison result 31 | public int CompareTo(ConsoleCommand? other) 32 | { 33 | return Order.CompareTo(other?.Order); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TimeSheet/Application/Utils/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace TimeSheet.Application.Utils 4 | { 5 | /// 6 | /// Utils to work with files/explorer 7 | /// 8 | internal static class FileUtils 9 | { 10 | /// 11 | /// Show file in explorer 12 | /// 13 | /// File path 14 | internal static void ShowInExplorer(string path) 15 | { 16 | if (!File.Exists(path)) 17 | { 18 | return; 19 | } 20 | 21 | var argument = "/select, \"" + path + "\""; 22 | Process.Start("explorer.exe", argument); 23 | } 24 | 25 | /// 26 | /// Launch file or open it on Explorer 27 | /// 28 | /// File path 29 | internal static void LaunchFile(string path) 30 | { 31 | try 32 | { 33 | Process.Start(path); 34 | } 35 | catch 36 | { 37 | ShowInExplorer(path); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /TimeSheet/Data/AnnualWeekendsInfo.cs: -------------------------------------------------------------------------------- 1 | namespace TimeSheet.Data 2 | { 3 | /// 4 | /// Year information about holidays/weekends by months 5 | /// 6 | public class AnnualWeekendsInfo : IEquatable 7 | { 8 | /// 9 | /// Dictionary month - days off 10 | /// 11 | private readonly Dictionary> _monthWeekends = new(); 12 | 13 | /// 14 | /// Constructor with parameters 15 | /// 16 | /// Data year 17 | public AnnualWeekendsInfo(int year) 18 | { 19 | Year = year; 20 | } 21 | 22 | /// 23 | /// Data year 24 | /// 25 | public int Year { get; } 26 | 27 | /// 28 | /// Get all weekends in month 29 | /// 30 | /// Month date 31 | /// List of month weekends 32 | public List GetMonthWeekends(DateTime date) 33 | { 34 | return _monthWeekends.ContainsKey(date.Month) ? _monthWeekends[date.Month] : new List(); 35 | } 36 | 37 | /// 38 | /// Add weekends by month 39 | /// 40 | /// Month 41 | /// List of weekends 42 | public void AddMonthWeekends(int month, List weekends) 43 | { 44 | if (!_monthWeekends.ContainsKey(month)) 45 | { 46 | _monthWeekends.Add(month, weekends); 47 | } 48 | else 49 | { 50 | _monthWeekends[month] = weekends; 51 | } 52 | } 53 | 54 | /// 55 | /// Check objects for equals 56 | /// 57 | /// Second object 58 | /// Flag if are equals 59 | public bool Equals(AnnualWeekendsInfo? other) 60 | { 61 | if (ReferenceEquals(null, other)) 62 | { 63 | return false; 64 | } 65 | 66 | if (ReferenceEquals(this, other)) 67 | { 68 | return true; 69 | } 70 | 71 | var isWeekendsEqual = _monthWeekends.Count == other._monthWeekends.Count && !_monthWeekends.Except(other._monthWeekends).Any(); 72 | return isWeekendsEqual && Year == other.Year; 73 | } 74 | 75 | /// 76 | /// Check objects for equals 77 | /// 78 | /// Second object 79 | /// Flag if are equals 80 | public override bool Equals(object? obj) 81 | { 82 | return obj?.GetType() == GetType() && Equals((AnnualWeekendsInfo)obj); 83 | } 84 | 85 | /// 86 | /// Get object hash code 87 | /// 88 | /// Hash code 89 | public override int GetHashCode() 90 | { 91 | return HashCode.Combine(_monthWeekends, Year); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /TimeSheet/Data/DepartmentInfo.cs: -------------------------------------------------------------------------------- 1 | namespace TimeSheet.Data 2 | { 3 | /// 4 | /// Information about a department 5 | /// 6 | public class DepartmentInfo : IEquatable 7 | { 8 | /// 9 | /// Department title 10 | /// 11 | public string DepartmentTitle { get; set; } = string.Empty; 12 | 13 | /// 14 | /// Department leader name 15 | /// 16 | public string LeaderName { get; set; } = string.Empty; 17 | 18 | /// 19 | /// Department leader position 20 | /// 21 | public string LeaderPosition { get; set; } = string.Empty; 22 | 23 | /// 24 | /// List of employees 25 | /// 26 | public List Employees { get; set; } = new(); 27 | 28 | /// 29 | /// Check objects for equals 30 | /// 31 | /// Second object 32 | /// Flag if are equals 33 | public bool Equals(DepartmentInfo? other) 34 | { 35 | if (ReferenceEquals(null, other)) 36 | { 37 | return false; 38 | } 39 | 40 | if (ReferenceEquals(this, other)) 41 | { 42 | return true; 43 | } 44 | 45 | return DepartmentTitle == other.DepartmentTitle && LeaderName == other.LeaderName && LeaderPosition == other.LeaderPosition && Employees.SequenceEqual(other.Employees); 46 | } 47 | 48 | /// 49 | /// Check objects for equals 50 | /// 51 | /// Second object 52 | /// Flag if are equals 53 | public override bool Equals(object? obj) 54 | { 55 | return obj?.GetType() == GetType() && Equals((DepartmentInfo)obj); 56 | } 57 | 58 | /// 59 | /// Get object hash code 60 | /// 61 | /// Hash code 62 | public override int GetHashCode() 63 | { 64 | return HashCode.Combine(DepartmentTitle, LeaderName, LeaderPosition, Employees); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /TimeSheet/Data/Employee.cs: -------------------------------------------------------------------------------- 1 | namespace TimeSheet.Data 2 | { 3 | /// 4 | /// Employee info 5 | /// 6 | public struct Employee : IEquatable 7 | { 8 | /// 9 | /// Constructor with parameters 10 | /// 11 | /// Employee name 12 | /// Work hours 13 | public Employee(string fullName, int workHours) 14 | { 15 | FullName = fullName; 16 | WorkHours = workHours; 17 | } 18 | 19 | /// 20 | /// Full name 21 | /// 22 | public string FullName { get; set; } 23 | 24 | /// 25 | /// Work hours 26 | /// 27 | public int WorkHours { get; set; } 28 | 29 | /// 30 | /// Check objects for equals 31 | /// 32 | /// Second object 33 | /// Flag if are equals 34 | public bool Equals(Employee other) 35 | { 36 | return FullName == other.FullName && WorkHours == other.WorkHours; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /TimeSheet/Domain/DepartmentStorage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using TimeSheet.Application.Interfaces; 3 | using TimeSheet.Data; 4 | 5 | namespace TimeSheet.Domain 6 | { 7 | /// 8 | /// Storage of department info: workers, leader, etc 9 | /// 10 | public sealed class DepartmentStorage : IEnumerable 11 | { 12 | /// 13 | /// Weekends provider 14 | /// 15 | private readonly IDepartmentProvider _provider; 16 | 17 | /// 18 | /// Department information 19 | /// 20 | private DepartmentInfo? _departmentInfo; 21 | 22 | /// 23 | /// Constructor with parameters 24 | /// 25 | /// Data provider 26 | public DepartmentStorage(IDepartmentProvider provider) 27 | { 28 | _provider = provider; 29 | } 30 | 31 | /// 32 | /// Check if a department has employees 33 | /// 34 | public bool HasEmployees => _departmentInfo?.Employees?.Count > 0; 35 | 36 | /// 37 | /// Employees count 38 | /// 39 | public int EmployeesCount => _departmentInfo == null ? 0 : _departmentInfo.Employees.Count; 40 | 41 | /// 42 | /// Department title 43 | /// 44 | public string DepartmentTitle => _departmentInfo?.DepartmentTitle ?? string.Empty; 45 | 46 | /// 47 | /// Department leader position 48 | /// 49 | public string LeaderPosition => _departmentInfo?.LeaderPosition ?? string.Empty; 50 | 51 | /// 52 | /// Department leader name 53 | /// 54 | public string LeaderName => _departmentInfo?.LeaderName ?? string.Empty; 55 | 56 | /// 57 | /// Enumerator 58 | /// 59 | /// List of employees 60 | public IEnumerator GetEnumerator() 61 | { 62 | return _departmentInfo != null ? _departmentInfo.Employees.GetEnumerator() : Enumerable.Empty().GetEnumerator(); 63 | } 64 | 65 | /// 66 | /// Get enumerator 67 | /// 68 | /// Enumerator 69 | IEnumerator IEnumerable.GetEnumerator() 70 | { 71 | return GetEnumerator(); 72 | } 73 | 74 | /// 75 | /// Load department info from a provider 76 | /// 77 | /// Task 78 | public async Task Load() 79 | { 80 | _departmentInfo = await _provider.Load(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /TimeSheet/Domain/Providers/ConsultantWeekendsProvider.cs: -------------------------------------------------------------------------------- 1 | using AngleSharp.Html.Dom; 2 | using AngleSharp.Html.Parser; 3 | using TimeSheet.Application.Interfaces; 4 | using TimeSheet.Application.Logger; 5 | using TimeSheet.Data; 6 | 7 | namespace TimeSheet.Domain.Providers 8 | { 9 | /// 10 | /// Provider for taking weekends/holidays from the Consultant Plus source 11 | /// 12 | public sealed class ConsultantWeekendsProvider : IWeekendsProvider 13 | { 14 | /// 15 | /// Path to the source url 16 | /// 17 | private const string ConsultantUrl = "http://www.consultant.ru/law/ref/calendar/proizvodstvennye/"; 18 | 19 | /// 20 | /// Get annual weekends 21 | /// 22 | /// Date 23 | /// Annual weekends 24 | public async Task GetAnnualWeekends(DateTime date) 25 | { 26 | var monthUrl = $"{ConsultantUrl}{date.Year}/"; 27 | var result = new AnnualWeekendsInfo(date.Year); 28 | try 29 | { 30 | string? html; 31 | using (var webClient = new HttpClient()) 32 | { 33 | html = await webClient.GetStringAsync(monthUrl); 34 | } 35 | 36 | var parser = new HtmlParser(); 37 | var document = await parser.ParseDocumentAsync(html); 38 | var selector = document.QuerySelectorAll(".cal"); 39 | for (var i = 0; i < selector.Length; i++) 40 | { 41 | var weekendSelector = selector[i].QuerySelectorAll(".holiday,.weekend").Where(x => x is IHtmlTableDataCellElement); 42 | result.AddMonthWeekends(i + 1, weekendSelector.Select(item => int.Parse(item.InnerHtml)).ToList()); 43 | } 44 | 45 | return result; 46 | } 47 | catch (Exception ex) 48 | { 49 | Log.Error("Не удалось получить данные по выходным дням с сервера Консультант Плюс.", ex); 50 | return new AnnualWeekendsInfo(0); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /TimeSheet/Domain/Providers/DepartmentFileProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using TimeSheet.Application.Interfaces; 3 | using TimeSheet.Application.Logger; 4 | using TimeSheet.Application.Structs; 5 | using TimeSheet.Application.Utils; 6 | using TimeSheet.Data; 7 | 8 | namespace TimeSheet.Domain.Providers 9 | { 10 | /// 11 | /// Department storage file data source 12 | /// 13 | internal sealed class DepartmentFileProvider : IDepartmentProvider 14 | { 15 | /// 16 | /// Load department information 17 | /// 18 | /// Department information 19 | public async Task Load() 20 | { 21 | const string errorMessage = "Не удалось загрузить данные по сотрудникам. Проверьте целостность файла данных."; 22 | if (string.IsNullOrWhiteSpace(DepartmentFileProviderSettings.SourcePath)) 23 | { 24 | Log.Error(errorMessage); 25 | return null; 26 | } 27 | 28 | await using var fileStream = new FileStream(DepartmentFileProviderSettings.SourcePath, FileMode.Open, FileAccess.Read); 29 | var result = await JsonSerializer.DeserializeAsync(fileStream); 30 | if (result == null) 31 | { 32 | Log.Error(errorMessage); 33 | } 34 | 35 | return result; 36 | } 37 | 38 | /// 39 | /// Class command handler 40 | /// 41 | /// Used by reflection. Do not remove 42 | internal sealed class DepartmentFileProviderCommandHandler : ICommandsHandler 43 | { 44 | /// 45 | /// Command to open a file in the Explorer 46 | /// 47 | private const string OpenSourceFileCommand = "employees"; 48 | 49 | /// 50 | /// Command to view a custom path to a file sourceЫ 51 | /// 52 | private const string ShowFilePathCommand = "print_path"; 53 | 54 | /// 55 | /// Command to setup a custom file source path 56 | /// 57 | private const string EditFilePathCommand = "path"; 58 | 59 | /// 60 | /// Get console commands 61 | /// 62 | /// List of console commands 63 | public List GetCommands() 64 | { 65 | return new List() 66 | { 67 | new (ShowFilePathCommand, $" * {ShowFilePathCommand}: вывести путь к файлу со списком сотрудников;", 3), 68 | new (EditFilePathCommand, $" * {EditFilePathCommand}: изменить путь к файлу со списком сотрудников;", 5), 69 | new(OpenSourceFileCommand, $" * {OpenSourceFileCommand}: открыть файл со списком сотрудников;", 4) 70 | }; 71 | } 72 | 73 | /// 74 | /// Process command 75 | /// 76 | /// A command 77 | /// A command processed 78 | public Task Process(string command) 79 | { 80 | var commands = GetCommands(); 81 | foreach (var localCommand in commands) 82 | { 83 | if (!localCommand.Name.Equals(command, StringComparison.InvariantCultureIgnoreCase)) 84 | { 85 | continue; 86 | } 87 | 88 | switch (localCommand.Name) 89 | { 90 | case OpenSourceFileCommand: 91 | DepartmentFileProviderSettings.LaunchStorageFile(); 92 | break; 93 | case ShowFilePathCommand: 94 | DepartmentFileProviderSettings.PrintPath(); 95 | break; 96 | case EditFilePathCommand: 97 | Console.WriteLine("Пожалуйста, укажите полный путь до файла со списком сотрудников (*.json)."); 98 | var path = Console.ReadLine(); 99 | if (string.IsNullOrWhiteSpace(path)) 100 | { 101 | Log.Warning("Путь до файла не может быть пустым."); 102 | break; 103 | } 104 | 105 | DepartmentFileProviderSettings.UpdateFilePath(path); 106 | break; 107 | } 108 | 109 | return Task.FromResult(true); 110 | } 111 | 112 | return Task.FromResult(false); 113 | } 114 | } 115 | 116 | /// 117 | /// Class settings 118 | /// 119 | internal static class DepartmentFileProviderSettings 120 | { 121 | /// 122 | /// Default path to a department info file 123 | /// 124 | private static readonly string DefaultSourcePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "department.json"); 125 | 126 | /// 127 | /// Custom user path to a department info file 128 | /// 129 | private static string? _sourcePath = DefaultSourcePath; 130 | 131 | /// 132 | /// Parameter-less constructor 133 | /// 134 | static DepartmentFileProviderSettings() 135 | { 136 | if (!string.IsNullOrWhiteSpace(Settings.Default.DepartmentStorageSourcePath)) 137 | { 138 | SourcePath = Settings.Default.DepartmentStorageSourcePath; 139 | ValidateUserFile(); 140 | } 141 | } 142 | 143 | /// 144 | /// Path to a file source 145 | /// 146 | internal static string? SourcePath 147 | { 148 | get => _sourcePath; 149 | 150 | private set 151 | { 152 | if (_sourcePath == value) 153 | { 154 | return; 155 | } 156 | 157 | _sourcePath = value; 158 | Settings.Default.DepartmentStorageSourcePath = _sourcePath; 159 | 160 | } 161 | } 162 | 163 | /// 164 | /// Open storage file 165 | /// 166 | internal static void LaunchStorageFile() 167 | { 168 | ValidateUserFile(); 169 | if (!string.IsNullOrWhiteSpace(SourcePath)) 170 | { 171 | FileUtils.LaunchFile(SourcePath); 172 | } 173 | } 174 | 175 | /// 176 | /// Update user path to a source file 177 | /// 178 | /// New path 179 | internal static void UpdateFilePath(string? filePath) 180 | { 181 | if (string.IsNullOrWhiteSpace(filePath) || filePath == SourcePath) 182 | { 183 | return; 184 | } 185 | 186 | SourcePath = filePath; 187 | ValidateUserFile(); 188 | } 189 | 190 | /// 191 | /// Print user path 192 | /// 193 | internal static void PrintPath() 194 | { 195 | Console.WriteLine(SourcePath); 196 | Console.WriteLine(); 197 | } 198 | 199 | /// 200 | /// Check and validate an user source path 201 | /// 202 | private static void ValidateUserFile() 203 | { 204 | if (!File.Exists(SourcePath)) 205 | { 206 | if (SourcePath == null || !SourcePath.Equals(DefaultSourcePath)) 207 | { 208 | Log.Warning("Указанный файл со списком сотрудников не найден. Будет использован файл по умолачнию"); 209 | } 210 | 211 | if (!File.Exists(DefaultSourcePath)) 212 | { 213 | File.WriteAllBytes(DefaultSourcePath, Resource.SampleDepartmentFile); 214 | } 215 | 216 | SourcePath = DefaultSourcePath; 217 | } 218 | } 219 | } 220 | } 221 | } -------------------------------------------------------------------------------- /TimeSheet/Domain/ReportCreator.cs: -------------------------------------------------------------------------------- 1 | using OfficeOpenXml; 2 | using System.Globalization; 3 | using OfficeOpenXml.Style; 4 | using TimeSheet.Application.Interfaces; 5 | using TimeSheet.Application.Logger; 6 | using TimeSheet.Domain.Providers; 7 | using TimeSheet.Application.Structs; 8 | using TimeSheet.Application.Utils; 9 | 10 | namespace TimeSheet.Domain 11 | { 12 | public static class ReportCreator 13 | { 14 | /// 15 | /// Create a time-sheet report 16 | /// 17 | /// Date of report 18 | /// Department storage provider 19 | /// Weekends provider 20 | /// Output path 21 | /// Path to the report 22 | public static async Task Create(DateTime date, IDepartmentProvider departmentProvider, IWeekendsProvider weekendsProvider, string? exportPath = null) 23 | { 24 | var dataStorage = new WeekendsStorage(weekendsProvider); 25 | var currentMonthWeekends = await dataStorage.GetMonthWeekends(date); 26 | 27 | var departmentStorage = new DepartmentStorage(departmentProvider); 28 | await departmentStorage.Load(); 29 | 30 | if (!departmentStorage.HasEmployees) 31 | { 32 | Log.Warning("Отсутствуют сотрудники для формирования табеля учета рабочих дней."); 33 | return null; 34 | } 35 | 36 | var path = exportPath; 37 | if (string.IsNullOrWhiteSpace(path)) 38 | { 39 | path = GetReportUniquePath(date); 40 | } 41 | else 42 | { 43 | if (File.Exists(path)) 44 | { 45 | File.Delete(path); 46 | } 47 | } 48 | 49 | try 50 | { 51 | using var excel = new ExcelPackage(new FileInfo(path)); 52 | var cultureInfo = CultureInfo.CreateSpecificCulture("ru"); 53 | var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month); 54 | var fullMonthName = date.ToString("MMMM", cultureInfo).ToLower(); 55 | 56 | const int startDataRow = 6; 57 | const int serviceColumnsCount = 2; 58 | const int serviceRowsCount = 3; 59 | var columnsCount = daysInMonth + serviceColumnsCount; 60 | var rowsCount = departmentStorage.EmployeesCount + serviceRowsCount; 61 | 62 | var worksheet = excel.Workbook.Worksheets.Add("Табель учета"); 63 | worksheet.PrinterSettings.Orientation = eOrientation.Landscape; 64 | var cell = worksheet.Cells[3, 1]; 65 | cell.Value = departmentStorage.DepartmentTitle; 66 | cell.Style.Font.Size = 12; 67 | worksheet.Cells[3, 1, 3, columnsCount].Merge = true; 68 | 69 | cell = worksheet.Cells[4, 1]; 70 | cell.Value = $"Табель учета рабочего времени за {fullMonthName} {date.Year} года."; 71 | cell.Style.Font.Size = 12; 72 | worksheet.Cells[4, 1, 4, columnsCount].Merge = true; 73 | 74 | cell = worksheet.Cells[startDataRow, 1]; 75 | cell.Value = "№ Ф.И.О."; 76 | cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; 77 | 78 | cell = worksheet.Cells[startDataRow, 2]; 79 | cell.Value = "Числа месяца"; 80 | cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; 81 | 82 | cell = worksheet.Cells[startDataRow, columnsCount]; 83 | cell.Value = "Кол-во рабочих дней"; 84 | cell.Style.WrapText = true; 85 | cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; 86 | cell.Style.VerticalAlignment = ExcelVerticalAlignment.Center; 87 | 88 | var dayNames = cultureInfo.DateTimeFormat.AbbreviatedDayNames.Select(dayName => dayName.ToLower()).ToList(); 89 | var employeeIndex = -1; 90 | foreach (var worker in departmentStorage) 91 | { 92 | ++employeeIndex; 93 | var currentRowPosition = startDataRow + employeeIndex + serviceRowsCount; 94 | worksheet.Cells[currentRowPosition, 1].Value = worker.FullName; 95 | 96 | for (var j = 1; j <= daysInMonth; j++) 97 | { 98 | var dayName = dayNames[(int)new DateTime(date.Year, date.Month, j).DayOfWeek]; 99 | if (employeeIndex == 0) 100 | { 101 | //// Day index 102 | cell = worksheet.Cells[startDataRow + 1, j + 1]; 103 | cell.Value = j.ToString(); 104 | cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; 105 | 106 | //// Day name 107 | cell = worksheet.Cells[startDataRow + 2, j + 1]; 108 | cell.Value = dayName; 109 | cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; 110 | } 111 | 112 | if (currentMonthWeekends.Contains(j)) 113 | { 114 | continue; 115 | } 116 | 117 | cell = worksheet.Cells[currentRowPosition, j + 1]; 118 | cell.Value = worker.WorkHours; 119 | cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; 120 | } 121 | 122 | cell = worksheet.Cells[currentRowPosition, columnsCount]; 123 | cell.Formula = $"SUM({worksheet.Cells[currentRowPosition, serviceColumnsCount, currentRowPosition, columnsCount - 1]})"; 124 | cell.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; 125 | } 126 | 127 | worksheet.Cells[startDataRow, columnsCount, startDataRow + departmentStorage.EmployeesCount, columnsCount].Style.Font.Size = 11; 128 | cell = worksheet.Cells[startDataRow + 1 + rowsCount, 1]; 129 | cell.Value = "к - командировка, б - больничный, о - отпуск, п - прогул, д - декрет, 8 - проработанное время, у - увольнения"; 130 | cell.Style.Font.Size = 10; 131 | cell.Style.Font.Italic = true; 132 | 133 | worksheet.Cells[startDataRow + rowsCount + 1, 1].Style.Font.Italic = true; 134 | 135 | var range = worksheet.Cells[startDataRow, 1, startDataRow + 2, 1]; 136 | range.Style.VerticalAlignment = ExcelVerticalAlignment.Center; 137 | range.Merge = true; 138 | 139 | range = worksheet.Cells[startDataRow, 2, startDataRow, columnsCount - 1]; 140 | range.Style.VerticalAlignment = ExcelVerticalAlignment.Center; 141 | range.Merge = true; 142 | 143 | range = worksheet.Cells[startDataRow, columnsCount, startDataRow + 2, columnsCount]; 144 | range.Style.VerticalAlignment = ExcelVerticalAlignment.Center; 145 | range.Merge = true; 146 | 147 | var leaderColumnPosition = columnsCount / 2; 148 | worksheet.Cells[startDataRow + rowsCount + 4, leaderColumnPosition].Value = departmentStorage.LeaderPosition; 149 | worksheet.Cells[startDataRow + rowsCount + 4, leaderColumnPosition].Style.Font.Size = 11; 150 | 151 | range = worksheet.Cells[startDataRow, 1, startDataRow + rowsCount - 1, columnsCount]; 152 | range.Style.Border.Top.Style = ExcelBorderStyle.Thin; 153 | range.Style.Border.Bottom.Style = ExcelBorderStyle.Thin; 154 | range.Style.Border.Left.Style = ExcelBorderStyle.Thin; 155 | range.Style.Border.Right.Style = ExcelBorderStyle.Thin; 156 | 157 | var signatureRowPosition = startDataRow + rowsCount + startDataRow; 158 | cell = worksheet.Cells[signatureRowPosition, leaderColumnPosition]; 159 | cell.Value = @"__________________________"; 160 | cell.Style.Font.Size = 11; 161 | 162 | var signatureColumnPosition = (int)(columnsCount * 0.8f); 163 | worksheet.Cells[signatureRowPosition, signatureColumnPosition].Value = departmentStorage.LeaderName; 164 | worksheet.Cells[signatureRowPosition, signatureColumnPosition].Style.Font.Size = 11; 165 | 166 | worksheet.Column(1).Width = 13.86; 167 | worksheet.Column(columnsCount).Width = 8.43; 168 | for (var j = serviceColumnsCount; j <= serviceColumnsCount + daysInMonth - 1; j++) 169 | { 170 | worksheet.Column(j).Width = 2.71; 171 | } 172 | 173 | excel.Save(); 174 | } 175 | catch (Exception ex) 176 | { 177 | Log.Error("При создании отчета произошла ошибка. Пожалуйста, обратитесь к разработчикам.", ex); 178 | return null; 179 | } 180 | 181 | Log.Info("Создание отчета успешно завершено."); 182 | return path; 183 | } 184 | 185 | /// 186 | /// Get unique path for a created report 187 | /// 188 | /// Report date 189 | /// Unique path 190 | private static string GetReportUniquePath(DateTime date) 191 | { 192 | var dateStr = date.ToShortDateString(); 193 | 194 | var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"report_{dateStr}.xlsx"); 195 | var indexer = 1; 196 | while (File.Exists(path)) 197 | { 198 | path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"report_{dateStr}({indexer}).xlsx"); 199 | indexer++; 200 | } 201 | 202 | return path; 203 | } 204 | 205 | /// 206 | /// Class command handler 207 | /// 208 | /// Used by reflection. Do not remove 209 | internal sealed class ReportCreatorCommandHandler : ICommandsHandler 210 | { 211 | /// 212 | /// Create time sheet report 213 | /// 214 | internal const string Report = "report"; 215 | 216 | /// 217 | /// Get console commands 218 | /// 219 | /// List of console commands 220 | public List GetCommands() 221 | { 222 | return new List() 223 | { 224 | new (Report, $" * {Report}: создать табель учета рабочего времени на текущую дату;", 1), 225 | new (Report, $" * {Report} ДАТА: создать табель учета рабочего времени на указанную дату;", 2), 226 | }; 227 | } 228 | 229 | /// 230 | /// Process command 231 | /// 232 | /// A command 233 | /// A command processed 234 | public async Task Process(string command) 235 | { 236 | var commands = GetCommands(); 237 | foreach (var localCommand in commands) 238 | { 239 | if (!command.StartsWith(localCommand.Name, StringComparison.InvariantCultureIgnoreCase)) 240 | { 241 | continue; 242 | } 243 | 244 | switch (localCommand.Name) 245 | { 246 | case Report: 247 | var subCommands = command.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); 248 | var date = DateTime.Now; 249 | if (subCommands.Length == 2) 250 | { 251 | if (!DateTime.TryParse(subCommands[1], out date)) 252 | { 253 | date = DateTime.Now; 254 | } 255 | } 256 | 257 | Console.WriteLine("Пожалуйста подождите, идет подготовка отчета..."); 258 | var path = await Create(date, new DepartmentFileProvider(), new ConsultantWeekendsProvider()); 259 | if (!string.IsNullOrWhiteSpace(path)) 260 | { 261 | FileUtils.LaunchFile(path); 262 | } 263 | 264 | break; 265 | } 266 | return true; 267 | } 268 | 269 | return false; 270 | } 271 | } 272 | } 273 | } -------------------------------------------------------------------------------- /TimeSheet/Domain/WeekendsStorage.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Application.Interfaces; 2 | using TimeSheet.Data; 3 | 4 | namespace TimeSheet.Domain 5 | { 6 | /// 7 | /// Storage of weekends 8 | /// 9 | internal sealed class WeekendsStorage 10 | { 11 | /// 12 | /// Annual weekends information 13 | /// 14 | private AnnualWeekendsInfo? _weekendsInfo; 15 | 16 | /// 17 | /// Weekends provider 18 | /// 19 | private readonly IWeekendsProvider _provider; 20 | 21 | /// 22 | /// Constructor with parameters 23 | /// 24 | /// Weekends provider 25 | internal WeekendsStorage(IWeekendsProvider provider) 26 | { 27 | _provider = provider; 28 | } 29 | 30 | /// 31 | /// Load weekends from data provider 32 | /// 33 | /// Date with year 34 | /// Task 35 | private async Task LoadWeekendsFromProvider(DateTime yearDate) 36 | { 37 | _weekendsInfo = await _provider.GetAnnualWeekends(yearDate); 38 | } 39 | 40 | /// 41 | /// Get weekends in month 42 | /// 43 | /// Date with month 44 | /// 45 | internal async Task> GetMonthWeekends(DateTime monthDate) 46 | { 47 | if (_weekendsInfo == null || _weekendsInfo.Year != monthDate.Year) 48 | { 49 | await LoadWeekendsFromProvider(monthDate); 50 | } 51 | 52 | return _weekendsInfo?.GetMonthWeekends(monthDate) ?? new List(); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /TimeSheet/Program.cs: -------------------------------------------------------------------------------- 1 | using TimeSheet.Application; 2 | using TimeSheet.Application.Logger; 3 | 4 | Log.Attach(new ConsoleWrapper()); 5 | Log.Attach(new NLogWrapper()); 6 | 7 | ProgramCore.Init(); 8 | 9 | Console.WriteLine($"Добро пожаловать в TimeSheet: программу для составления табелей учета рабочего времени. Введите '{ProgramCore.ApplicationCommandHandler.Help}' для справки."); 10 | while (true) 11 | { 12 | var command = Console.ReadLine()?.ToLower().Trim(); 13 | if (string.IsNullOrWhiteSpace(command)) 14 | { 15 | continue; 16 | } 17 | 18 | var isCommandProcessed = false; 19 | foreach (var handler in ProgramCore.ConsoleCommandHandlers) 20 | { 21 | if (await handler.Process(command)) 22 | { 23 | isCommandProcessed = true; 24 | break; 25 | } 26 | } 27 | 28 | if (!isCommandProcessed) 29 | { 30 | Console.WriteLine($"Команда не найдена. Введите '{ProgramCore.ApplicationCommandHandler.Help}' для справки."); 31 | Console.WriteLine(); 32 | } 33 | } -------------------------------------------------------------------------------- /TimeSheet/Resource.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TimeSheet { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resource { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resource() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TimeSheet.Resource", typeof(Resource).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Byte[]. 65 | /// 66 | internal static byte[] SampleDepartmentFile { 67 | get { 68 | object obj = ResourceManager.GetObject("SampleDepartmentFile", resourceCulture); 69 | return ((byte[])(obj)); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /TimeSheet/Resource.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | bin\Debug\net6.0\department.json;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 123 | 124 | -------------------------------------------------------------------------------- /TimeSheet/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace TimeSheet { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.2.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | internal static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | 26 | [global::System.Configuration.UserScopedSettingAttribute()] 27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 28 | [global::System.Configuration.DefaultSettingValueAttribute("")] 29 | public string DepartmentStorageSourcePath { 30 | get { 31 | return ((string)(this["DepartmentStorageSourcePath"])); 32 | } 33 | set { 34 | this["DepartmentStorageSourcePath"] = value; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /TimeSheet/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TimeSheet/TimeSheet.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | True 24 | True 25 | Resource.resx 26 | 27 | 28 | True 29 | True 30 | Settings.settings 31 | 32 | 33 | 34 | 35 | 36 | ResXFileCodeGenerator 37 | Resource.Designer.cs 38 | 39 | 40 | 41 | 42 | 43 | SettingsSingleFileGenerator 44 | Settings.Designer.cs 45 | 46 | 47 | 48 | 49 | --------------------------------------------------------------------------------