lazy100.psm1

$BoxUnassignedId = -1
$IgnoreBoxId = 0

$Script:TagBoxHT = @{}
$Script:BoxToIdHT = @{}
$Script:IdToBoxHT = @{}

function Set-DotEnv {
    param(
        $EnvFile = (Join-Path $env:USERPROFILE '.lazy100')
    )
    if (-not (Test-Path $EnvFile)) { 
        Copy-Item (Join-Path $MyInvocation.MyCommand.Module.ModuleBase 'lazy100-sample.conf') $EnvFile
        return
    }
    Get-Content $EnvFile `
    | Where-Object { $_ -match '=' -and $_ -notmatch '^#' } `
    | ForEach-Object { $name, $value = $_ -split '=', 2; [Environment]::SetEnvironmentVariable($name.Trim(), $value.Trim().Trim('"').Trim("'"), 'Process') }
}
Set-DotEnv

function Test-Configuration {
    param(
        $Variables = @("sage_host", "sage_db", "sage_tenant", "sage_authorization", "sage_employee", "manictime_exports", "lazy100_configs")
    )

    $Variables | ForEach-Object { 
        if ([string]::isNullOrEmpty([Environment]::GetEnvironmentVariable($_))) {
            Write-Warning "Environment Variable '$_' is not configured"
        }
    }
}
Test-Configuration

function Import-TagBoxMapping {
    param(
        $MappingFile = (Join-Path $env:lazy100_configs 'tags.conf' -ErrorAction SilentlyContinue)
    )
    if (-not (Test-Path $MappingFile)) { return }
    Get-Content -Path $MappingFile `
    | Where-Object { $_ -match '=' -and $_ -notmatch '^#' } `
    | ForEach-Object { $name, $value = $_ -split '=', 2; $Script:TagBoxHT[$name.Trim()] = $value.Trim().Trim('"').Trim("'") }
}
Import-TagBoxMapping

function Get-TagBoxMapping {
    param(
        $Tag
    )

    ($null -eq $Script:TagBoxHT[$Tag]) ? -1 : ($Script:TagBoxHT[$Tag] -eq "" ? 0 : $Script:TagBoxHT[$Tag])
}

function Add-TagBoxMapping {
    param(
        $Tag, $Box
    )

    $Script:TagBoxHT[$Tag] = $Box
    Export-TagBoxMapping
}

function Remove-TagBoxMapping {
    param(
        $Tag
    )
    $Script:TagBoxHT.Remove($Tag)
}

function Export-TagBoxMapping {
    param(
        $MappingFile = (Join-Path $env:lazy100_configs 'tags.conf')
    )
    $HT = $Script:TagBoxHT
    Set-Content -Path $MappingFile -Value (@($HT.Keys | Sort-Object | ForEach-Object { "$($_)=$($HT[$_])" }) -join "`n")
}

function Rename-DuplicateCsvHeaders {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [string]$Content,
        [string]$Delimiter = ','
    )

    process {
        $lines = $Content -split '\r?\n'
        if ($lines.Count -eq 0 -or $lines[0] -eq '') {
            $Content
            return
        }

        $seen = @{}
        $headers = foreach ($header in ($lines[0] -split [regex]::Escape($Delimiter))) {
            $name = $header.Trim('"')

            if ($seen.ContainsKey($name)) {
                $seen[$name]++
                "${name}_$($seen[$name])"
            }
            else {
                $seen[$name] = 1
                $name
            }
        }

        $lines[0] = $headers -join $Delimiter
        $lines -join "`n"
    }
}

function Invoke-FileChooser {
    param(
        $Path = $PWD.Path,
        $Filter,
        $Title,
        $OutputMode = 'Single'
    )

    Get-ChildItem -Path $Path -Filter $Filter `
    | Sort-Object Name -Descending `
    | Out-ConsoleGridView -Title $Title -OutputMode Single
}

function Invoke-ManicTimeCsvProcessing {
    param(
        $File
    )

    $csv = Get-Content -Path $File -Raw | Rename-DuplicateCsvHeaders | ConvertFrom-Csv

    $csv `
    | Select-Object -SkipLast 1 `
    | Select-Object `
    @{n = 'tag'; e = { $_.'Tag 1' } }, 
    @{n = 'box'; e = { Get-TagBoxMapping $_.'Tag 1' } }, 
    @{n = 'name'; e = { Get-SageActivityBox -Id (Get-TagBoxMapping $_.'Tag 1') } }, 
    @{n = 'billable'; e = { $_.Billable -ne 'Not billable' } }, 
    @{n = 'date'; e = { ($_ | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -match '\d{4}' }).Name } },
    @{n = 'total'; e = { $_.Total } },
    @{n = 'description'; e = { $_.Notes.Trim("'") } },
    @{n = 'reference'; e = { ($_.Notes -match '\[([\w\-]+)\]' | ForEach-Object { $Matches[1] }) ?? "" } }
}

function Invoke-SageRestRequest {
    param(
        $Uri,
        $Method,
        $Headers,
        $Body
    )

    if (-not $Headers) {
        $Headers = @{}
        $Headers.Add("Content-Type", "application/json")
        $Headers.Add("X-ApplicationId", "Abf")
        $headers.Add("X-MachineName", "lazy100")
        $Headers.Add("Accept", "application/json,application/atom+xml")
        $Headers.Add("User-Agent", 'lazy100')
        $Headers.Add("Authorization", $env:sage_authorization)
    }

    $response = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers -UseBasicParsing -ContentType 'application/json' -Body $Body -ErrorAction Stop
    return $response.Content ?? $response
}

function Invoke-SageActivityBoxesRequest {
    Invoke-SageRestRequest -Uri "https://$($env:sage_host)/sdata/ol/ControlCenterData/$($env:sage_db)%3B$($env:sage_tenant)/%24service/dtsADBoxSuche.106214717.LAGActivityDesk?include=%24children&language=de" -Method POST -Body '{
  "PartKey": "lstADBoxSuche.106214717.LAGActivityDesk",
  "ClientCultureName": "de",
  "Configuration": {
    "SortFieldName": "BoxMatchcode",
    "Filters": [
      {
        "FieldName": "Aktiv",
        "FilterType": "Equal",
        "FilterValue1": "-1",
        "IsFilterActive": true
      }
    ],
    "PartFields": [
      {
        "Name": "BoxID"
      },
      {
        "Name": "BoxMatchcode"
      }
    ]
  }
}'


}

function Save-SageActivityBoxes {
    param(
        $BoxSaveFile = (Join-Path $env:lazy100_configs 'boxes.conf')
    )
    try {
        $json = Invoke-SageActivityBoxesRequest | ConvertFrom-Json
    }
    catch {
        Write-Warning "Failed to get Activity boxes: $($_)"
        return
    }

    Set-Content -Path $BoxSaveFile -Value (
        @($json.Rows | ForEach-Object { "$($_.BoxId)=$($_.BoxMatchCode)" }) -join "`n"
    )
}

function Get-SageActivityBoxes {
    param(
        $BoxSaveFile = (Join-Path $env:lazy100_configs 'boxes.conf' -ErrorAction SilentlyContinue)
    )

    if (-not (Test-Path $BoxSaveFile)) { return }
    
    Get-Content $BoxSaveFile `
    | Where-Object { $_ -match '=' -and $_ -notmatch '^#' } `
    | ForEach-Object { 
        $name, $value = $_ -split '=', 2
        [PSCustomObject]@{
            Id   = $name.Trim()
            Name = $value.Trim().Trim('"').Trim("'")
        }
    }

}

function Invoke-CacheSageActivityBoxes {
    Get-SageActivityBoxes | ForEach-Object { 
        $Script:BoxToIdHT[$_.Name] = $_.Id
        $Script:IdToBoxHT[$_.Id] = $_.Name
    }
}
Invoke-CacheSageActivityBoxes

function Get-SageActivityBox {
    param(
        $Id,
        $Name
    )

    $Id ? $script:IdToBoxHT[$Id] : $script:BoxToIdHT[$Name]
}

function New-SageActivityEntry {
    param(
        [int]$Box,
        [DateTime]$Date,
        [float]$Total,
        [bool]$Billable,
        [string]$Description,
        [string]$Reference,
        [string]$Employee = $env:sage_employee,
        [bool]$TestMode = ($env:lazy100_testmode -eq 1)
    )

    # TODO: build json from Hashtable
    $RequestHT = @{
        BoxID = $Box
        Datum = $Date.ToString("yyyy-MM-dd")
        Mitarbeiter = $Employee
        Stunden = $Total
        Verrechenbar = $Billable ? -1 : 0
        Referenz = $Reference
        Beschreibung = @($Description -split "`n") -join "\n"
    }

    $RequestMethod = 'POST'
    $RequestUri = "https://$($env:sage_host)/sdata/ol/MasterData/$($env:sage_db);$($env:sage_tenant)/strADBoxBuchung.106214717.LAGActivityDesk?language=de" 

    if ($TestMode) {
        return @{
            Method = $RequestMethod
            Uri = $RequestUri
            Body = $RequestHT
        }
    }

    $headers = @{}
    $headers.Add("Accept", "application/json,application/atom+xml")
    $headers.Add("X-ApplicationId", "Abf")
    $headers.Add("X-ContextToken", "Application")
    $headers.Add("X-PartContext", "ediADBoxBuchung.106214717.LAGActivityDesk")
    $headers.Add("X-PartContextType", "DataEdit")
    $headers.Add("X-MachineName", "lazy100")
    $headers.Add("User-Agent", "JIRACustomApp")
    $headers.Add("Authorization", $env:sage_authorization)
    $headers.Add("Content-Type", "application/json")

    $RequestBody = $RequestHT | ConvertTo-Json -Compress

    Invoke-SageRestRequest -Uri $RequestUri -Method $RequestMethod -Headers $headers -Body $RequestBody
}

function Invoke-Lazy100 {
    Set-DotEnv
    $File = Invoke-FileChooser -Title 'select ManicTime export' -Filter '*.csv' -Path $env:manictime_exports
    if (-not $File) { return }

    Invoke-ManicTimeCsvProcessing -File $File `
    | Where-Object { $_.box -eq $BoxUnassignedId } `
    | Select-Object -Expand tag -Unique `
    | ForEach-Object {
        $tag = $_
        Write-Host "Tag $tag is not assigned to a box, assigning..."
        Get-SageActivityBoxes `
        | Out-ConsoleGridView -Title "Choose box for tag '$tag'" -OutputMode Single `
        | ForEach-Object {
            Add-TagBoxMapping -Tag $tag -Box $_.Id
        }
    }

    Invoke-ManicTimeCsvProcessing -File $File `
    | Where-Object { $_.box -notin @($BoxUnassignedId, $IgnoreBoxId) } `
    | Out-ConsoleGridView -Title "Select entries to create in Sage Activity" `
    | ForEach-Object {
        New-SageActivityEntry -Box $_.box -Date ([DateTime]::Parse($_.date)) -Total $_.total -Description $_.description -Reference $_.reference -Billable $_.billable
    }
}
if (-not (Get-Alias lazy100 -ErrorAction SilentlyContinue)) {
    New-Alias -Name lazy100 -Value "Invoke-Lazy100"
}