#!/usr/bin/perl
# ----------------------------------------------------------------------------
# This software is Copyright (C) 2007, All Rights Reserved
# ----------------------------------------------------------------------------
# This program is protected by local and international copyright laws. Any
# use of this program is subject to the the terms of the license agreement
# included as part of this distribution archive. Any other uses are strictly
# prohibited without the written permission of the Vendor and all
# other rights are reserved.
# -----------------------------------------------------------------------------
# Warning : Modifying this program violates your license agreement!
# -----------------------------------------------------------------------------

package IT::App;

use IT::Globals;
use Time::Local;

use strict;

# user defined globals (set by Init call)
our @REQUIRED_USER_DEFINED_GLOBALS = qw(LOGOFF_URL_REDIRECT HELP_URL);
our $LOGOFF_URL_REDIRECT;
our $HELP_URL;
our $INIT_CALLED    = 0;    # indicated whether init() has been called or not

# module globals
our $ALERT_MESSAGES;
our $ALERT_WIDTH    = 'wide';
our $DEBUG_MESSAGES;
our $DEBUG_ENABLED;

# ---------------------------------------------------------------------------------
# Function    : alert
# Description : alert messages are non-critical warning or notice messages displayed
#               "above" the web based ui for user information.  The alert function
#               adds any arguments passed to any existing alert messages and returns
#               the full message.  Alert messages are automatically displayed by
#               displayInterface();
#
#               Note: alert output  will not display HTML, it will HTML_Encode();
#
# Usage       : &IT::App::alert("message");              # add new alert
#               $alertMsg = &IT::App::alert();           # return full alert
# ---------------------------------------------------------------------------------

sub alert {
  my $msg = shift;
  my $keepHTML = shift;

  ### error checking
  &IT::Error::dieOnValidationErrors(
    ["\@_",        \@_,       "noArgsLeft"],
    ["\$keepHTML", $keepHTML, "oneOf(,keepHTML)"],
  );

  ###
  if (length $msg) {
    $msg = &IT::Common::htmlEncode($msg) unless $keepHTML;
    $msg =~ s/\n/<br \/>\n/g;  # convert nextlines to breaks
    $msg =~ s/<br \/>\n$//sg;  # remove trailing break
    if ($ALERT_MESSAGES) { $ALERT_MESSAGES .= "<br />\n"; } # add break between this and last message
    $ALERT_MESSAGES .= "$msg";
  }

  # display alert even if template module isn't loaded yet
  unless ($IT::Template::INIT_CALLED) {
    return $msg;
  }

  # return formatted alert output
  unless (length $ALERT_MESSAGES) { return; }
  my $placeholders = { 'message' => $ALERT_MESSAGES };
  my $htmlOutput;
  if    ($ALERT_WIDTH eq 'wide')   { $htmlOutput = &templateCell('admin/menus/_alerts.html', 'alertwide', $placeholders); }
  elsif ($ALERT_WIDTH eq 'narrow') { $htmlOutput = &templateCell('admin/menus/_alerts.html', 'alertnarrow', $placeholders); }
  else { die "invalid value for \$ALERT_WIDTH '$ALERT_WIDTH'!"; }

  return $htmlOutput;
}

# ----------------------------------------------------------------------------
# Function    : setAlertWidthTo
# Description : set the width of the box that alerts are displayed in
# Usage       : &setAlertWidthTo('narrow');
# ----------------------------------------------------------------------------

sub setAlertWidthTo{
  my $width = shift;

  ### error checking
  &IT::Error::dieOnValidationErrors(
    ["\@_",       \@_,    "noArgsLeft"],
    ["\$width",   $width, "oneOf(narrow, wide)"],
  );

  $ALERT_WIDTH = $width;
}

# ---------------------------------------------------------------------------------
# Function    : debug
# Description : Debug message are only displayed when debug mode is on (internally).
#               This function adds any arguments passed to it to the debug message
#               and returns the full message.  debug messages are automatically
#               displayed by displayInterface().
#
#               Note: debug output will not display HTML, it will HTML_Encode (inline);
#
# Usage       : &IT::App::debug("message");              # add new debug message
#               $debug = &IT::App::debug();              # returns all debug messages
# ---------------------------------------------------------------------------------

sub debug {
  return unless $DEBUG_ENABLED;
  my @msg = @_;
  my $msg;

  foreach $msg (@msg) {
    $msg = &IT::Common::htmlEncode($msg);
    $msg =~ s/\n/<br \/>\n/g;
    $msg =~ s/  / &nbsp;/g;
    $DEBUG_MESSAGES .= "$msg<br />\n";
  }

  # return formatted debug output
  my $htmlOutput;
  if ($DEBUG_MESSAGES) {
    $htmlOutput .= qq|<table border=1 cellspacing=0 cellpadding=4 width=590 align=center>|;
    $htmlOutput .= qq| <tr><td bgcolor="#CC0000"><font face="arial" size=2 color="#FFFFFF"><b>&nbsp;Debug Output:</b></font></td></tr>|;
    $htmlOutput .= qq| <tr><td bgcolor="#FFFFFF"><font face="Courier New, Courier" size=2>$DEBUG_MESSAGES</font></td></tr>|;
    $htmlOutput .= qq|</table>|;
  }

  return $htmlOutput;
}

# ---------------------------------------------------------------------------------
# Function    : demoModeAlertAndAction
# Description : Sets demo mode alert message, calls passed code ref and exits.
#               (Only if INI->demoMode is set
# Usage       : &IT::App::demoModeAlertAndAction("message", $codeRef)
# ---------------------------------------------------------------------------------

sub demoModeAlertAndAction {
  my $alertMessage = shift;
  my $codeRef      = shift || die "no coderef specified!";

  unless (&IT::App::inDemoMode()) { return; }

  # set alert
  &IT::App::alert($alertMessage);

  # call code ref
  &$codeRef;
  exit;

}


# ---------------------------------------------------------------------------------
# Function    : inDemoMode
# Description :
# Usage       : &IT::App::inDemoMode()
# ---------------------------------------------------------------------------------

sub inDemoMode {

  if ($INI->{'demoMode'}) { return 1; }
  else                    { return 0; }

}


# ----------------------------------------------------------------------------
# Function    : init
# Description : define filepaths and globals required by library
# Usage       : &init({
#                 'ARGUMENT' => "$value",
#               });
#
# Note: Don't call any functions that call &_die_unless_init_called()
#       from this function because then we'll get a confusing error about the init
#       function not being called instead of the actual error.
# ----------------------------------------------------------------------------

sub init {
  my $args = shift;
  if (!$args)                     { die("no arguments specified!"); }
  if (ref $args ne 'HASH')        { die("args must be passed by hash ref!"); }

  # error checking
  my($thisSubroutine) = (caller(0))[3];
  foreach (@REQUIRED_USER_DEFINED_GLOBALS) {
    unless (exists $args->{$_}) { &IT::Error::dieAsCaller("required $thisSubroutine argument '$_' not defined"); }
  }

  # set user defined globals
  $LOGOFF_URL_REDIRECT = $args->{'LOGOFF_URL_REDIRECT'};
  $HELP_URL            = $args->{'HELP_URL'};

  # error checking

  # set sub globals
  $INIT_CALLED = 1;
}

# ----------------------------------------------------------------------------
# Function    : _die_unless_init_called()
# Description : Dies unless &init() has been called.  This is called
#               first by many public function to make sure the init function
#               for the module has been called.
# Usage       : &_die_unless_init_called();
# ----------------------------------------------------------------------------

sub _die_unless_init_called {
  if ($INIT_CALLED) { return; }

  my($thisPackage) = (caller(0))[0];
  my($subroutine) = (caller(1))[3];
  &IT::Error::dieAsCaller("You must call &${thisPackage}::init() before calling $subroutine",2);

}

# ------------------------------------------------------------------------
# Function    : defineGlobalPlaceholders
# Description : define global template placeholders that will be available
#               to all core applications.
# Usage       : &IT::App::defineGlobalTemplatePlaceholders();
# ------------------------------------------------------------------------

sub defineGlobalTemplatePlaceholders {

  my $publishWebPath = $SETTINGS->{'publish_url'};
  $publishWebPath =~ s|^\w+://[^/]+/*|/|;

  # set globals
  &IT::Template::setGlobalPlaceholder('_weburl',              $SETTINGS->{'webfolder_url'});
  &IT::Template::setGlobalPlaceholder('_cgiurl',              $CGIURL);
  &IT::Template::setGlobalPlaceholder('_cgidir',              $CGIDIR);
  &IT::Template::setGlobalPlaceholder('_uploadurl',           $SETTINGS->{'upload_url'});

  &IT::Template::setGlobalPlaceholder('_publishDir',          $SETTINGS->{'publish_dir'});
  &IT::Template::setGlobalPlaceholder('_publishUrl',          $SETTINGS->{'publish_url'});
  &IT::Template::setGlobalPlaceholder('_publishWebPath',      $publishWebPath);     # global placeholder is used by v1 upgraders who had a similar placeholder

  # set '_lastUpdated' - indicates when website was last updated - set after publishing
  my $lastUpdatedDate        = $SETTINGS->{'lastUpdated'} || time();
  my $lastUpdatedPlaceholder = &IT::Template::createDatePlaceholder( $lastUpdatedDate );
  &IT::Template::setGlobalPlaceholder('_lastUpdated', $lastUpdatedPlaceholder);

  # create form data placeholders
  &IT::Template::createPrefixedGlobalPlaceholderSet('form.', $FORM);

  # create user defined placeholders
  &IT::Template::createPrefixedGlobalPlaceholderSet('my.', $SETTINGS->{'myPlaceholders'} );

  # create current user placeholders
  &IT::Template::createPrefixedGlobalPlaceholderSet('currentUser.', $CURRENT_USER) if (keys %$CURRENT_USER); # this function gets called before and after $CURRENT_USER is defined

  # create settings placeholders
  &IT::Template::createPrefixedGlobalPlaceholderSet('settings.', $SETTINGS);

  # create ini placeholders
  &IT::Template::createPrefixedGlobalPlaceholderSet('ini.', $INI);

}

# ------------------------------------------------------------------------
# Function    : getRequestedAction
# Description : Dispath actions that require login access of various levels
# Usage       : my $action = &IT::App::getRequestedAction();
# ------------------------------------------------------------------------

sub getRequestedAction {

  ### Get action
  my $action;
  if    ($FORM->{'_advancedAction'}) { $action = $FORM->{'_advancedCommand'}; } # advanced command? (only if "go" (advancedAction) is clicked)
  elsif ($FORM->{'action'})          { $action = $FORM->{'action'}; }           # explicit actions
  else                               { $action = $FORM->{'_defaultAction'}; }   # default action

  ### set default
  $action ||= '';

  ### store action as menu attribute
  &IT::App::Menu::setMenuAttribute('all', 'all', 'action', $action);

  return $action;

}


# -----------------------------------------------------------------------------
# Function    : displayInterface
# Description : Print program interface with specified content inside.
#
# Usage       : &displayInterface({
#                 'templatecell'       => 'session_expired',
#                 'template'           => 'login_pages.html',
#                 'placeholders'       => {},

#                 'selectedMenuOption' => 'login',
#                 'menuGroup'          => '',
#               });
#
# Arguments   : templatecell - template file to display
#               placeholders - hash ref of values to display on page
#               menuGroup    - menu name to display
#               selectedMenuOption  - menubar link to highlight
#               template     - additional template file to load
# -----------------------------------------------------------------------------

sub displayInterface {
  my $args = shift;

  ### error checking
  &IT::Error::dieOnValidationErrors(
    ["\@_",                            \@_,                           "noArgsLeft"],
    ["\$args",                         $args,                         "hashKeysRequired(selectedMenuOption, templatecell, template, placeholders)
                                                                       hashKeysAllowed(selectedMenuOption, menuGroup, templatecell, template, placeholders, doNotExit)"],
    ["\$args->{'selectedMenuOption'}", $args->{'selectedMenuOption'}, "notBlank"],
    ["\$args->{'templatecell'}",       $args->{'templatecell'},       "notBlank"],
    ["\$args->{'template'}",           $args->{'template'},           "notBlank"],
    ["\$args->{'placeholders'}",       $args->{'placeholders'},       "hashref"],
  );

  ### set globals
  &IT::Template::setGlobalPlaceholder('selectedMenuOption', $args->{'selectedMenuOption'});

  ### Define Content
  my $content = &templateCell($args->{'template'},
                              $args->{'templatecell'},
                              $args->{'placeholders'});

  ### Display Interface
  print &IT::Common::contentType('text/html');
  print &templateCell('admin/menus/_wrapper.html', 'header', {});        # print header
  print &IT::App::alert();                                               # display program alerts
  print $content;
  print &IT::App::debug() if $DEBUG_ENABLED;                             # display debug messages
  print &templateCell('admin/menus/_wrapper.html', 'footer', {});        # print footer

  ### Display footer tag
  my $licenseNum  = int &IT::App::License::isValidProductId($SETTINGS->{'licenseProductId'});
  my $executeTime = time() - $^T;
  print "\n<!--\n";
  print "$SETTINGS->{'programName'} v$PROGRAM_VERSION (Build: $PROGRAM_BUILD.$PROGRAM_ID.$licenseNum)\n";
  print "Licensed to: $SETTINGS->{'licenseCompanyName'} ~ $SETTINGS->{'licenseDomainName'}\n";
  print "Execute time: $executeTime seconds\n";
  print "-->\n";

  exit unless $args->{'doNotExit'};

}

# ------------------------------------------------------------------------
# Function    : Home
# Description : Display the 'home' page, with big, beautiful icons
# Usage       : &IT::App::Home($errors);
# ------------------------------------------------------------------------

sub home {
  my $error = shift;

  if ($error) { &IT::App::alert($error); }

  # define placeholders
  my $placeholders = {};

  # display page
  &displayInterface({
    'template'            => 'admin/menus/home.html',
    'templatecell'        => 'home',
    'selectedMenuOption'  => 'home',
    'menuGroup'           => '',
    'placeholders'        => $placeholders,
  });
}

# ------------------------------------------------------------------------
# Function    : displayHelp
# Description : Display documentation
# ------------------------------------------------------------------------

sub displayHelp {
  if ($HELP_URL) {
    &IT::Common::redirectBrowserToURL($HELP_URL);
  }
  else {
    &IT::App::displayInterface({
      'templatecell'       => 'nohelpavailable',
      'template'           => 'admin/menus/helpNotAvailable.html',
      'placeholders'       => {},
      'selectedMenuOption' => 'none',
      'menuGroup'          => '',
    });
  }
  exit;
}

# ------------------------------------------------------------------------------
# Function    : getPlaceholdersFromRecordFields
# Description :
# Usage       : my $placeholders = &IT::App::getPlaceholdersFromRecordFields($tableName, $recordFields, $placeholderPrefix);
# ------------------------------------------------------------------------------

sub getPlaceholdersFromRecordFields {
  my $tableName         = shift || die "no tablename specified!";
  my $recordFields      = shift || die "no field values specified!";
  my $placeholderPrefix = shift;

  # error checking
  if ($placeholderPrefix && $placeholderPrefix !~ /\.$/) { die "placeholderPrefix, if defined, must end in '.'."; }
  if (ref $recordFields ne 'HASH') { die "recordFields must be a hash reference!"; }

  my $placeholders = {};

  foreach my $fieldName (keys %$recordFields) {
    my $appFieldType    = $IT::DB::Schema::FIELD_ATTRIBUTES->{$tableName}{$fieldName}{'appFieldType'};
    my $appFieldOptions = $IT::DB::Schema::FIELD_ATTRIBUTES->{$tableName}{$fieldName}{'appFieldOptions'};
    my $fieldValue      = $recordFields->{$fieldName};

    # undefined fields
    if    ($appFieldType eq '' ||
           $appFieldType eq 'none') {
      $placeholders->{"$placeholderPrefix$fieldName"} = $fieldValue;
    }

    # textfield
    elsif ($appFieldType eq 'textfield') {
      $placeholders->{"$placeholderPrefix$fieldName"} = $fieldValue;
    }

    # textbox
    elsif ($appFieldType eq 'textbox') {
      $placeholders->{"$placeholderPrefix$fieldName"} = $fieldValue;
    }

    # date
    elsif ($appFieldType eq 'date')      {
      $placeholders->{"$placeholderPrefix$fieldName"} = '';   # default value
      my $isValidDateInteger = ($fieldValue eq int $fieldValue);
      if ($fieldValue && $isValidDateInteger) {
        $placeholders->{"$placeholderPrefix$fieldName"} = &IT::Template::createDatePlaceholder( $fieldValue );
      }
    }

    # lists
    elsif ($appFieldType eq 'list')      {
      $placeholders->{"$placeholderPrefix$fieldName"} = &IT::Template::createListPlaceholder( $fieldValue );
    }

    # checkbox
    elsif ($appFieldType eq 'checkbox')  {
      my $isChecked = $fieldValue eq '1';
      if ($isChecked) { $placeholders->{"$placeholderPrefix$fieldName"} = $appFieldOptions->{'checkbox_checkedValue'}; }
      else            { $placeholders->{"$placeholderPrefix$fieldName"} = $appFieldOptions->{'checkbox_uncheckedValue'}; }
    }

    # upload
    elsif ($appFieldType eq 'upload') {
      my $uploadPlaceholderValue;
      if ($fieldValue) { $uploadPlaceholderValue = &IT::Template::createUploadPlaceholder( $fieldValue ); }
      $placeholders->{"$placeholderPrefix$fieldName"} = $uploadPlaceholderValue;
    }

    # custom
    elsif ($appFieldType eq 'custom') {
      my $appCustomFieldType    = $IT::DB::Schema::FIELD_ATTRIBUTES->{$tableName}{$fieldName}{'appCustomFieldType'};
      my $customFieldAttributes = &IT::App::Menu::getCustomFieldType($appCustomFieldType);
      $placeholders->{"$placeholderPrefix$fieldName"} = &{$customFieldAttributes->{'getPlaceholderFromRecordField'}}($fieldValue, $recordFields);
    }

    # unknown field types
    else {
      die "unknown field type '$appFieldType' for field '$fieldName'";
    }
  }

  return $placeholders;
}

# ------------------------------------------------------------------------------
# Function    : replaceMediaPlaceholders
# Description : replace ***imagePlaceholders*** in placeholder fields
# Usage       : $placeholders = &IT::App::replaceMediaPlaceholders($tableName, $recordFields, $placeholders, $template);
# ------------------------------------------------------------------------------

sub replaceMediaPlaceholders {
  my $tableName    = shift || die "no tablename specified!";
  my $recordFields = shift || &IT::Error::dieAsCaller("no field values specified!");
  my $placeholders = shift || die "no placeholder specified!";
  my $userTemplate = shift;

  ### Create hash of embedded media placeholders and hash values
  my %mediaPlaceholders;
  foreach my $fieldName (keys %$recordFields) {
    my $appFieldType    = $IT::DB::Schema::FIELD_ATTRIBUTES->{$tableName}{$fieldName}{'appFieldType'};
    my $appFieldOptions = $IT::DB::Schema::FIELD_ATTRIBUTES->{$tableName}{$fieldName}{'appFieldOptions'};
    my $fieldValue      = $recordFields->{$fieldName};

    # skip all but embedded media fields (with content)
    my $isEmbeddedMediaField = ($appFieldType eq 'upload' && $appFieldOptions->{'upload_type'} eq 'media');
    unless ($isEmbeddedMediaField && $fieldValue) { next; }

    # get upload(s) values
    my $listStruct   = &IT::DhtmlFieldLists::convertListStringToStruct($fieldValue);
    my @uploads      = values %{$listStruct->{'rows'}};

    # assign a mediaplaceholder name and value(hash) for each upload
    foreach my $uploadHash (@uploads) {
      my $mediaPlaceholderName                  = $fieldName . $uploadHash->{'_num'};
      $mediaPlaceholders{$mediaPlaceholderName} = $uploadHash;
    }
  }

  ### Replace support media placeholder in placeholder fields
  my $globalDefaultTemplate     = "common/embeddedMedia.html";
  my $globalDefaultTemplateCell = "globalDefault.embeddedMedia";
  my $pageDefaultTemplate       = $userTemplate;
  my $pageDefaultTemplateCell   = "pageDefault.embeddedMedia";
  foreach my $name (keys %$placeholders) {
    if     (ref $placeholders->{$name}) { next; } # skip special placeholder values

    # if a break is before an image make it clear all alignments so multiple aligned images
    # in a row don't "staircase stagger" across wide screens.
    $placeholders->{$name} =~ s|<br[^>]*>(\s*\*\*\*[a-zA-Z0-9_]+\*\*\*)|<br clear="all" />\n$1|gsi;

    $placeholders->{$name} =~ s/(\*\*\*([a-zA-Z0-9_]+)(:url)?\*\*\*)/
      my $matchedName      = $2;
      my $fullPlaceholder  = $1;
      my $hasUrlModifier   = $3;
      my $replacementValue = $fullPlaceholder;

      if (exists $mediaPlaceholders{$matchedName}) {

        # generate placeholders
        my $cellPlaceholders = &IT::App::createUploadFilePlaceholders($placeholders, $mediaPlaceholders{$matchedName});

        # for ***placeholder:url*** just return webUrl
        if ($hasUrlModifier) {
          $replacementValue = $cellPlaceholders->{'file.webUrl'};
        }

        # else insert templateCell
        else {
          # Figure out which templatecell to use
          my($template, $templateCell);
          my $userTemplateCell    = "$name.embeddedMedia";
          if (&IT::Template::templateCellExists($userTemplate, $userTemplateCell)) {
            $template     = $userTemplate;
            $templateCell = $userTemplateCell;
          }
          elsif (&IT::Template::templateCellExists($pageDefaultTemplate, $pageDefaultTemplateCell)) {
            $template     = $pageDefaultTemplate;
            $templateCell = $pageDefaultTemplateCell;
          }
          else {
            $template     = $globalDefaultTemplate;
            $templateCell = $globalDefaultTemplateCell;
          }



          # get replacement content
          $replacementValue = &templateCell($template, $templateCell, $cellPlaceholders);
        }
      }
      $replacementValue;
    /ge;
  }

  return $placeholders;
}

# ------------------------------------------------------------------------------
# Function    : createUploadFilePlaceholders
# Description :
# Usage       : my $placeholders = &IT::App::createUploadFilePlaceholders($placeholders, $uploadHash);
# ------------------------------------------------------------------------------

sub createUploadFilePlaceholders {
  my $oldPlaceholders = shift || die "no placeholder specified!";
  my $uploadHash      = shift || die "no upload hash specified!";

  # copy old placeholders (don't modify original reference)
  my $newPlaceholders = { %$oldPlaceholders }; # copy hash, don't modify original reference

  # assign existing placeholders
  foreach my $name (keys %{$uploadHash}) {
    $newPlaceholders->{"file.$name"} = $uploadHash->{$name};
  }

  # assign extra placeholders
  $newPlaceholders->{"file.webUrl"}          = $SETTINGS->{'upload_url'} . $uploadHash->{'pathFromUploadDir'};
  $newPlaceholders->{"file.thumbnailWebUrl"} = $SETTINGS->{'upload_url'} . $uploadHash->{'thumbnailPathFromUploadDir'};

  return $newPlaceholders;
}

# ------------------------------------------------------------------------------
# Function    : getUserSortedFieldNames
# Description : return a list of fieldnames for table sorted in the order the user
#               specified in the field editor
# Usage       : my @sortedFieldNames = &IT::App::getUserSortedFieldNames($tableNameWithPrefix, 'INCLUDE_SCHEMA_ONLY_FIELDS');
# ------------------------------------------------------------------------------

sub getUserSortedFieldNames {
  my $tableNameWithPrefix = shift || die "no tableNameWithPrefix specified!";
  my $includeSchemaOnlyFields = shift;

  # error checking
  if ($includeSchemaOnlyFields && $includeSchemaOnlyFields ne 'INCLUDE_SCHEMA_ONLY_FIELDS') {
    die "\$includeSchemaOnlyFields must be blank or 'INCLUDE_SCHEMA_ONLY_FIELDS'!\n";
  }

  # get field list
  my @unsortedFieldNames = &IT::DB::Schema::getFieldNames($tableNameWithPrefix, $includeSchemaOnlyFields);

  # get user sort order for each field
  my %userFieldOrder;
  foreach my $fieldName (@unsortedFieldNames) {
    $userFieldOrder{$fieldName} = $IT::DB::Schema::FIELD_ATTRIBUTES->{$tableNameWithPrefix}{$fieldName}{'appDisplayOrder'};
  }

  # create sorted list
  my @sortedFieldNames = sort { $userFieldOrder{$a} <=> $userFieldOrder{$b} } @unsortedFieldNames;

  return @sortedFieldNames
}


# ------------------------------------------------------------------------------
# Function    : getRecordFieldsFromFormInput
# Description : If a recordFields value is specified then existing values will be
#               overwritten or added for any field that has form input for it.  Record fields
#               are only defined or overwritten for form field that are submitted!
# Usage       : my $recordFields = &IT::App::getRecordFieldsFromFormInput($tableName);
# Usage       : my $recordFields = &IT::App::getRecordFieldsFromFormInput($tableName, $recordFields);
# ------------------------------------------------------------------------------

sub getRecordFieldsFromFormInput {
  my $tableName    = shift || die "no tablename specified!";
  my $recordFields = shift;

  # copy existing record fields or define new hash
  if (ref $recordFields) { $recordFields = { %$recordFields }; } # copy hash values so we don't modify original reference
  else                   { $recordFields = {}; }

  # get form input for each field
  my @sortedFieldNames = &IT::App::getUserSortedFieldNames($tableName);

  foreach my $fieldName (@sortedFieldNames) {
    my($appFieldType, $appFieldOptions, $appDisplayOnly) = &IT::DB::Schema::getFieldAttributes($tableName, $fieldName, 'appFieldType', 'appFieldOptions', 'appDisplayOnly');

    # system fields
    if    ($fieldName eq 'num') {
      $recordFields->{$fieldName} ||= $FORM->{$fieldName};
    }
    elsif ($fieldName eq 'lastUpdatedTime') {
      $recordFields->{$fieldName} = $FORM->{$fieldName} if exists $FORM->{$fieldName};  # needs to be set when editing after revisionConfict
    }
    elsif ($fieldName eq 'lastUpdatedByUserNum') {
      $recordFields->{$fieldName} = $FORM->{$fieldName} if exists $FORM->{$fieldName};  # needs to be set when editing after revisionConfict
    }

    # undefined/etc fields
    elsif ($appFieldType eq '' ||
           $appFieldType eq 'none' ||
           $appDisplayOnly) {
      # ignore form input
    }

    # textfield
    elsif ($appFieldType eq 'textfield') {
      unless (exists $FORM->{$fieldName}) { next; }
      $recordFields->{$fieldName} = $FORM->{$fieldName};
    }

    # textbox
    elsif ($appFieldType eq 'textbox') {
      unless (exists $FORM->{$fieldName}) { next; }

      my $doAutoFormat = (!$appFieldOptions->{'textbox_isWysiwyg'} && !$appFieldOptions->{'textbox_noAutoFormat'});
      if ($doAutoFormat) {
        my $formattedValue = $FORM->{$fieldName};
        $formattedValue   =~ s|\n|<br/>\n|gs;  # auto-format: add break tags
        $recordFields->{$fieldName} = $formattedValue;
      }
      else {
        $recordFields->{$fieldName} = $FORM->{$fieldName};
      }

    }

    # date
    elsif ($appFieldType eq 'date') {
      unless (exists $FORM->{"$fieldName.year"}) { next; }
      my $epochTime = &getEpochTimeFromFormInputDateFields($fieldName);
      $recordFields->{$fieldName} = $epochTime;
    }

    # lists
    elsif ($appFieldType eq 'list') {
      unless (exists $FORM->{$fieldName}) { next; }
      my $listValues = join "\t", @{$FORM_ARRAYS->{$fieldName}};
      $recordFields->{$fieldName} = $listValues
    }

    # checkbox
    elsif ($appFieldType eq 'checkbox')  {
      unless (exists $FORM->{$fieldName}) { next; }
      my $isChecked = ($FORM->{$fieldName} eq '1');
      $recordFields->{$fieldName} = $isChecked ? 1 : 0;
    }

    # upload
    elsif ($appFieldType eq 'upload') {

      if (exists $FORM->{"$fieldName"}) {
        my $listStruct = &IT::DhtmlFieldLists::getListStructFromFormData($fieldName);
        my $listString = &IT::DhtmlFieldLists::convertListStructToString($listStruct);
        $recordFields->{$fieldName} = $listString;
      }
      else {
        my $isAttributeFieldsSubmitted = grep { /$fieldName:/ } keys %$FORM;  # attributes but no main field is because no uploads were submitted
        unless ($isAttributeFieldsSubmitted) { next; }
        $recordFields->{$fieldName} = '';
      }

    }

    # custom
    elsif ($appFieldType eq 'custom') {
      unless (exists $FORM->{$fieldName}) { next; }
      my($appCustomFieldType)     = &IT::DB::Schema::getFieldAttributes($tableName, $fieldName, 'appCustomFieldType');
      my $customFieldAttributes   = &IT::App::Menu::getCustomFieldType($appCustomFieldType);
      $recordFields->{$fieldName} = &{$customFieldAttributes->{'getRecordFieldFromFormInput'}}( $fieldName );
    }

    # unknown field types
    else {
      die "unknown field type '$appFieldType' for field '$fieldName'";
    }

  }

  return $recordFields;
}

# ------------------------------------------------------------------------------
# Function    : overrideRecordFieldsWithFormInput
# Description : A more descriptive name for getRecordFieldsFromFormInput
# Usage       : my $recordFields = &IT::App::overrideRecordFieldsWithFormInput($tableName);
# Usage       : my $recordFields = &IT::App::overrideRecordFieldsWithFormInput($tableName, $recordFields);
# ------------------------------------------------------------------------------

sub overrideRecordFieldsWithFormInput {
  &getRecordFieldsFromFormInput(@_);
}

# ------------------------------------------------------------------------------
# Function    : getEpochTimeFromFormInputDateFields
# Description : Converts date form fields to epoch time.  Also limits day to
#               month end (eg: feb 31 is converted to 28/29)
# Usage       : my $epochTime = &IT::App::getEpochTimeFromFormInputDateFields($fieldName);
# ------------------------------------------------------------------------------

sub getEpochTimeFromFormInputDateFields {
  my $fieldName = shift || die "no fieldname specified!";

  # get date values
  my $year    = $FORM->{"$fieldName.year"} || die "no year value specified!";
  my $mon     = $FORM->{"$fieldName.mon"} || die "no month value specified!";
  my $day     = &IT::Date::limitDayToMonthEnd($FORM->{"$fieldName.year"}, $FORM->{"$fieldName.mon"}, $FORM->{"$fieldName.day"});
  my $hour24;
  if    (exists $FORM->{"$fieldName.hour24"}) { $hour24 = $FORM->{"$fieldName.hour24"} }
  elsif (exists $FORM->{"$fieldName.hour12"}) { $hour24 = &IT::Date::convert12HourTo24Hour($FORM->{"$fieldName.hour12"}, $FORM->{"$fieldName.isPM"}); }
  else                                        { $hour24 = 0; }
  my $min     = $FORM->{"$fieldName.min"} || 0;
  my $sec     = $FORM->{"$fieldName.sec"} || 0;

  # get epoch time
  my $epochTime = eval { timegm($sec, $min, $hour24, $day, $mon-1, $year-1900); };
  if ($@) { die("invalid date (timegm error: $@)"); }

  return $epochTime;
}

# ----------------------------------------------------------------------------
# end of file
1;

