TimeTrackerApp.psm1
#Region '.\Public\Get-ConfigFile.ps1' 0 function Get-ConfigFile { <# .SYNOPSIS Get config file .DESCRIPTION This function is reading existing config file. .EXAMPLE Get-ConfigFile CircularLogging : True OutputFormat : CSV OutputFolder : C:\Users TimeIncrementMins : 30 Technician : Mr Test LoggingLevel : None #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] param ( ) begin { } process { if ($pscmdlet.ShouldProcess("$PSScriptRoot\TimeTracker.config", "Get-ConfigFile")){ if(Test-Path -Path $PSScriptRoot\TimeTracker.config){ Write-Verbose "Config Exists" $Config=Get-Content "$PSScriptRoot\TimeTracker.config" | ConvertFrom-Json if(Test-ConfigFile $Config){ Write-Verbose "Config has been successfully validated." $Config }else{ Write-Error "Config File is invalid." Write-Verbose "Please run New-ConfigFile cmdlet or remove error(s) in existing configuration file." } }else { Write-Error "Config Not Found" Write-Verbose "Please run New-ConfigFile cmdlet" } } } end { } } #EndRegion '.\Public\Get-ConfigFile.ps1' 45 #Region '.\Public\Get-TimeTracker.ps1' 0 function Get-TimeTracker { <# .SYNOPSIS Get TimeTracker instance .DESCRIPTION This function is reading all started TimeTracker instances. .EXAMPLE Get-TimeTracker StartTime Technician Comment Id --------- ---------- ------- -- 2/7/2023 12:56:40 PM Mr Test Reading emails 000e67ec-48b9-4244-8f90-006a0afe3929 #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] param ( ) begin { } process { if ($pscmdlet.ShouldProcess("All started time tracking instances", "Get-TimeTracker")){ $Config=Get-ConfigFile $Tracker=Get-ChildItem -Path "$($Config.OutputFolder)\TimeTracker\" -Filter "*.track" $TimeTracking=New-Object System.Collections.Generic.List[PSObject] foreach ($f in $Tracker) { $TimeTracking.Add($(Get-Content $f.FullName | ConvertFrom-Json )) | Out-Null } $TimeTracking } } end { } } #EndRegion '.\Public\Get-TimeTracker.ps1' 35 #Region '.\Public\Invoke-TimeTracker.ps1' 0 function Invoke-TimeTracker { <# .SYNOPSIS Invoke TimeTracker WPF UI .DESCRIPTION This function is loading TimeTracker WPF App. .EXAMPLE Invoke-TimeTracker #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] param () begin {} process { if ($pscmdlet.ShouldProcess("Graphic User Interface", "Invoke-TimeTracker")) { try { if($IsWindows){ $ModuleVersion=$(Get-Module TimeTrackerApp | Select-Object -ExpandProperty Version).ToString() Write-Verbose "Loading Windows Presentation Foundation" [void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework') #region WPF Windows Design #region MainWindow [xml]$MainWindowXAML = @" <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="" Height="400" Width="385" Topmost="True" WindowStartupLocation="CenterScreen" Name="Window" WindowStyle='None' ResizeMode='CanMinimize' > <Grid Background="#2c2c32"> <ListView Name="TimeTrackersListView" Background="#2c2c32" BorderThickness="0" Foreground="#F5F5F5" HorizontalAlignment="Left" Margin="30,40,0,0" VerticalAlignment="Top" Height="280" Width="320"> <ListView.ItemContainerStyle> <Style> <Style.Triggers> <Trigger Property="Control.IsMouseOver" Value="True"> <Setter Property="Control.Background" Value="Transparent" /> </Trigger> </Style.Triggers> </Style> </ListView.ItemContainerStyle> <ListView.View> <GridView AllowsColumnReorder="false" ColumnHeaderToolTip="Time Tracking Information"> <GridViewColumn DisplayMemberBinding="{Binding StartTime}" Header="Start Time" Width="130"/> <GridViewColumn DisplayMemberBinding="{Binding Comment}" Header="Comment" Width="180"/> </GridView> </ListView.View> </ListView> <TextBox Name="TitleTextBox" HorizontalAlignment="Center" Height="31" TextWrapping="Wrap" Text="Time Tracker App" VerticalAlignment="Top" Width="525" Margin="0,-1,-0.2,0" TextAlignment="Center" VerticalContentAlignment="Center" Foreground="#F5F5F5" Background="#585858" FontFamily="Century Gothic" FontSize="14" FontWeight="Bold" IsReadOnly="True" IsEnabled="False"/> <Button Name="btnStartTimer" Content="Start Timer" HorizontalAlignment="Left" Margin="30,300,0,0" VerticalAlignment="Top" Width="100" Height="54" BorderThickness="0" Foreground="#F5F5F5" Background="#585858" FontSize="14" /> <Button Name="btnStopTimer" Content="Stop Timer" HorizontalAlignment="Left" Margin="255,300,0,0" VerticalAlignment="Top" Width="100" Height="54" BorderThickness="0" Foreground="#F5F5F5" Background="#585858" FontSize="14" /> <TextBlock Name="VersionTextBlock" HorizontalAlignment="Left" Height="31" TextWrapping="Wrap" VerticalAlignment="Top" Width="100" Margin="0,370,-0.2,0" TextAlignment="Center" Foreground="#F5F5F5" Background="#2c2c32" FontFamily="Century Gothic" FontSize="12"> <Hyperlink>v$ModuleVersion</Hyperlink> </TextBlock> <TextBlock Name="AuthorTextBlock" HorizontalAlignment="Left" Height="31" VerticalAlignment="Top" Width="150" Margin="235,370,-0.2,0" TextAlignment="Center" Foreground="#F5F5F5" Background="#2c2c32" FontFamily="Century Gothic" FontSize="12"> <Hyperlink>by @andysvints</Hyperlink> </TextBlock> <Button Name="btnClose" Content="X" HorizontalAlignment="Left" Margin="357,3,0,0" VerticalAlignment="Top" Width="25" Height="25" BorderThickness="1" FontWeight="Bold"/> <Button Name="btnMinimize" Content="_" HorizontalAlignment="Left" Margin="332,3,0,0" VerticalAlignment="Top" Width="25" Height="25" BorderThickness="1" FontWeight="Bold"/> </Grid> </Window> "@ #endregion #region Comment Windows [xml]$CommentWindowXAML = @' <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="" Height="189" Width="300" Topmost="True" WindowStartupLocation="CenterScreen" Name="Window1" WindowStyle='None' ResizeMode='CanMinimize' > <Grid Background="#2c2c32"> <TextBox Name="CommentTitleTextBox" HorizontalAlignment="Center" Height="31" TextWrapping="Wrap" Text="Add Comment" VerticalAlignment="Top" Width="525" Margin="0,-1,-0.2,0" TextAlignment="Center" VerticalContentAlignment="Center" Foreground="#F5F5F5" Background="#585858" FontFamily="Century Gothic" FontSize="14" FontWeight="Bold" IsReadOnly="True" IsEnabled="False"/> <TextBox Name="CommentTextBox" HorizontalAlignment="Center" TextWrapping="Wrap" AcceptsReturn="True" VerticalAlignment="Top" Height="110" Width="300" Margin="0,35,0,0" Foreground="#F5F5F5" Background="#585858" FontFamily="Century Gothic" FontSize="14" /> <Button Name="btnStart" Content="Start" HorizontalAlignment="Left" Margin="115,150,0,0" VerticalAlignment="Top" Width="70" Height="34" BorderThickness="0" Foreground="#F5F5F5" Background="#585858" FontSize="14" /> <Button Name="btnCloseBox" Content="X" HorizontalAlignment="Left" Margin="272,3,0,0" VerticalAlignment="Top" Width="25" Height="25" BorderThickness="1" FontWeight="Bold"/> </Grid> </Window> '@ #endregion #endregion Write-Verbose "Reading XAML for Main Window" $MainFormXAMLReader=(New-Object System.Xml.XmlNodeReader $MainWindowXAML) Write-Verbose "Loading Main Window" $MainForm=[Windows.Markup.XamlReader]::Load( $MainFormXAMLReader ) Write-Verbose "Store Main Window Form Objects In PowerShell" $MainWindowXAML.SelectNodes("//*[@Name]") | ForEach-Object {Set-Variable -Name ($_.Name) -Value $MainForm.FindName($_.Name)} Write-Verbose "Reading XAML for Comments Window" $CommentFormXAMLReader=(New-Object System.Xml.XmlNodeReader $CommentWindowXAML) Write-Verbose "Loading Comments Window" $CommentForm=[Windows.Markup.XamlReader]::Load( $CommentFormXAMLReader ) Write-Verbose "Store Comment Window Form Objects In PowerShell" $CommentWindowXAML.SelectNodes("//*[@Name]") | ForEach-Object {Set-Variable -Name ($_.Name) -Value $CommentForm.FindName($_.Name)} #region WPF Form functions $btnClose.Add_Click({ $MainForm.Close() }) $btnMinimize.Add_Click({ $MainForm.WindowState="Minimized" }) $MainForm.Add_MouseDown({ $MainForm.DragMove() }) $MainForm.Add_ContentRendered({ $TimeTrackersListView.ItemsSource=@(Get-TimeTracker) }) $btnStartTimer.Add_Click({ $CommentForm.ShowDialog() | out-null }) $CommentForm.Add_MouseDown({ $CommentForm.DragMove() }) $btnStopTimer.Add_Click({ #TODO: Adjust Comment if needed if($TimeTrackersListView.SelectedItems){ $TimeTrackingInstance=$TimeTrackersListView.SelectedItems Write-Verbose "Selected: Comment - $($TimeTrackingInstance.Comment)" Stop-TimeTracker -Id $TimeTrackingInstance.Id $TimeTrackersListView.ItemsSource=@(Get-TimeTracker) }else{ $MessageText="No TimeTracking Item has been selected." [System.Windows.MessageBox]::Show($MessageText,"",[System.Windows.MessageBoxButton]::OK,[System.Windows.MessageBoxImage]::Warning) Write-Verbose $MessageText } }) $AuthorTextBlock.Add_PreviewMouseDown({ Start-Process "https://twitter.com/andysvints" }) $VersionTextBlock.Add_PreviewMouseDown({ $ModuleUrl=$(Get-Module TimeTracker | Select-Object -ExpandProperty ProjectUri).ToString() Start-Process "$ModuleUrl" }) $btnStart.Add_Click({ Start-TimeTracker -Comment $($CommentTextBox.Text) $TimeTrackersListView.ItemsSource=@(Get-TimeTracker) $CommentForm.Hide() $CommentTextBox.Text="" }) $btnCloseBox.Add_Click({ $CommentForm.Hide() }) #endregion $MainForm.ShowDialog() | out-null }else{ Write-Information "Graphic User Interface is supported only on Windows Platform." } } catch { Write-Verbose "Something bad happened. Please review exception message for more details" Write-Error ("Catched Exception: - $($_.exception.message)") } } } end { } } #EndRegion '.\Public\Invoke-TimeTracker.ps1' 155 #Region '.\Public\New-DefaultConfigFile.ps1' 0 function New-DefaultConfigFile { <# .SYNOPSIS Create default config file, .DESCRIPTION This function is generating default config file used by TimeTracker module. Default config file: { "CircularLogging" : true, "LoggingLevel":"None", "TimeIncrementMins" : 30, "OutputFolder": $($env:USERPROFILE), "OutputFormat": "CSV", "Technician": "" } .EXAMPLE New-DefaultConfigFile -Verbose VERBOSE: Performing the operation "Create New Config File" on target "DefaultConfig". VERBOSE: Creating Default Config File #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] param ( ) begin { } process { if ($pscmdlet.ShouldProcess("DefaultConfig", "Create New Config File")) { Write-Verbose "Creating Default Config File" $Config=@{ "CircularLogging" = $true "LoggingLevel"="None" "TimeIncrementMins" = 30 "OutputFolder"= $home "OutputFormat"= "CSV" "Technician"= "" } Write-Verbose "Config File Location: $PSScriptRoot\TimeTracker.config" $Config |ConvertTo-Json| Out-File $PSScriptRoot\TimeTracker.config } } end { } } #EndRegion '.\Public\New-DefaultConfigFile.ps1' 56 #Region '.\Public\Start-TimeTracker.ps1' 0 function Start-TimeTracker { <# .SYNOPSIS Start TimeTracking instance. .DESCRIPTION This function is starting time tracking instance which relates to a specific actitivy. .EXAMPLE Start-TimeTracker -Comment "Reading & replying to emails" .PARAMETER Comment Description of the activity that you are treacking time for. .PARAMETER Technician Name of the Technician the is performing the activity. Is optional as it can be configured in config file. #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=0)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Alias("c")] [string]$Comment, [string]$Technician ) begin { } process { if ($pscmdlet.ShouldProcess("Comment: '$($Comment)'", "Starting TimeTracker")) { $StartDate=Get-Date $Id=New-Guid | Select-Object -ExpandProperty Guid Write-Verbose "Starting new TimeTracker Instance." $Config=Get-ConfigFile if(!(Test-Path -Path "$($Config.OutputFolder)\TimeTracker")){ New-Item -ItemType Directory -Name "TimeTracker" -Path $($Config.OutputFolder) } $p=@{ Id=$Id StartTime=$StartDate Comment=$Comment Technician=if($Technician){$Technician}else{$($Config.Technician)} } $p | ConvertTo-Json | Out-File "$($Config.OutputFolder)\TimeTracker\$Id.track" Write-Verbose "TimeTracking started: $id" } } end { } } #EndRegion '.\Public\Start-TimeTracker.ps1' 67 #Region '.\Public\Stop-TimeTracker.ps1' 0 function Stop-TimeTracker { <# .SYNOPSIS Stop TimeTracking instance .DESCRIPTION This function is stopping TimeTracking instance based on Id. .EXAMPLE Stop-TimeTracker -Guid f73e4c31-1e32-4a83-935c-14ac4bd74302 -Verbose VERBOSE: Performing the operation "Stopping TimeTracker" on target "'Id: f73e4c31-1e32-4a83-935c-14ac4bd74302'". VERBOSE: Stopping new TimeTracker Instance. VERBOSE: Config Exists VERBOSE: Config has been successfully validated. VERBOSE: Checking if C:\Users\test\TimeTracker\f73e4c31-1e32-4a83-935c-14ac4bd74302.track exists. VERBOSE: Id: f73e4c31-1e32-4a83-935c-14ac4bd74302 exists VERBOSE: TimeTracking stopped: f73e4c31-1e32-4a83-935c-14ac4bd74302 .PARAMETER Guid Id of the TimeTracking instance that need to be stopped. #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=0)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Alias("Id")] $Guid ) begin { } process { if ($pscmdlet.ShouldProcess("'Id: $($Guid)'", "Stopping TimeTracker")) { $EndDate=Get-Date Write-Verbose "Stopping new TimeTracker Instance." $Config=Get-ConfigFile Write-Verbose "Checking if $($Config.OutputFolder)\TimeTracker\$($Guid).track exists." if((Test-Path -Path "$($Config.OutputFolder)\TimeTracker\$($Guid).track" -PathType Leaf)) { Write-Verbose "Id: $Guid exists" $TrackingFile=Get-Content "$($Config.OutputFolder)\TimeTracker\$($Guid).track" | ConvertFrom-Json $TrackingFile | Add-Member -NotePropertyName "EndTime" -NotePropertyValue $EndDate $TimeElapsed=$($EndDate-$($TrackingFile.StartTime)) $MinutesSpent=if($TimeElapsed.TotalMinutes -lt $Config.TimeIncrementMins) {$Config.TimeIncrementMins} else { $TimeElapsed.TotalMinutes} $TrackingFile | Add-Member -NotePropertyName "MinutesElapsed" -NotePropertyValue $MinutesSpent $TrackingFile | Select-Object Id,StartTime,EndTime,MinutesElapsed,Comment,Technician | Export-csv -Path "$($Config.OutputFolder)\TimeTracker\TimeTrackingReport.csv" -NoTypeInformation -Append Remove-item "$($Config.OutputFolder)\TimeTracker\$($Guid).track" Write-Verbose "TimeTracking stopped: $Guid" }else{ Write-Error "Id: $Guid is not found" } } } end { } } #EndRegion '.\Public\Stop-TimeTracker.ps1' 64 #Region '.\Public\Test-ConfigFile.ps1' 0 function Test-ConfigFile { <# .SYNOPSIS Verify validity of the config file .DESCRIPTION This function is reading existing config file and validating it is valid. .EXAMPLE Test-ConfigFile -ConfigObj $ConfigObj .PARAMETER ConfigObj Config file object that has to be validated #> [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')] [OutputType([Bool])] param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=0)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Alias("cfg")] $ConfigObj ) begin { } process { if ($pscmdlet.ShouldProcess("Config: '$($ConfigObj)'", "Validate Config")) { <#DefaultConfig Template "LoggingLevel": "Verbose" "CircularLogging" = $true "TimeIncrementMins" = 30 "OutputFolder"= $($env:USERPROFILE) "OutputFormat"= "CSV" "Technician"= "" #> switch ($ConfigObj) { {$ConfigObj.LoggingLevel -ne "Verbose" -and $ConfigObj.LoggingLevel -ne "None"} { Write-Verbose "LoggingLevel key is not present or contains invalid value." return $false break } {$ConfigObj.CircularLogging -ne $true -and $ConfigObj.CircularLogging -ne $false } { Write-Verbose "CircularLogging key is not present or contains invalid value." return $false break } {$ConfigObj.TimeIncrementMins -ne 30 -and $ConfigObj.TimeIncrementMins -ne 60 } { Write-Verbose "TimeIncrementMins key is not present or contains invalid value." return $false break } {!(Test-Path -Path $ConfigObj.OutputFolder)} { Write-Verbose "OutputFolder key is not present or contains invalid value." return $false break } Default { return $true } } } } end { } } #EndRegion '.\Public\Test-ConfigFile.ps1' 76 |