PWPS_CDOT.psm1

#####################################################################################################################################################################################################################
##############################| |####################################################################################################################################################################################
############## ########| |#
########### #####| |# Name : PWPS_CDOT Script Module
######### ####### ####| |# Type : Script Module
######## ############ ####| |# Purpose : Provide Functions for CDOT PS Scripts that interact with ProjectWise
####### ##### ##### ####| |# Author : Bentley Systems, Inc.
############## ##### #####| |# Date : 09/25/2020
############# ### ######| |# Script Version : 1.0.2.3
########## #######| |# PowerShell Version : 5.0 and above
######## ##########| |# ProjectWise Version : Not needed
######## ##############| |#
######### #########| |# Requirements:
######## ###### ########| |# - PWPS_DAB Module Version 1.16.0.0 or above (Tested on Version 01.22.2.0)
####### ######### #######| |# -
####### ########## #######| |# -
###### ######### ########| |# -
###### #### ########| |# -
###### ##########| |# -
######### #############| |# -
##############################| |#
#####################################################################################################################################################################################################################
##
## Function List:
## < Get-CDOTModuleLocation : Returns the location of the PWPS_CDOT Module
## < Get-CDOTProjectNumberFromProjectNameReturns : Project Number based on first five characters of folder name
## < Out-CDOTLogFile : Logs the time and a message in a log file (Default is "$env:LOCALAPPDATA\Bentley\Logs\CDOTProjectUpdate.log")
## < Remove-DataTableDuplicateRows : Removes Duplicate Rows from a Data Table based on a Primary Key
## < Remove-DataTableRowsByCriteria : Removes Rows from a Data Table with a given criteria string
## < Select-CDOTSQLDataTable : Creates a new PowerShell Data Table from a SQL Connection and SQL Statement
## < Update-CDOTPWProjectCodes : Attempts to add a project code to items that do not have a project code in a PW Rich Projects Data Table
## < Update-CDOTPWProjectInfoFromMasterTable : Updates a PW Rich Project Report Data Table with updated Project Properties
## < Update-CDOTPWProjectFolderNameProps : Updates folder name and description based on three project properties
## < Update-CDOTPWProjectsByPath : Updates project properties for a given project type in a given path
## < Compare-CDOTStartEndDates : Used to compare date and time values to determine if a process should continue or not.
## < New-CDOTPWLogin : Attempts a PW Login and writes result to a log file
## < New-CDOTPWAssetDocument : Creates a new abstract PW Document using a datarow from a datatable
## < Update-CDOTPWAssetDocument : Updates an existing abstract PW Document using a datarow from a datatable
##
## The Following Functions were inserted from a blog by Chad Miller and are listed at the end of this module
## (see https://gallery.technet.microsoft.com/scriptcenter/4208a159-a52e-4b99-83d4-8048468d29dd)
## < Get-Type : Returns Type for a given PowerShell object
## < Out-DataTable : Creates a DataTable for an object
##
##
## Description/Notes:
## - Version 01.00.00.01
## - Writes log information to a custom log file for the following functions
## < Update-CDOTPWProjectFolderNameProps
## < Update-CDOTPWProjectsByPath
##
## - Version 1.0.1.0
## - Added the following functions - all new functions use Write-PWPSLog in PWPS_DAB
## < Compare-CDOTStartEndDates
## < New-CDOTPWLogin
##
## - Version 1.0.2.0
## - Added the following functions - all new functions use Write-PWPSLog in PWPS_DAB
## < New-CDOTPWAssetDocument
## < Update-CDOTPWAssetDocument
##
## - Version 1.0.2.1
## - Repaired call to fixed SQL Server when calling CDOT_UpdatePWStructures_v01.ps1 from CDOT_UpdatePWProjects_CallingScript_v03.ps1
##
## - Version 1.0.2.2
## - Updated CDOT_GetPWRichProjectDataTable_v01 at line 135. Project Properties were not being captured.
##
## - Version 1.0.2.3
## - PWPS_CDOT.psm1:1155 - Fixed bug that tried to make a decimal out of a date: if ($($assetRow.$columnName) -match "^[-]?[0-9.]+$")
## - CDOT_UpdatePWProjects_CallingScript_v03.ps1: 227 - Now logs if $dtProjectList cannot be populated because no applicable work areas are found in the ProjectWise datasource
##
#####################################################################################################################################################################################################################
#####################################################################################################################################################################################################################

Function Get-CDOTModuleLocation {
    <#
    .SYNOPSIS
      Returns the location of the PWPS_CDOT Module
    .INPUTS
      None
    .OUTPUTS
      Location of PWPS_CDOT Module
    .EXAMPLE
        Get-CDOTModuleLocation
        Returns the location of the PWPS_CDOT Module
    .EXAMPLE
        $CDOTModuleLocation = Get-CDOTModuleLocation
        Returns the location of the PWPS_CDOT Module as a variable
    #>

    [CMDLETBINDING()]
     $PWPS_CDOTModuleLocation = $PSScriptRoot
    return $PWPS_CDOTModuleLocation
}
Export-ModuleMember Get-CDOTModuleLocation

FUNCTION Out-CDOTLogFile {
    <#
    .SYNOPSIS
      Logs the time and a message in a log file
    .DESCRIPTION
      The function uses two parameters: a message and a log file.
      The default log file is "$env:LOCALAPPDATA\Bentley\Logs\CDOTProjectUpdate.log"
    .PARAMETER Message
      The message to write to the log file (accepts string from pipeline)
    .PARAMETER logFile
      The file to write to
    .INPUTS
      None
    .OUTPUTS
      None
    .EXAMPLE
        Out-CDOTLogFile -Message "A new message"
        <Writes to the default log file>
    .EXAMPLE
        "A new message" | Out-CDOTLogFile
        <Writes to the default log file using the pipeline>
    #>

    [CMDLETBINDING()]
    PARAM([Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)] [string]$Message, 
        [PARAMETER(Position=1, Mandatory=$false)] [string]$logFile="$env:LOCALAPPDATA\Bentley\Logs\CDOTProjectUpdate.log"
    )
    "$(Get-Date) $Message" | Out-File -FilePath $logFile -Append -Force
}
Export-ModuleMember -Function Out-CDOTLogFile

FUNCTION Remove-DataTableDuplicateRows {
    <#
    .SYNOPSIS
      Removes Duplicate Rows from a Data Table based on a Primary Key
    .DESCRIPTION
      The function uses two parameters: a select statement and a data table.
      If select statement produces a result, then the corresponding rows are deleted from the table
    .PARAMETER dt
      The data table that is processed and returned (accepts datatable from pipeline)
    .PARAMETER PrimaryKeyName
      The column name to test for duplicates
    .INPUTS
      None
    .OUTPUTS
      None
    .EXAMPLE
        Remove-DataTableDuplicateRows -dt $dtPWProjects -PrimaryKeyName "PROJECT_Project_Code"
        # Returns the first instance of each project number in $dtPWProjects
    #>

    [CMDLETBINDING()]
    PARAM([Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)] $dt, 
        [PARAMETER(Position=1, Mandatory=$true)] [string]$PrimaryKeyName
    )
   
    $_item1 = "";

    for ($m = 0; $m -lt $dt.Rows.Count; $m++)
    {
        $_rCount = 0;
        $_item1 = $dt.Rows[$m]["$PrimaryKeyName"].ToString().Trim();

        for ($k = 0; $k -lt $dt.Rows.Count; $k++)
        {
            if ($_item1 -eq $dt.Rows[$k]["$PrimaryKeyName"].ToString().Trim())
            {
                if ($_rCount -ne 0)
                {
                    $strProjectPath = "$($dt.Rows[$k].ExistingProjectPath)"
                    $strProjectPrimaryKey = "$($dt.Rows[$k].$PrimaryKeyName)"
                    if ($strProjectPrimaryKey.Length -gt 1) {
                        $strMessage = "'$strProjectPath' uses a duplicate $PrimaryKeyName ($strProjectPrimaryKey) and will not be updated."
                        Write-Warning $strMessage
                        Out-CDOTLogFile $strMessage
                    }
                    $dt.Rows.RemoveAt($k);                       

                }
                $_rCount++;

            }
        }
    }
        
    return $dt;
}
Export-ModuleMember Remove-DataTableDuplicateRows

FUNCTION Remove-DataTableRowsByCriteria 
{ 

    <#
      .SYNOPSIS
        Removes rows in a data table by a condition
      .DESCRIPTION
        Removes rows in a data table by a condition
      .PARAMETER dt
        Data table to process (accepts pipeline object)
      .PARAMETER rowCriteria
        Criteria to use in a dt.Select statement
      .INTPUTS
        dt
      .OUTPUTS
        dt
      .EXAMPLE
        Remove-DataTableRowsByCriteria -dt $dtPWProjects -rowCriteria "FolderName IS NULL"
        # Remove rows in a data table whose value is NULL in the FolderName column
      #>


    [CmdletBinding()] 
    param([Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)] $dt, 
          [Parameter(Position=1, Mandatory=$true,HelpMessage="Enter the row criteria")] [string]$rowCriteria)
 
    Begin 
    { 
    } 
    Process 
    { 
        $rowsToDelete = $dt.Select("$rowCriteria")
        foreach ($row in $rowsToDelete) { $row.Delete() }
        $dt.AcceptChanges()
    }  
      
    End 
    { 
        return $dt;
    } 

} #Remove-DataTableRowsByCriteria
Export-ModuleMember -Function Remove-DataTableRowsByCriteria


FUNCTION Update-CDOTPWProjectFolderNameProps {

    <#
      .SYNOPSIS
        Updates the folder name and description for a CDOT_INFO Project
      .DESCRIPTION
         Folder name consists of two project properties and a separator.
         Folder description is the Project Number.
         The function also removes illegal characters in the folder name based on entries in a CSV File named PWPS_CDOT_Illegal_Characters.csv.
      .PARAMETER ProjectFolderPath
        Path to the project
      .PARAMETER PWFolderNameProjectProperty01
        Project Property to begin folder name
      .PARAMETER PWProjectFolderSeparator
        Characters to separate first and second part of folder name
      .PARAMETER PWFolderNameProjectProperty02
        Project Property to end folder name
      .PARAMETER PWFolderDescriptionProjectProperty
        Project property that will be the description of the folder
      .PARAMETER illegalCharacterMapFile
        CSV File that contains two columns: IllegalCharacter and SubstitutionString
      .OUTPUTS
        None
      .EXAMPLE
        Update-CDOTPWProjectFolderNameProps
         
        Description
        -----------
        Update a Project based on its path
      #>


    [CMDLETBINDING()]
    PARAM([PARAMETER(Position=0,Mandatory=$true,HelpMessage="Enter path to project", ValueFromPipeline = $true)] [string]$ProjectFolderPath,
          [PARAMETER(Position=1,Mandatory=$false)] [string]$PWFolderNameProjectProperty01 = 'PROJECT_Project_Code',
          [PARAMETER(Position=2,Mandatory=$false)] [string]$PWProjectFolderSeparator = ' - ',
          [PARAMETER(Position=3,Mandatory=$false)] [string]$PWFolderNameProjectProperty02 = 'PROJECT_Project_Description',
          [PARAMETER(Position=4,Mandatory=$false)] [string]$PWFolderDescriptionProjectProperty = 'PROJECT_Project_Number',
          [PARAMETER(Position=5,Mandatory=$false)] [string]$illegalCharacterMapFile = "$(Get-CDOTModuleLocation)\PWPS_CDOT_Illegal_Characters.csv",
          [PARAMETER()] $logFile
    )

    BEGIN{
        if (!(Get-PWCurrentDatasource)) {Throw "A ProjectWise session is required to update Projects"}
        $illegalCharacterMap = Import-Csv($illegalCharacterMapFile)
    }
    PROCESS{
        
        try {
            
            # Get the Rich Project and its properties by folder path
            $PWProject = Get-PWRichProjects -FolderPath $ProjectFolderPath -JustOne

            # Validate Project Property Names
            $PWPropKeyNames = $PWProject.ProjectProperties.Keys

            # Ensure that the First Project Property and Second Project Property Exist
            if($($PWPropKeyNames) -match $PWFolderNameProjectProperty01 -and $($PWPropKeyNames) -match $PWFolderNameProjectProperty02)
            {
    
                # Read Existing Folder Name and Description
                $existFolderName = $PWProject.Name
                $existFolderDescription = $PWProject.Description
        
                # Create parts of folder name
                $newFolderNamePart01 = $PWProject.ProjectProperties[$PWFolderNameProjectProperty01]
                $newFolderNamePart02 = $PWProject.ProjectProperties[$PWFolderNameProjectProperty02]
        
                # Create new folder name
                $newFolderName = "$($newFolderNamePart01)$($PWProjectFolderSeparator)$($newFolderNamePart02)"

                # Replace illegal characters in folder name
                foreach ($row in $illegalCharacterMap) { $newFolderName = $newFolderName.Replace($row.IllegalCharacter,$row.SubstitutionString) }
                
                # Make folder name lower case
                $newFolderName = $newFolderName.ToLower()

                # Create new folder description
                $newFolderDescripion = $PWProject.ProjectProperties[$PWFolderDescriptionProjectProperty]

                # Update folder name if it is different than the current name
                if ($newFolderName -ne $existFolderName -or $newFolderDescripion -ne $existFolderDescription) {
                    # Update Folder Name and Description if name or description is different
                    Update-PWFolderNameProps -FolderPath $ProjectFolderPath -NewName $newFolderName -NewDescription $newFolderDescripion
                    
                    if ($newFolderName -ne $existFolderName) {
                        $strMessage = "Name for $ProjectFolderPath was changed to $newFolderName"
                        Out-CDOTLogFile $strMessage -logFile $logFile
                    }
                    
                    if ($newFolderDescripion -ne $existFolderDescription) {
                        $strMessage = "Description for $ProjectFolderPath was changed to $newFolderDescripion"
                        Out-CDOTLogFile $strMessage -logFile $logFile
                    }
                }
                else {
                    # Write-Host "FOLDER NAME NOT CHANGED!"
                }
                                        
            }
            else {
                "$PWFolderNameProjectProperty01 OR $PWFolderNameProjectProperty02 NOT FOUND..."
            }
            
        }
        catch [system.exception]
        {
            Write-Host $_
        }
        finally {
             [void]$PWProject.Dispose
             [void]$PWPropKeyNames.Dispose
             [void]$existFolderName.Dispose
             [void]$existFolderDescription.Dispose
             [void]$newFolderNamePart01.Dispose
             [void]$newFolderNamePart02.Dispose
             [void]$newFolderName.Dispose
             [void]$newFolderDescripion.Dispose
        }

        
    }
    END{
    }

}
Export-ModuleMember -Function Update-CDOTPWProjectFolderNameProps

FUNCTION Update-CDOTPWProjectsByPath {
    <#
    .SYNOPSIS
      Updates project properties for a given project type in a given path
    .DESCRIPTION
      The function finds all projects in a ProjectWise Path and updates
      the Project Propeties based using the following parameters:
       
      Datasource Definition, Master Table data table object, ProjectWise Path,
       
    .PARAMETER ProjectWisePath
      The path to process
    .PARAMETER ProjectWiseProjectType
      The Project Type to Update. Default is 'CDOT_INFO'
    .PARAMETER MasterProjectsDataTable
      The data table that contains the new values for project properties. Default is $dtCDOTDATA
    .PARAMETER PrimaryKeyName
      The column name in both tables that will be used for the merge. Default is 'PROJECT_Project_Code'
    .PARAMETER UpdateFolderNames
      When set, folder names will be updated using the project code, a separator and the project description
    .INPUTS
      None
    .OUTPUTS
      None
    .EXAMPLE
        Update-CDOTPWProjectsByPath -ProjectWisePath 'Projects\'
        # Update all projects in \Projects
    .EXAMPLE
        PS C:\>$InvalidFolderNames = "Engineering_Estimate", "Final_Engineers", "DELETE*", "*Backup"
        PS C:\>Update-CDOTPWProjectsByPath -ProjectWisePath 'Projects\' -InvalidFolderNames $InvalidFolderNames
         
        Description
        -----------
        Update all projects in \Projects except for ones with a certain folder name pattern
    #>

    [CMDLETBINDING()]
    PARAM(
        [PARAMETER(Position=0, Mandatory=$true)] [string]$ProjectWisePath,
        [PARAMETER(Position=1, Mandatory=$false)] [string]$ProjectWiseProjectType="CDOT_INFO",
        [PARAMETER(Position=2, Mandatory=$true)] $MasterProjectsDataTable,
        [PARAMETER(Position=3, Mandatory=$false)] [string]$PrimaryKeyName="PROJECT_Project_Code",
        [PARAMETER(Position=4, Mandatory=$false)] [switch]$UpdateFolderNames,
        [PARAMETER()] $logFile
    )

    BEGIN {
        if (!(Get-PWCurrentDatasource)) {Throw "A ProjectWise session is required to update Projects"}
        # Create array to hold paths that are not updated
        $foldersNotIncluded = @()
    }
    PROCESS {
        # 1. Get Projects in $PWPath
        $dtPWProjects = (Get-PWRichProjectReport -FolderPath $ProjectWisePath -ProjectType $ProjectWiseProjectType -OutputDataTables)
        if ($dtPWProjects.Rows.Count -gt 0) {
            # Process the projects
            # 2. Clean up the ProjectWise Projects Data Table
        
                # A. Select Projects with no Primary Key value and update
                Update-CDOTPWProjectCodes -projectCodeColumnName $PrimaryKeyName -dt $dtPWProjects

                # D. Remove duplicates
                Remove-DataTableDuplicateRows -dt $dtPWProjects -PrimaryKeyName $PrimaryKeyName
   
            try {
                # 3. Update Project Properties for in the data table (PW Projects with Duplicate Project Numbers will be removed)
                Update-CDOTPWProjectInfoFromMasterTable -ProjectWiseProjectsDataTable $dtPWProjects -MasterProjectsDataTable $MasterProjectsDataTable -PrimaryKeyName $PWProjectNumberProperty

                # 4. Update the ProjectWise Projects using the ProjectWise datatable
                Update-PWRichProjectsFromDataTables -Data $dtPWProjects

                # 5. Write message to log file here
                foreach ($item in $dtPWProjects) {
                    Write-Verbose "$($item.FolderName) was updated"
                    Out-CDOTLogFile "$($item.FolderName) was updated" -logFile $logFile 
                }

            }
            catch {
                $strMessage = ($dtPWProjects | Select-Object -Property ExistingProjectPath, PROJECT_Project_Code | Out-String)
                Write-Warning "Some of the following folders have duplicate Project IDs:`n$strMessage"
                Undo-PWLogin
                Throw "Exiting...'$PrimaryKeyName' does not have unique values in ProjectWise Projects Data Table"
            }
            # 5. Update the folder names and descriptions (if set)
            if ($UpdateFolderNames)
            {
                foreach ($project in $dtPWProjects) { Update-CDOTPWProjectFolderNameProps -ProjectFolderPath $project.ExistingProjectPath }
            }
        }
        else {Write-Warning "$ProjectWisePath Contains no Projects"}
    }
    END {
        if ($foldersNotIncluded.Count -gt 0) {
            # 5. Output the folders that were not updated
            $strMessage = ($foldersNotIncluded | Out-String).Replace("$PWPath"," $PWPath")
            Write-Warning "The following folders were not updated:`n$strMessage"
        }
    }
}
Export-ModuleMember Update-CDOTPWProjectsByPath

FUNCTION Update-CDOTPWProjectInfoFromMasterTable {
    <#
    .SYNOPSIS
      Updates a PW Rich Project Report Data Table with updated Project Properties
    .DESCRIPTION
      The function uses three parameters: ProjectWiseProjectsDataTable, MasterProjectsDataTable, PrimaryKeyName
      The ProjectWiseProjectsDataTable is updated with information from the MastetProejctsDataTable
    .PARAMETER ProjectWiseProjectsDataTable
      ProjectWise Rich Project Data Table for Input
    .PARAMETER MasterProjectsDataTable
      The data table that contains the new values for project properties
    .PARAMETER PrimaryKeyName
      The column name in both tables that will be used for the merge
    .INPUTS
      None
    .OUTPUTS
      None
    .EXAMPLE
        Update-CDOTPWProjectInfoFromMasterTable -MasterProjectsDataTable $dtPWProjects -PrimaryKeyName PROJECT_Project_Code -ProjectWiseProjectsDataTable $dtCDOTDATA
        # Updates the PROJECT Columns in the $dtPWProjects data table from the values in the $dtCDOTDATA data table
    .EXAMPLE
        $dtPWProjects | Update-CDOTPWProjectInfoFromMasterTable -PrimaryKeyName PROJECT_Project_Code -ProjectWiseProjectsDataTable $dtCDOTDATA
         
        Description
        -----------
        Updates the PROJECT Columns in the $dtPWProjects data table (from the pipeline) from the values in the $dtCDOTDATA data table
    #>

    [CMDLETBINDING()]
    PARAM(
        [PARAMETER(Position=0, Mandatory=$true, ValueFromPipeline = $true)] $ProjectWiseProjectsDataTable,
        [PARAMETER(Position=1, Mandatory=$true)] $MasterProjectsDataTable,
        [PARAMETER(Position=2, Mandatory=$true)] [string]$PrimaryKeyName
    )

    BEGIN
    {
        # Verify that the input objects are data tables
        if (($ProjectWiseProjectsDataTable.GetType().Name -eq 'DataTable') -eq $false) {
            Throw "PW Project Input object is not a datatable!"
        }
        if (($MasterProjectsDataTable.GetType().Name -eq 'DataTable') -eq $false) {
            Throw "Master Projects object is not a datatable!"
        }

        # Verify that both data tables use the same table name
        if (!($ProjectWiseProjectsDataTable.TableName -eq $MasterProjectsDataTable.TableName)) {
            Throw "Table Name must match in both tables"
        }

        # Try to set a primary key for both tables - remove dups in ProectWise if found
        try { $ProjectWiseProjectsDataTable.PrimaryKey = $ProjectWiseProjectsDataTable.Columns[$PrimaryKeyName] }
        catch {
            Write-Warning "$PrimaryKeyName' does not have unique values in ProjectWise Projects Data Table...removing duplicates..."
            Remove-DataTableDuplicateRows -dt $ProjectWiseProjectsDataTable -PrimaryKeyName $PrimaryKeyName
            $ProjectWiseProjectsDataTable.PrimaryKey = $ProjectWiseProjectsDataTable.Columns[$PrimaryKeyName]
        }
        try { $MasterProjectsDataTable.PrimaryKey = $MasterProjectsDataTable.Columns[$PrimaryKeyName] }
        catch {
            Write-Warning "$PrimaryKeyName' does not have unique values in Master Projects Data Table...removing duplicates..."
            Remove-DataTableDuplicateRows -dt $MasterProjectsDataTable -PrimaryKeyName $PrimaryKeyName
            $MasterProjectsDataTable.PrimaryKey = $MasterProjectsDataTable.Columns[$PrimaryKeyName]
        }
        
    }
    PROCESS
    {
        # 0. Obtain Column Names for the Project Table
        $AllColumnNames = ($ProjectWiseProjectsDataTable.Columns).ColumnName
        $ProjectPropertyColumnNames = $AllColumnNames -like 'PROJECT_*' -ne $PrimaryKeyName
        
        # 1. Remove PROJECT Columns from $dtPWProjects so the values from the input table can be added
        foreach ($colName in $ProjectPropertyColumnNames) { $ProjectWiseProjectsDataTable.Columns.Remove($colName) }
       
        # 2. Merge Columns from Master Table into Project Table
        $ProjectWiseProjectsDataTable.Merge($MasterProjectsDataTable)

        # 3. Remove all records where FolderName is Null, PROJECT_Project_Code = 0, PROJECT_Project_Status is NULL
        Remove-DataTableRowsByCriteria -dt $ProjectWiseProjectsDataTable -rowCriteria "FolderName IS NULL"
        Remove-DataTableRowsByCriteria -dt $ProjectWiseProjectsDataTable -rowCriteria "$PrimaryKeyName = 0"
        Remove-DataTableRowsByCriteria -dt $ProjectWiseProjectsDataTable -rowCriteria "PROJECT_Project_Status is NULL"
        

        # 4. Cleanup Columns that are not in the original Project Data Table
        $NewColumnNames = ($ProjectWiseProjectsDataTable.Columns).ColumnName
        foreach ($colName in $NewColumnNames) { 
            if ($colName -notin $AllColumnNames) {$ProjectWiseProjectsDataTable.Columns.Remove($colName)}
        }

        $ProjectWiseProjectsDataTable.AcceptChanges()
    }
    END
    {
        # Return the Data Table
        #Return $ProjectWiseProjectsDataTable
        #Write-Output @(,($ProjectWiseProjectsDataTable))
    }


}
Export-ModuleMember -Function Update-CDOTPWProjectInfoFromMasterTable

FUNCTION Get-CDOTProjectNumberFromProjectName ([string]$PWFolderName){
   try{
    return [int]$PWFolderName.Substring(0,5)
   }
   catch{
    return 0
   }
}
Export-ModuleMember -Function Get-CDOTProjectNumberFromProjectName

FUNCTION Update-CDOTPWProjectCodes {
    <#
    .SYNOPSIS
      Attempts to add a project code to items that do not have a project code
      in a PW Rich Projects Data Table
    .DESCRIPTION
      The function finds all records in the data table that do not have a
      project code.
       
      Once the records are found, the function attempts to ascertain the
      project code based on the first five characters of the folder name
      using the folders DMS number (o_projectno)
    .PARAMETER projectCodeColumnName
      The name of the column to update in the data table
    .PARAMETER dt
      The data table that is processed
    .INPUTS
      None
    .OUTPUTS
      None
    .EXAMPLE
        Remove-CDOTInvalidPWFolders -projectCodeColumnName PROJECT_Project_Code -dt $dtPWProjects
         
        Description
        -----------
        Update all rows from $dtPWProjects whose value for PROJECT_Project_Code is blank
    #>

    [CMDLETBINDING()]
    PARAM(
        [PARAMETER(Position=0, Mandatory=$true)] [string]$projectCodeColumnName,
        [PARAMETER(Position=1, Mandatory=$true)] $dt
    )

    $dtProjectsToUpdate = $dt.Select("$projectCodeColumnName = ''") | Out-DataTable

    try {

        if ($dtProjectsToUpdate.Rows.Count -gt 0) {
            $col = $dt.Columns[$projectCodeColumnName];
            foreach($row in $dt.Rows) {
                $row[$col] = Get-CDOTProjectNumberFromProjectName -PWFolderName $row.FolderName
            }
            Write-Host $dtProjectsToUpdate.Count "Project Codes Updated"
        }
        else {
            Write-Host "All Projects have a project Code"
        }
        
        
    }
    catch {
        $ex = $_.Exception 
        Write-Error "$ex.Message" 
        continue 
    }
}
Export-ModuleMember -Function Update-CDOTPWProjectCodes


FUNCTION Select-CDOTSQLDataTable {

    <#
      .SYNOPSIS
        Creates a new PowerShell Data Table from a SQL Connection and SQL Statement
      .DESCRIPTION
         Creates a new PowerShell Data Table from a SQL Connection and SQL Statement
      .OUTPUTS
        System.Data.DataTable
      .EXAMPLE
        Select-SQLDataTable -SQLConnectionString "Database=master; Integrated Security=true" -SqlQuery "SELECT * FROM spt_monitor"
         
        Description
        -----------
        Runs a SELECT statement on the Master database on the local server
      #>


    [CMDLETBINDING()]
    PARAM([PARAMETER(Position=0,Mandatory=$true,HelpMessage="Enter Connection String to SQL Database")] [string]$SQLConnectionString,
          [PARAMETER(Position=1,Mandatory=$true,HelpMessage="Enter SELECT Statement to generate the data table")] [string]$SqlQuery,
          [PARAMETER(Position=2,Mandatory=$true,HelpMessage="Enter the table name for the data table")] [string]$TableName
    )

    BEGIN{
        $SQLConnection = New-Object   System.Data.SqlClient.SqlConnection
        $SQLCommand = New-Object System.Data.SqlClient.SqlCommand
        $dataAdaptor = New-Object System.Data.SqlClient.SqlDataAdapter
        $dataTable = New-Object System.Data.DataTable($TableName)
    }
    PROCESS{
        
        
        
        try {
            
            $SQLConnection.ConnectionString = $SQLConnectionString
            $SQLCommand.Connection = $SQLConnection
            $SQLCommand.CommandText = $SqlQuery

            [void]$SQLConnection.Open

            $dataAdaptor.SelectCommand = $SQLCommand

            $null = $dataAdaptor.Fill($dataTable)

            [void]$SQLConnection.Close
        }
        catch [system.exception]
        {
            Write-Host $_
        }
        finally {
             [void]$dataAdaptor.Dispose
             [void]$SQLCommand.Dispose
             [void]$SQLConnection.Dispose
        }

        
    }
    END{
        # Return the Data Table
        #Return $dataTable
        Write-Output @(,($datatable)) 
    }

}
Export-ModuleMember -Function Select-CDOTSQLDataTable



############################################################################################
# Following Functions are from Chad Miller
# https://gallery.technet.microsoft.com/scriptcenter/4208a159-a52e-4b99-83d4-8048468d29dd
############################################################################################

#######################
function Get-Type 
{ 
    param($type) 
 
$types = @( 
'System.Boolean', 
'System.Byte[]', 
'System.Byte', 
'System.Char', 
'System.Datetime', 
'System.Decimal', 
'System.Double', 
'System.Guid', 
'System.Int16', 
'System.Int32', 
'System.Int64', 
'System.Single', 
'System.UInt16', 
'System.UInt32', 
'System.UInt64') 
 
    if ( $types -contains $type ) { 
        Write-Output "$type" 
    } 
    else { 
        Write-Output 'System.String' 
         
    } 
} #Get-Type
Export-ModuleMember -Function Get-Type

#######################
 
function Out-DataTable { 
<#
.SYNOPSIS
Creates a DataTable for an object
.DESCRIPTION
Creates a DataTable based on an objects properties.
.INPUTS
Object
    Any object can be piped to Out-DataTable
.OUTPUTS
   System.Data.DataTable
.EXAMPLE
$dt = Get-psdrive | Out-DataTable
This example creates a DataTable from the properties of Get-psdrive and assigns output to $dt variable
.NOTES
Adapted from script by Marc van Orsouw see link
Version History
v1.0 - Chad Miller - Initial Release
v1.1 - Chad Miller - Fixed Issue with Properties
v1.2 - Chad Miller - Added setting column datatype by property as suggested by emp0
v1.3 - Chad Miller - Corrected issue with setting datatype on empty properties
v1.4 - Chad Miller - Corrected issue with DBNull
v1.5 - Chad Miller - Updated example
v1.6 - Chad Miller - Added column datatype logic with default to string
v1.7 - Chad Miller - Fixed issue with IsArray
v1.8 (CDOT) - Adrian Crowfoot - Added optional parameter to add Table Name
.LINK
https://gallery.technet.microsoft.com/scriptcenter/4208a159-a52e-4b99-83d4-8048468d29dd
#>
 
    [CmdletBinding()] 
    param([Parameter(Position=0, Mandatory=$true, ValueFromPipeline = $true)] [PSObject[]]$InputObject, 
          [Parameter(Position=1, Mandatory=$false,HelpMessage="Enter the table name for the data table")] [string]$TableName="Table1")
 
    Begin 
    { 
        $dt = new-object Data.datatable   
        $First = $true  
    } 
    Process 
    { 
        foreach ($object in $InputObject) 
        { 
            $DR = $DT.NewRow()   
            foreach($property in $object.PsObject.get_properties()) 
            {   
                if ($first) 
                {   
                    $Col =  new-object Data.DataColumn   
                    $Col.ColumnName = $property.Name.ToString()   
                    if ($property.value) 
                    { 
                        if ($property.value -isnot [System.DBNull]) { 
                            $Col.DataType = [System.Type]::GetType("$(Get-Type $property.TypeNameOfValue)") 
                         } 
                    } 
                    $DT.Columns.Add($Col) 
                }   
                if ($property.Gettype().IsArray) { 
                    $DR.Item($property.Name) =$property.value | ConvertTo-XML -AS String -NoTypeInformation -Depth 1 
                }   
               else { 
                    $DR.Item($property.Name) = $property.value 
                } 
            }   
            $DT.Rows.Add($DR)   
            $First = $false 
        } 
    }  
      
    End 
    { 
        $dt.TableName = $TableName
        Write-Output @(,($dt)) 
    } 
 
} #Out-DataTable
Export-ModuleMember -Function Out-DataTable


FUNCTION Compare-CDOTStartEndDates {
    <#
            .SYNOPSIS
            Used to compare date and time.
            .DESCRIPTION
            Used to compare date and time values to determine if a process should continue or not.
            Returns true or false.
            .EXAMPLE
            $ReturnValue = Compare-CDOTStartEndDates -StartTime 04:00 -EndTime 20:00
    #>

    
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [Parameter(
                HelpMessage = "Start time to compare.",
                Mandatory = $true,
        Position = 0)]
        [DateTime] $StartTime,

        [ValidateNotNullOrEmpty()] 
        [Parameter(
                HelpMessage = "End time to compare.",
                Mandatory = $true,
        Position = 1)]
        [DateTime] $EndTime,
        
        [ValidateNotNullOrEmpty()]        
        [Parameter(
                HelpMessage = "Log file to write messages to.",
                Mandatory = $true,
        Position = 2)]
        [String] $LogFile
    ) # end param...

    BEGIN {
        $CmdletName = $MyInvocation.MyCommand.Name
        
        $Splat_Log = @{
            Path = $LogFile
            Cmdlet = $CmdletName
        }
        
        Write-PWPSLog @Splat_Log -Message "[BEGIN] $(Get-Date) - Entering '$CmdletName' Function..." -Level Info
    } # end BEGIN...
    
    PROCESS { 
        
        [bool] $Return = $false
        
        
        $CurrentDateTime = Get-Date
        # The current time must be after the Start time.
        <#
            No conversions are needed. Full datetimes with proper hours must be supplied.
            I think we really want this always set to tomorrow at least now so the Sat to Sun run is multiday.....
        $StartTimeMilitary = Get-Date $StartTime
        # The current time must be before the End time.
        $EndTimeMilitary = Get-Date $EndTime
         
        if($EndTimeMilitary -lt $StartTimeMilitary) {
 
            #This was the only active line until removed due to it being moved out to the file that kicks it off with a splat. $EndTimeMilitary = $EndTimeMilitary.AddDays(1)
        #}
        #>

               #Write-PWPSLog @Splat_Log -Message "cur: $CurrentDateTime start: $StartTimeMilitary end: $EndTimeMilitary" -Level Info

        if(( $CurrentDateTime -gt $StartTime ) -and ( $CurrentDateTime -lt $EndTime )) {
            $Return = $true
        }   
 
    } # end PROCESS...
    
    END {
        Write-PWPSLog -Message "[END] $(Get-Date) - Exiting '$CmdletName' Function..." -Path $LogFile -Level Info -Cmdlet $CmdletName
        Write-Output $Return
    } # end END...
} # end FUNCTION Compare-CDOTStartEndDates...
Export-ModuleMember -Function Compare-CDOTStartEndDates

FUNCTION New-CDOTPWLogin {
    [CmdletBinding()]
    param( 
        [ValidateNotNullOrEmpty()]
        [Parameter(
            HelpMessage = 'ProjectWise Datasource to log into.',
            Position = 0,
            Mandatory = $true)]
        [string] $DatasourceName,

        [ValidateNotNullOrEmpty()]
        [Parameter(
            HelpMessage = 'ProjectWise User Name.',
            Position = 0,
            Mandatory = $true)]
        [string] $UserName,

        [ValidateNotNullOrEmpty()]
        [ValidateScript( { Get-ChildItem -Path $_ -File } )]
        [Parameter(
            HelpMessage = 'Text file to get password from.',
            Position = 1,
            Mandatory = $true)]
        [string] $PWFilePathName,

        [ValidateNotNullOrEmpty()]
        [Parameter(
            HelpMessage = 'Log file path and name.',
            Position = 2,
            Mandatory = $true)]
        [string] $LogFilePathName
    ) # end param...

    BEGIN {
        $CmdletName = $MyInvocation.MyCommand.Name
        
        $Splat_Log = @{
            Path = $LogFilePathName
            Cmdlet = $CmdletName
        }
        
        Write-PWPSLog @Splat_Log -Message "[BEGIN] $(Get-Date) - Entering '$CmdletName' Function..." -Level Info
    } #end BEGIN...

    PROCESS {
        [bool] $Return = $true
        # Log into Datasource
        Write-PWPSLog @Splat_Log -Message '[PROCESS] Logging into ProjectWise' -Level Info

        # Get secure password from provided file for use with logging into ProjectWise source datasource.
        $Password = Get-SecureStringFromEncryptedFile -FileName $PWFilePathName
        if( -not (New-PWLogin -DatasourceName $DatasourceName -UserName $UserName -Password $Password)) {
            # failed to log into source datasource. Throw an error message.
            $Return = $false
        } else {
            Write-PWPSLog @Splat_Log -Message '[PROCESS] Successfully logged into ProjectWise datasource.' -Level Info
            
            # Verify in datasource
            Write-PWPSLog @Splat_Log -Message "[PROCESS] Current datasource is '$(Get-PWCurrentDatasource)'." -Level Info
            
            # Verify current user's working directory exists.
            $pwUserWorkingDirectory = Get-PWUserWorkingDirectory -UserName $UserName
            
            if( -not (Test-Path -Path $pwUserWorkingDirectory -PathType Container )) {
                Write-PWPSLog @Splat_Log -Message "[PROCESS] Working directory does not exist for current user '$((Get-PWCurrentUser).Name)'. Attempting to create." -Level Warn
                New-Item -Path $pwUserWorkingDirectory -ItemType Directory -Force
            }
        }
    } # end PROCESS...

    END {
        Write-PWPSLog @Splat_Log -Message "[END] $(Get-Date) - Exiting '$CmdletName' Function..." -Level Info
        Write-Output $Return
    } # end END...
} #end FUNCTION New-CDOTPWLogin...
Export-ModuleMember -Function New-CDOTPWLogin

function New-CDOTPWAssetDocument
{
  <#
      .SYNOPSIS
      Creates a new abstract PW Document using a datarow from a datatable
      .DESCRIPTION
      Creates a new abstract PW Document with attributes from a datarow that represents an asset
      .EXAMPLE
      New-PWAssetDocument
      explains how to use the command
      can be multiple lines
      .EXAMPLE
      New-PWAssetDocument
      another example
      can have as many examples as you like
  #>


  param
  (
    [Parameter(
        HelpMessage = 'Datarow that includes project number, asset number and asset attributes',
    Mandatory=$true, Position=0)]
    [Data.DataRow] $assetRow, # = ,
    
    [Parameter(
        HelpMessage = 'Column Name in DataRow and PW Asset Environment for the Project ID',
    Mandatory =$true, Position=1)]
    [String]$projectIDColumnName,
    
    [Parameter(
        HelpMessage = 'Column Name in DataRow and PW Asset Environment for the Asset ID',
    Mandatory =$true, Position=2)]
    [String] $assetIDColumnName,
    
    [Parameter(
        HelpMessage = 'Prefix to name each document',
        Mandatory = $true,
    Position = 3 )]
    [string]$PWDocumentNamePrefix, # ='ST',

    [Parameter(
        HelpMessage = 'PW Folder Path to place asset document',
        Mandatory = $true,
    Position = 4 )]
    [string]$PWFolderRootPath , # ='AdminConfig\Assets\',

    ###########################################################################################
    # Script and Log File Information
    ###########################################################################################
    [ValidateNotNullOrEmpty()]
    [ValidateScript( { Test-Path -Path $_ -PathType Container })]
    [Parameter( 
        HelpMessage = 'Location of script file and log folder.',
        Mandatory = $true,
    Position = 5 )]
    [string] $Path,    
    [ValidateNotNullOrEmpty()]
    [Parameter(
        HelpMessage = 'Log file to be generated for each run of the script.',
        Mandatory = $true,
    Position = 6 )]
    [string] $LogFilePathName,
    [Parameter(
        HelpMessage = 'Name of cmdlet to place in log file',
        Mandatory = $true,
    Position = 7 )]
    [string] $CmdletName
            
  )
  

  # Set Project Code, Asset ID, and Asset Name to check in document
  $projectID = $assetRow.$projectIDColumnName
  $assetID = $assetRow.$assetIDColumnName
  
  # Convert Asset ID to String if it is an integer ("00")
  if ($assetID.GetTypeCode() -eq 'Int32'){$assetID = $assetID.ToString('00')}
  $assetDocName = $projectID + '_' + $PWDocumentNamePrefix + $assetID
  
  # Create New Document
  $Splat_NewDocument = @{
    FolderPath = $PWFolderRootPath
    Count = 1
    Description = $assetDocName
    DocumentName = $assetDocName
  }
  $newAssetDocument = New-PWDocumentAbstract @Splat_NewDocument -DoNotAddSuffix
  
  
  # Create Attribute Hash table
  $htUpdatedAttributes = @{}
  foreach ($columnName in $assetRow.Table.Columns.ColumnName){
    $htUpdatedAttributes.$columnName = $assetRow.$columnName
  }
  
  #update the attribute with that value so we can see it
  Update-PWDocumentAttributes -InputDocuments $newAssetDocument -Attributes $htUpdatedAttributes -ReturnBoolean # | Out-Null
}
Export-ModuleMember -Function New-CDOTPWAssetDocument


function Update-CDOTPWAssetDocument
{
  <#
      .SYNOPSIS
      Updates an existing abstract PW Document using a datarow from a datatable
      .DESCRIPTION
      Updates an existing abstract PW Document with attributes from a datarow that represents an asset
      .EXAMPLE
      Update-PWAssetDocument
      explains how to use the command
      can be multiple lines
  #>

  param
  (
    [Parameter(
        HelpMessage = 'Existing PW Document with Asset info',
    Mandatory=$true, Position=0)]
    [PWPS_DAB.CommonTypes+ProjectWiseDocument]$assetDocument,
    [Parameter(
        HelpMessage = 'Datarow that includes project number, asset number and asset attributes',
    Mandatory=$true, Position=1)]
    [Data.DataRow] $assetRow, # = ,
    
    [Parameter(
        HelpMessage = 'Column Name in DataRow and PW Asset Environment for the Project ID',
    Mandatory =$true, Position=2)]
    [String]$projectIDColumnName,
    
    [Parameter(
        HelpMessage = 'Column Name in DataRow and PW Asset Environment for the Asset ID',
    Mandatory =$true, Position=3)]
    [String] $assetIDColumnName,
    
    [Parameter(
        HelpMessage = 'Column Name in DataRow and PW Asset Environment for the Asset Name',
    Mandatory =$true, Position=4)]
    [String] $assetNameColumnName,

    ###########################################################################################
    # Script and Log File Information
    ###########################################################################################
    [ValidateNotNullOrEmpty()]
    [ValidateScript( { Test-Path -Path $_ -PathType Container })]
    [Parameter( 
        HelpMessage = 'Location of script file and log folder.',
        Mandatory = $true,
    Position = 5 )]
    [string] $Path,    
    [ValidateNotNullOrEmpty()]
    [Parameter(
        HelpMessage = 'Log file to be generated for each run of the script.',
        Mandatory = $true,
    Position = 6 )]
    [string] $LogFilePathName,
    [Parameter(
        HelpMessage = 'Name of cmdlet to place in log file',
        Mandatory = $true,
    Position = 7 )]
    [string] $CmdletName
            
  )
  
  # Set Project Code, Asset ID, and Asset Name to check in document
  $projectID = $assetRow.$projectIDColumnName
  $assetID = $assetRow.$assetIDColumnName
  $assetName = $assetRow.$assetNameColumnName
  
  # Compare assetRow to assetDocument attributes
  $htAssetDocumentAttributes = $assetDocument.CustomAttributes
            
  # Reset $htUpdatedAttributes
  $htUpdatedAttributes = @{}
  
  
  # Compare values in each $assetRow.Column to the corresponding key in $htAssetDocumentAttributes
  foreach ($columnName in $assetRow.Table.Columns.ColumnName) {
    # Process only if NOT projectID or assetID
    if ($columnName -notin $projectIDColumnName, $assetIDColumnName) {
      
      # Reset $ValueSource and $ValueTarget
      if ($valueSource) {Remove-Variable -Name valueSource}
      if ($valueTarget) {Remove-Variable -Name valueTarget}
      
      # Determine if row column is a Value or String
      if ($($assetRow.$columnName) -match "^[-]?[0-9.]+$") {
        $valueSource = $assetRow.$columnName
        $valueTarget = [decimal]$htAssetDocumentAttributes.$columnName
      }
      elseif ($($assetRow.$columnName).GetType().Name -eq 'DBNULL'){
        # Check if Target Value is 0
        try {
          if ([int]$htAssetDocumentAttributes.$columnName -eq 0) {
            # Set Source and Target to 0
            $valueSource = 0
            $valueTarget = 0
          }
          else {
            $valueSource = 0
            $valueTarget = $htAssetDocumentAttributes.$columnName
          }
        }
        catch {
          # Use as null and null
          $valueSource = ''
          $valueTarget = ''
        }
      }
      else {
        # Treat Source value as string
        $valueSource = [string]$assetRow.$columnName
        $valueTarget = [string]$htAssetDocumentAttributes.$columnName
      }
      
      if ($valueSource -ne $valueTarget) {
        $htUpdatedAttributes.$columnName = $valueSource
      }
    }
  } # END For Each Attribute
  
  # Check $htUpdatedAttributes count
  # Update Document ONLY if Attributes were updated (ie.$htUpdatedAttributes.Count -gt 0)
  if ($htUpdatedAttributes.Count -gt 0) {
    Update-PWDocumentAttributes -InputDocuments $assetDocument -Attributes $htUpdatedAttributes | Out-Null
    $msg = ('{0}/{1} ({2}) EXISTS - The following attributes were updated:' -f $projectID, $assetID, $assetName)
    Write-PWPSLog -Message ('[PROCESS] {0}' -f $msg) -Path $LogFilePathName -Level Info -Cmdlet $CmdletName
    $msg = ("{0}/{1} ({2})'{3}'" -f $projectID, $assetID, $assetName, ($htUpdatedAttributes.Keys -join "', '"))
    Write-PWPSLog -Message ('[PROCESS] {0}' -f $msg) -Path $LogFilePathName -Level Info -Cmdlet $CmdletName
  }
  else {
    $msg = ('{0}/{1} ({2}) EXISTS - NO CHANGES MADE' -f $projectID, $assetID, $assetName)
    Write-PWPSLog -Message ('[PROCESS] {0}' -f $msg) -Path $LogFilePathName -Level Info -Cmdlet $CmdletName
  }
}
Export-ModuleMember -Function Update-CDOTPWAssetDocument