#!/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::Menu;

# load modules
use strict;

use IT::Globals;

# module globals
use vars qw($CUSTOM_FIELD_TYPES %VALID_MENU_TYPES %VALID_MENU_ATTRIBUTES $MENU_ATTRIBUTES);

$CUSTOM_FIELD_TYPES = [];

%VALID_MENU_TYPES = map { $_ => 1 } qw(list list2 add edit preview save confirmErase erase all);

%VALID_MENU_ATTRIBUTES = map { $_, 1 } qw(
  template selectedMenuOption tableNameWithPrefix menuTemplateCell
  sortBy pageNumber keyword listRowTemplateCell
  publish placeholders
  recordNum function limitUpdatableFields
  preErrorChecking postErrorChecking
  preSetGlobalPlaceholders postSetGlobalPlaceholders
  preSave postSave alwaysRequireEraseConfirm
  preErase erase postErase
  preLoadRecords loadRecords getSelectedRecords
  preRowCode
  init
  createUserSearchDbQuery saveLastQueryToCookie
  preResetSearch resetSearch postResetSearch
  action
  whereQuery recordsPerPage
  rowCode

  sqlStatement searchConditions

);
#  preRowCode rowCode postRowCode
#  preOutput Output postOutput
#  finish
$MENU_ATTRIBUTES = {};

use vars qw($DEFAULT_UPLOAD_ALIGNMENT);
$DEFAULT_UPLOAD_ALIGNMENT = 'left';

# ------------------------------------------------------------------------------
# Function    : _loadDefaultPlaceholderValues
# Description : used by menu add
# Usage       : my $placeholders = &_loadDefaultPlaceholderValues($tableNameWithPrefix, $placeholders);
# ------------------------------------------------------------------------------

sub _loadDefaultPlaceholderValues {
  my $tableNameWithPrefix = shift || die "no tableName specified!";
  my $placeholders        = shift || die "no placeholders specified!";
  $placeholders = { %$placeholders }; # copy placeholder hash so we don't modify by reference

  my @sortedFieldNames = &IT::App::getUserSortedFieldNames($tableNameWithPrefix, 'INCLUDE_SCHEMA_ONLY_FIELDS');

  foreach my $fieldName (@sortedFieldNames) {
    my($appFieldType, $appFieldOptions) = &IT::DB::Schema::getFieldAttributes($tableNameWithPrefix, $fieldName, 'appFieldType', 'appFieldOptions');
    my $fieldValue = $placeholders->{$fieldName};

    # textfield
    if ($appFieldType eq 'textfield') {
      unless (exists $placeholders->{$fieldName}) { $placeholders->{$fieldName} = $appFieldOptions->{'textfield_defaultValue'}; }
    }

    # textbox
    elsif ($appFieldType eq 'textbox') {
      unless (exists $placeholders->{$fieldName})  { $placeholders->{$fieldName} = $appFieldOptions->{'textbox_defaultValue'}; }
#      $placeholders->{'textbox_heightInPixels'} = $appFieldOptions->{'textbox_heightInPixels'} || 100;
    }
  }

  return $placeholders;
}


# ------------------------------------------------------------------------------
# Function    : _createFieldListHtml
# Description : generates fields for add/edit page.  Sets default values from
#               field editor if no values are defined for fields.
# Usage       : &_createFieldListHtml($tableNameWithPrefix, $record);
# ------------------------------------------------------------------------------

sub _createFieldListHtml {
  my $tableNameWithPrefix = shift || die "no tableName specified!";
  my $placeholders        = shift || die "no placeholders specified!";

  my @sortedFieldNames = &IT::App::getUserSortedFieldNames($tableNameWithPrefix, 'INCLUDE_SCHEMA_ONLY_FIELDS');

  my $fieldListHtml;
  foreach my $fieldName (@sortedFieldNames) {
    my($appFieldType, $appFieldLabel)        = &IT::DB::Schema::getFieldAttributes($tableNameWithPrefix, $fieldName, 'appFieldType', 'appFieldLabel');
    my($appFieldOptions, $appFieldSeparator, $appHiddenField) = &IT::DB::Schema::getFieldAttributes($tableNameWithPrefix, $fieldName, 'appFieldOptions', 'appFieldSeparator','appHiddenField');
    my $fieldValue = $placeholders->{$fieldName};

    # set field placeholders
    $placeholders->{'_fieldLabel'} = $appFieldLabel;
    $placeholders->{'_fieldName'}  = $fieldName;
    $placeholders->{'_fieldValue'} = $fieldValue;

    # get field templatecell
    my($templateCell, $fieldHTML);
    if ($appHiddenField)                 { $templateCell = 'none'; }
    elsif ($appFieldType eq '')          { $templateCell = 'none'; }
    elsif ($appFieldType eq 'none')      { $templateCell = 'none'; }

    # textfield
    elsif ($appFieldType eq 'textfield') {
      if ($appFieldOptions->{'textfield_isPasswordField'}) { $templateCell = 'textField_password'; }
      else                                                 { $templateCell = 'textField'; }
      unless (exists $placeholders->{$fieldName})          { $placeholders->{'_fieldValue'} = $appFieldOptions->{'textfield_defaultValue'}; }
    }

    # textbox
    elsif ($appFieldType eq 'textbox') {
      my $removeAutoFormatting = (!$appFieldOptions->{'textbox_isWysiwyg'} && !$appFieldOptions->{'textbox_noAutoFormat'});
      if ($removeAutoFormatting) {
        $placeholders->{'_fieldValue'} =~ s|<br/?>\n|\n|gis;   # remove auto-formatting so content is displayed as entered
      }

      if ($appFieldOptions->{'textbox_isWysiwyg'}) { $templateCell = 'textbox_wysiwyg'; }
      else                                         { $templateCell = 'textbox'; }
      unless (exists $placeholders->{$fieldName})  { $placeholders->{'_fieldValue'} = $appFieldOptions->{'textbox_defaultValue'}; }
      $placeholders->{'textbox_heightInPixels'} = $appFieldOptions->{'textbox_heightInPixels'} || 100;
    }

    # date
    elsif ($appFieldType eq 'date') {
      $placeholders      = &_createDateFieldHtml($placeholders, $appFieldOptions);
      $templateCell      = 'date';
    }

    # lists
    elsif ($appFieldType eq 'list') {
     ($placeholders, $templateCell) = &_createListFieldHtml($placeholders, $appFieldOptions, $fieldName);
    }

    # checkbox
    elsif ($appFieldType eq 'checkbox')  {
      $templateCell   = 'checkbox';
      my $checkedHtml = 'checked="checked"';
      $placeholders->{'_checked'} = '';

      # use default value when adding records
      if (!exists $placeholders->{$fieldName} && $appFieldOptions->{'checkbox_checkedByDefault'}) {
        $placeholders->{'_checked'} = $checkedHtml;
      }

      # set previous checkbox value
      # NOTE: Even though this is stored as 0 or 1 in database, placeholder value is checked or unchecked string
      elsif ($fieldValue eq $appFieldOptions->{'checkbox_checkedValue'}) {
        $placeholders->{'_checked'} = $checkedHtml;
      }

      # set placeholders
      $placeholders->{'_description'}    = $appFieldOptions->{'checkbox_description'};
      $placeholders->{'_checkedValue'}   = $appFieldOptions->{'checkbox_checkedValue'};
      $placeholders->{'_uncheckedValue'} = $appFieldOptions->{'checkbox_uncheckedValue'};

    }

    # upload
    elsif ($appFieldType eq 'upload') {
      $placeholders->{'_uploadType'}          = $appFieldOptions->{'upload_type'};
      $placeholders->{'_maxUploads'}          = $appFieldOptions->{'upload_maxUploads'};
      $placeholders->{'_highestRecordNumber'} = 0;
      $placeholders->{'_hiddenFields'}       = '';

      if (&IT::Template::getPlaceholderType($fieldValue) eq 'upload') {

        # recreate listStruct from uploadPlaceholder
        # NOTE: Another way to do this would be use the useTemplateCell modifier and list all the upload attributes in the template
        #       ... or ... we could add an attribute that output the queryEncoded attribute string and use that for generating hidden
        #       fields.
        my $listStruct = {};
        $listStruct->{'config'} = $fieldValue->{'config'};
        $listStruct->{'rows'}   = {};
        foreach my $row (@{ $fieldValue->{'rows'} }) {
          $listStruct->{'rows'}{ $row->{'_num'} } = $row;
        }

        # define placeholders
        $placeholders->{'_highestRecordNumber'} = &IT::DhtmlFieldLists::getHighestRecordNumber($listStruct);
        $placeholders->{'_hiddenFields'}        = &IT::DhtmlFieldLists::createHiddenRowFields($fieldName, $listStruct);
      }

      if    ($appFieldOptions->{'upload_type'} eq 'single')      { $templateCell = 'uploadSingle'; }
      elsif ($appFieldOptions->{'upload_type'} eq 'attachments') { $templateCell = 'uploadAttachments'; }
      elsif ($appFieldOptions->{'upload_type'} eq 'media')       { $templateCell = 'uploadEmbeddedMedia'; }
      else { die "unknown upload type '$appFieldOptions->{'upload_type'}'"; }
    }

    # custom
    elsif ($appFieldType eq 'custom') {
      my($appCustomFieldType)    = &IT::DB::Schema::getFieldAttributes($tableNameWithPrefix, $fieldName, 'appCustomFieldType');
      my $customFieldAttributes  = &IT::App::Menu::getCustomFieldType($appCustomFieldType);

      $fieldHTML = &{$customFieldAttributes->{'createFieldHtml'}}($tableNameWithPrefix, $fieldName, $placeholders);
    }

    # separators
    elsif ($appFieldType eq 'separator') {
      my $fieldSeparatorHtml;

      if    ($appFieldSeparator->{'separatorType'} eq 'blankLine') {
        $fieldSeparatorHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'separator_blankLine', {});
      }
      elsif ($appFieldSeparator->{'separatorType'} eq 'headerBar') {
        my $separatorPlaceholders = { '_headerbarTitle' => $appFieldSeparator->{'separatorHeaderText'} };
        my $seperatorBarHtml = &templateCell('admin/menus/_defaultEditFields.html', 'separator_headerBar', $separatorPlaceholders);

        &IT::Template::addTemplateCellToCache("internal", "htmlBarSeparator", $seperatorBarHtml);
        $fieldSeparatorHtml  .= &templateCell("internal", "htmlBarSeparator", $placeholders);
      }
      elsif ($appFieldSeparator->{'separatorType'} eq 'html') {
        &IT::Template::addTemplateCellToCache("internal", "htmlSeparator", $appFieldSeparator->{'separatorHTML'});
        $fieldSeparatorHtml  .= &templateCell("internal", "htmlSeparator", $placeholders);
      }

      $fieldHTML .= $fieldSeparatorHtml;
    }

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

    ### generate field
    if (!$fieldHTML && $appFieldOptions->{'useCustomFieldHtml'}) {
      $templateCell        = 'custom';
      my $relativeFilePath = 'internal';
      my $cellContent      = $appFieldOptions->{'customFieldHtml'};
      &IT::Template::addTemplateCellToCache($relativeFilePath, $templateCell, $cellContent);
      $fieldHTML           = &templateCell($relativeFilePath, $templateCell, $placeholders);
    }
    elsif (!$fieldHTML) {
      $fieldHTML           = &templateCell('admin/menus/_defaultEditFields.html', $templateCell, $placeholders);
    }



    ### set fieldlist placeholder
    &IT::Template::appendToGlobalPlaceholder("_fieldList", $fieldHTML);
  }

  return $fieldListHtml;
}


# ------------------------------------------------------------------------------
# Function    : _createDateFieldHtml
# Description :
# Usage       : $placeholders->{'dateFields'} = &_createDateFieldHtml($placeholders, $appFieldOptions);
# ------------------------------------------------------------------------------

sub _createDateFieldHtml {
  my $placeholders    = shift || die "no placeholders defined!";
  my $appFieldOptions = shift || {};

  $placeholders = { %{$placeholders} };  # copy hash values so we don't modify global

  # default to current time if no date/time value
  my $currentGmtTime               = &IT::App::Common::getTimeAdjustedToMatchGMT();
  $placeholders->{'_fieldValue'} ||= &IT::Template::createDatePlaceholder( $currentGmtTime );

  # add month and day pulldowns
  my $dateFieldHtml;
  $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'date_mon', $placeholders);
  $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'date_day', $placeholders);

  # generate year pulldown
  $placeholders->{'date_yearOptions'} = '';
  my $minYear = &IT::Common::mathMin($SETTINGS->{'date_range_year_start'}, $SETTINGS->{'date_range_year_end'});
  my $maxYear = &IT::Common::mathMax($SETTINGS->{'date_range_year_start'}, $SETTINGS->{'date_range_year_end'});
  my $epochTime = $placeholders->{'_fieldValue'}{'time'};
  my $yearValue = &IT::Date::formatDate('yyyy', $epochTime);
  foreach my $year ($minYear .. $maxYear) {
    my $selected = qq|selected="selected"| if ($yearValue == $year);
    $placeholders->{'date_yearOptions'} .= qq|  <option value="$year" $selected>$year</option>\n|;
  }
  $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'date_year', $placeholders);

  # add other date fields (hour, min, sec, am/pm)
  if ($appFieldOptions->{'date_showHourMinFields'}) {
    $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'date_spacer', $placeholders);

    # show 12 or 24 hour cell
    my($hourCell);
    if    ($SETTINGS->{'time_format_12_or_24'} == 12) { $hourCell = 'date_hour12'; }
    elsif ($SETTINGS->{'time_format_12_or_24'} == 24) { $hourCell = 'date_hour24'; }
    else { die "SETTINGS 'time_format_12_or_24' isn't defined!"; }
    $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', $hourCell, $placeholders);

    # minutes
    $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'date_min', $placeholders);

    # seconds
    if ($appFieldOptions->{'date_showSecondsField'}) {
      $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'date_sec', $placeholders);
    }

    # am/pm
    if ($SETTINGS->{'time_format_12_or_24'} == 12) {
      $dateFieldHtml .= &templateCell('admin/menus/_defaultEditFields.html', 'date_isPM', $placeholders);
    }
  }

  $placeholders->{'dateFields'} = $dateFieldHtml;

  return $placeholders;
}


# ------------------------------------------------------------------------------
# Function    : _createListFieldHtml
# Description :
# Usage       : ($placeholders->{'dateFields'}, $templateCell) = &_createListFieldHtml($placeholders, $appFieldOptions);
# ------------------------------------------------------------------------------

sub _createListFieldHtml {
  my $placeholders    = shift || die "no placeholders defined!";
  my $appFieldOptions = shift || die "no appFieldOptions specified!";
  my $fieldName       = shift || die "no fieldname specified!";

  $placeholders = { %{$placeholders} };  # copy hash values so we don't modify global

  # get list attributes
  my $singleOrMultipleChoiceList = $appFieldOptions->{'list_singleOrMultipleChoice'} || 'single';
  my $listOptions                = $appFieldOptions->{'list_options'};
  my $isSingleChoice             = ($singleOrMultipleChoiceList eq 'single');
  my $isMultipleChoice           = ($singleOrMultipleChoiceList eq 'multiple');
  my $listType                   = $isSingleChoice ? $appFieldOptions->{'list_singleType'} : $appFieldOptions->{'list_multipleType'};
  my $listRows                   = $isSingleChoice ? $appFieldOptions->{'list_singleRows'} : $appFieldOptions->{'list_multipleRows'};

  # get values
  my @selectedValues;
  @selectedValues    = @{ $placeholders->{'_fieldValue'}{'listValues'} } if exists $placeholders->{'_fieldValue'}{'listValues'};
  my %selectedValues = map { $_ => 1 } @selectedValues;

  # set defaults
  $listType ||= "pulldown";
  $listRows ||= 1;
  $listRows = 1 if ($isSingleChoice && $listType eq 'pulldown');

  # set placeholders
  $placeholders->{'_multiple'} = ($isMultipleChoice) ? 'multiple="multiple"' : '';
  $placeholders->{'_listSize'} = $listRows;

  # NOTE: multi value pulldowns don't have the first option selected by default, so
  #       we set it here so user doesn't get field not submitted errors.
  my $isNoValueSelected = @selectedValues == 0;
  my $hasDefaultValue   = grep { $_->{'selectedByDefault'} } @$listOptions;
  $placeholders->{'_blankValueSelected'} = ($isMultipleChoice && $isNoValueSelected && !$hasDefaultValue) ? 'selected="selected"' : '';

  # get templatecell name
  my $templateCell;
  if    ($listType eq 'pulldown')   { $templateCell = 'list_pulldown'; }
  elsif ($listType eq 'listbox')    { $templateCell = 'list_pulldown'; }
  elsif ($listType eq 'radios')     { $templateCell = 'list_radios'; }
  elsif ($listType eq 'checkboxes') { $templateCell = 'list_checkboxes'; }
  else { die "Unknown list type '$listType'"; }

  my $submittedValues = join ", ", keys %selectedValues;

  # add options
  $placeholders->{'_listOptions'} = '';
  foreach my $listOption (@$listOptions) {
    my $isOptionSelected = (!exists $placeholders->{$fieldName} && $listOption->{'selectedByDefault'}) || $selectedValues{ $listOption->{'value'} };
    my $optionPlaceholders = {};
    $optionPlaceholders->{'_fieldName'}   = $placeholders->{'_fieldName'};
    $optionPlaceholders->{'_optionLabel'} = $listOption->{'label'};
    $optionPlaceholders->{'_optionValue'} = $listOption->{'value'};
    $optionPlaceholders->{'_selected'}    = $isOptionSelected ? 'selected="selected"' : '';
    $optionPlaceholders->{'_checked'}     = $isOptionSelected ? 'checked="checked"' : '';

    # get option templatecell name
    my $optionTemplateCell;
    if    ($listType eq 'pulldown')   { $optionTemplateCell = 'list_pulldownOption'; }
    elsif ($listType eq 'listbox')    { $optionTemplateCell = 'list_pulldownOption'; }
    elsif ($listType eq 'radios')     { $optionTemplateCell = 'list_radio'; }
    elsif ($listType eq 'checkboxes') { $optionTemplateCell = 'list_checkboxOption'; }
    else { die "unknown list type!"; }

    # add option to list
    my $optionHtml = &templateCell('admin/menus/_defaultEditFields.html', $optionTemplateCell, $optionPlaceholders);
    $placeholders->{'_listOptions'} .= $optionHtml;
  }


  return ($placeholders, $templateCell);

}

# ------------------------------------------------------------------------------
# Function    : rotateBgColorClassGlobalPlaceholder
# Description :
# Usage       : &IT::App::Menu::rotateBgColorClassGlobalPlaceholder()
# ------------------------------------------------------------------------------

sub rotateBgColorClassGlobalPlaceholder {
  my $currentBgColorClass = &IT::Template::getGlobalPlaceholder('_bgColorClass');
  my $newBgColorClass;

  if ($currentBgColorClass eq 'listRowOdd') { $newBgColorClass = 'listRowEven'; }
  else                                      { $newBgColorClass = 'listRowOdd'; }

  &IT::Template::setGlobalPlaceholder('_bgColorClass', $newBgColorClass);
}

# ------------------------------------------------------------------------------
# Function    : _dieOnInvalidMenuAttributes
# Description :
# Usage       : &_dieOnInvalidMenuAttributes($menuName, $menuType, $attr);
# ------------------------------------------------------------------------------

sub _dieOnInvalidMenuAttributes {
  my $menuName = shift || die "no menuName specified!";
  my $menuType = shift || die "no menuType specified!";
  my $attr     = shift || die "no attr hash specified!";

  # all menus
  if (!$attr->{'template'})                                { die "menu '$menuName' doesn't have a 'template' attribute defined!"; }
  if (!&IT::Template::templateExists($attr->{'template'})) { die "menu '$menuName' template '$attr->{'template'}' doesn't exist!"; }
  if (!$attr->{'tableNameWithPrefix'})                     { die "menu '$menuName' doesn't have a 'tableNameWithPrefix' attribute defined!"; }
  if (!&IT::Template::templateCellExists($attr->{'template'}, 'revisionConflict')) { die "menu '$menuName' templateCell 'revisionConflict' doesn't exist in '$attr->{'template'}'!"; }


  # list
  if ($menuType eq 'list') {
    if (!$attr->{'listRowTemplateCell'})                     { die "menu '$menuName' doesn't have a 'listRowTemplateCell' attribute defined!"; }
    if (!&IT::Template::templateCellExists($attr->{'template'}, $attr->{'listRowTemplateCell'}))   { die "menu '$menuName' templateCell '$attr->{'listRowTemplateCell'}' doesn't exist in '$attr->{'template'}'!"; }
    if (!$attr->{'selectedMenuOption'})                      { die "menu '$menuName' doesn't have a 'selectedMenuOption' attribute defined!"; }
    if (!$attr->{'menuTemplateCell'})                        { die "menu '$menuName' doesn't have a 'menuTemplateCell' attribute defined!"; }
    if (!&IT::Template::templateCellExists($attr->{'template'}, $attr->{'menuTemplateCell'})) { die "menu '$menuName' templateCell '$attr->{'menuTemplateCell'}' doesn't exist in '$attr->{'template'}'!"; }
  }

  # add
  elsif ($menuType eq 'add') {
    if (!$attr->{'selectedMenuOption'})                      { die "menu '$menuName' doesn't have a 'selectedMenuOption' attribute defined!"; }
    if (!$attr->{'menuTemplateCell'})                        { die "menu '$menuName' doesn't have a 'menuTemplateCell' attribute defined!"; }
    if (!&IT::Template::templateCellExists($attr->{'template'}, $attr->{'menuTemplateCell'})) { die "menu '$menuName' templateCell '$attr->{'menuTemplateCell'}' doesn't exist in '$attr->{'template'}'!"; }
  }

  # edit
  elsif ($menuType eq 'edit') {
    if (!$attr->{'selectedMenuOption'})                      { die "menu '$menuName' doesn't have a 'selectedMenuOption' attribute defined!"; }
    if (!$attr->{'recordNum'})                               { die "No record number specified!"; }
    if ($attr->{'recordNum'} ne int $attr->{'recordNum'})    { die "Record number must be an integer!"; }
    if (!$attr->{'menuTemplateCell'})                        { die "menu '$menuName' doesn't have a 'menuTemplateCell' attribute defined!"; }
    if (!&IT::Template::templateCellExists($attr->{'template'}, $attr->{'menuTemplateCell'})) { die "menu '$menuName' templateCell '$attr->{'menuTemplateCell'}' doesn't exist in '$attr->{'template'}'!"; }
  }

  # preview
  elsif ($menuType eq 'preview') {
    if (!$attr->{'menuTemplateCell'})                        { die "menu '$menuName' doesn't have a 'menuTemplateCell' attribute defined!"; }
    if (!&IT::Template::templateCellExists($attr->{'template'}, $attr->{'menuTemplateCell'})) { die "menu '$menuName' templateCell '$attr->{'menuTemplateCell'}' doesn't exist in '$attr->{'template'}'!"; }
  }

  # save
  elsif ($menuType eq 'save') {
    if (!$attr->{'selectedMenuOption'})                      { die "menu '$menuName' doesn't have a 'selectedMenuOption' attribute defined!"; }
    if ($attr->{'recordNum'} &&
        $attr->{'recordNum'} ne int $attr->{'recordNum'})    { die "Record number must be an integer!"; }
  }

  # confirmErase
  elsif ($menuType eq 'confirmErase') {
    if (!$attr->{'selectedMenuOption'})                      { die "menu '$menuName' doesn't have a 'selectedMenuOption' attribute defined!"; }
    if (!$attr->{'menuTemplateCell'})                        { die "menu '$menuName' doesn't have a 'menuTemplateCell' attribute defined!"; }
    if (!&IT::Template::templateCellExists($attr->{'template'}, $attr->{'menuTemplateCell'})) { die "menu '$menuName' templateCell '$attr->{'menuTemplateCell'}' doesn't exist in '$attr->{'template'}'!"; }
  }

  # erase
  elsif ($menuType eq 'erase') {
    if (!$attr->{'selectedMenuOption'})                      { die "menu '$menuName' doesn't have a 'selectedMenuOption' attribute defined!"; }
  }

  # unknown
  else {
    die "unknown menuType '$menuType'!";
  }

}

# ------------------------------------------------------------------------------
# Function    : addCustomFieldType
# Description :
# Usage       : &IT::App::Menu::addCustomFieldType($args);
# ------------------------------------------------------------------------------

sub addCustomFieldType {
  my $attr = shift || die "no attributes specified!";

  # error checking
  &IT::Error::dieOnValidationErrors(
    ["\@_",                     \@_,           "noArgsLeft"],
    ["\$attr",                  $attr,         "hashKeysRequired(name, label, createFieldHtml, getRecordFieldFromFormInput, getPlaceholderFromRecordField, errorCheckingForUserInput)"],
    ["\$attr",                  $attr,          "hashKeysAllowed(name, label, createFieldHtml, getRecordFieldFromFormInput, getPlaceholderFromRecordField, errorCheckingForUserInput, errorCheckingForFieldEditor, validDatabaseFieldTypes, postSaveOperations)"],
    ["createFieldHtml", $attr->{'createFieldHtml'}, "codeRef"],
  );

  # add custom field type
  my $thisCustomField = {};
  $thisCustomField->{'name'}                           = $attr->{'name'};
  $thisCustomField->{'label'}                          = $attr->{'label'};
  $thisCustomField->{'createFieldHtml'}                = $attr->{'createFieldHtml'};
  $thisCustomField->{'getRecordFieldFromFormInput'}    = $attr->{'getRecordFieldFromFormInput'};
  $thisCustomField->{'getPlaceholderFromRecordField'}  = $attr->{'getPlaceholderFromRecordField'};
  $thisCustomField->{'errorCheckingForUserInput'}      = $attr->{'errorCheckingForUserInput'};
  $thisCustomField->{'errorCheckingForFieldEditor'}    = $attr->{'errorCheckingForFieldEditor'};
  $thisCustomField->{'validDatabaseFieldTypes'}        = $attr->{'validDatabaseFieldTypes'};
  $thisCustomField->{'postSaveOperations'}             = $attr->{'postSaveOperations'};

  #
  push @$CUSTOM_FIELD_TYPES, $thisCustomField;

}

# ------------------------------------------------------------------------------
# Function    : getCustomFieldType
# Description :
# Usage       : my $customFieldAttributes = &IT::App::Menu::getCustomFieldType($typeName);
# ------------------------------------------------------------------------------

sub getCustomFieldType {
  my $typeName = shift || die "no custom field name specified!";

  # get hash of fields by name
  my %customFieldsByName;
  foreach my $customFieldType (@$CUSTOM_FIELD_TYPES) {
    $customFieldsByName{$customFieldType->{'name'}} = $customFieldType;
  }

  # error checking
  if (!exists $customFieldsByName{$typeName}) { die "Couldn't find custom field type '$typeName'!"; }

  return $customFieldsByName{$typeName};
}

# ------------------------------------------------------------------------------
# Function    : listCustomFieldTypes
# Description :
# Usage       : my $customFieldsByName = &IT::App::Menu::listCustomFieldTypes();
# ------------------------------------------------------------------------------

sub listCustomFieldTypes {
  return $CUSTOM_FIELD_TYPES;
}


# ------------------------------------------------------------------------------
# Function    : setMenuAttribute
# Description : Define or override a menu attribute or handler for the specified
#               menu name and/or type.
#               Valid Types: all, list, add, edit, preview, save, confirmErase, erase
# Usage       : &IT::App::Menu::setMenuAttribute($menuName, $menuType, $name, $value);
# ------------------------------------------------------------------------------

sub setMenuAttribute {
  unless (@_ == 4) { die "setMenuAttribute takes 4 arguments!"; }
  my($menuName, $menuType, $name, $value) = @_;

  unless ($VALID_MENU_ATTRIBUTES{$name}) { die "'$name' isn't a valid menu attribute name!"; }

  # set menu event handler
  $MENU_ATTRIBUTES->{$menuName}{$menuType}{$name} = $value;
}

# ------------------------------------------------------------------------------
# Function    : getMenuAttribute
# Description :
# Usage       : my $attr = &IT::App::Menu::getMenuAttributes($menuName, $menuType);
# ------------------------------------------------------------------------------

sub getMenuAttributes {
  my $menuName       = shift || die "menuName not specified";
  my $menuType       = shift || die "menuType not specified";

  # error checking
  if (!$VALID_MENU_TYPES{$menuType}) { die "menuType '$menuType' isn't a valid menu type!"; }

  # copy attributes
  my %validMenuNames = map { $_ => 1 } ($menuName, 'all');
  my %validMenuTypes = map { $_ => 1 } ($menuType, 'all');
  my $attr;
  foreach my $thisMenuName (keys %$MENU_ATTRIBUTES) {
    unless ($validMenuNames{$thisMenuName}) { next; }

    foreach my $thisMenuType (keys %{$MENU_ATTRIBUTES->{$thisMenuName}}) {
      unless ($validMenuTypes{$thisMenuType}) { next; }

      foreach my $thisAttrName (keys %{$MENU_ATTRIBUTES->{$thisMenuName}{$thisMenuType}}) {
        my $thisAttrValue      = $MENU_ATTRIBUTES->{$thisMenuName}{$thisMenuType}{$thisAttrName};
        $attr->{$thisAttrName} = $thisAttrValue;
      }
    }
  }

  return $attr;
}

# ------------------------------------------------------------------------
# Function    : list
# Description : Display a list of records in a table
#               NOTE: Keep handlers commented out till we use them in an application
#               in case we need to re-arrange them later so we don't need to support
#               handlers we may have never needed or used
# ------------------------------------------------------------------------

sub list {
  my $menuName = shift || die "no menuName defined!";
  my $errors   = shift;
  my $attr     = &getMenuAttributes($menuName, 'list');

  ### override function (only needed for functions than are called directly by other menu functions: list, add, edit, erase)
  if ($attr->{'function'}) { return &{$attr->{'function'}}($menuName, $errors); }

  # display alerts
  &IT::App::alert($errors);

  ### new search
  if ($attr->{'action'} eq "${menuName}Search") {
    $attr->{'pageNumber'} = 1;
  }

  ### reset search
  if ($attr->{'action'} eq "${menuName}ResetSearch") {
    if ($attr->{'preResetSearch'})  { &{$attr->{'preResetSearch'}}($attr); }
    if ($attr->{'resetSearch'})     { &{$attr->{'resetSearch'}}($attr); }
    else {
      $attr->{'keyword'}    = '';
      $attr->{'pageNumber'} = 1;
    }
    if ($attr->{'postResetSearch'}) { &{$attr->{'postResetSearch'}}($attr); }
  }

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}              = $menuName;
  $attr->{'placeholders'}          ||= {};
  $attr->{'sortBy'}                ||= &IT::DB::Schema::getTableAttributes($attr->{'tableNameWithPrefix'}, 'defaultSort') || ['num'];
  $attr->{'pageNumber'}            ||= $FORM->{'_pagenum'} || &IT::App::Menu::List::loadLastQueryValueFromCookie($attr->{'menuName'}, 'pageNum') || 1;
  $attr->{'recordsPerPage'}        ||= $FORM->{'_perpage'} || &IT::App::Menu::List::loadLastQueryValueFromCookie($attr->{'menuName'}, 'perPage') || 20;
  $attr->{'keyword'}               = &firstDefinedValue($attr->{'keyword'}, $FORM->{'_keyword'}, &IT::App::Menu::List::loadLastQueryValueFromCookie($attr->{'menuName'}, 'keyword'));
  $attr->{'menuTemplateCell'}      ||= 'list';
  $attr->{'listRowTemplateCell'}   ||= 'listrow';
  $attr->{'whereQuery'}            ||= ''; # by default list all records
  $attr->{'userSearchDbQuery'}       ||= ''; # by default list all records
  &_dieOnInvalidMenuAttributes($menuName, 'list', $attr);
  ### Note: Don't use reference FORM vars anywhere below here, use $attr instead!

  ### init
  if ($attr->{'init'})  { &{$attr->{'init'}}($attr); }

  ### Set global placeholders (override these in init if needed)
  if ($attr->{'preSetGlobalPlaceholders'}) { &{$attr->{'preSetGlobalPlaceholders'}}($attr); }
  {
    &IT::Template::setGlobalPlaceholder('_menuName', $attr->{'menuName'});
    &IT::Template::setGlobalPlaceholder('_keyword', $attr->{'keyword'});
    &IT::Template::setGlobalPlaceholder('_perpage', $attr->{'recordsPerPage'});
    $attr->{'placeholders'}{'_listrows'} ||= '';
  }
  if ($attr->{'postSetGlobalPlaceholders'}) { &{$attr->{'postSetGlobalPlaceholders'}}($attr); }


  ### Save search values cookie
  if ($attr->{'saveLastQueryToCookie'}) { &{$attr->{'saveLastQueryToCookie'}}($attr); }
  else {
    &IT::App::Menu::List::saveLastQueryToCookie({
      menuName => $attr->{'menuName'},
      keyword  => $attr->{'keyword'},
      perPage  => $attr->{'recordsPerPage'},
      pageNum  => $attr->{'pageNumber'},
    });
  }



  ### create user search query
  if ($attr->{'createUserSearchDbQuery'})     { &{$attr->{'createUserSearchDbQuery'}}($attr); }
  else {
    $attr->{'userSearchDbQuery'} = &IT::App::Menu::List::createUserSearchDbQueryFromKeyword($attr);
  }


  ### Rowcode - this block is executed below by listpage for each record on list page
  $attr->{'outerRowCode'} = sub {
    my $record = shift; # get record
    $attr->{'cellPlaceholders'} = {};

    if ($attr->{'preRowCode'}) { &{$attr->{'preRowCode'}}($attr, $record); }
    if ($attr->{'rowCode'})    { &{$attr->{'rowCode'}}($attr, $record); }
    else {
      &IT::App::Menu::rotateBgColorClassGlobalPlaceholder();

      my $recordPlaceholders      = &IT::App::getPlaceholdersFromRecordFields($attr->{'tableNameWithPrefix'}, $record);
      $attr->{'cellPlaceholders'} = { %{$attr->{'cellPlaceholders'}}, %$recordPlaceholders }; # recordPlaceholders override others
      my $listRow                 = &IT::Template::templateCell($attr->{'template'}, $attr->{'listRowTemplateCell'}, $attr->{'cellPlaceholders'});

      &IT::Template::appendToGlobalPlaceholder('_listRows', $listRow);
    }
    if ($attr->{'postRowCode'}) { &{$attr->{'postRowCode'}}($attr, $record); }
  };

  ### Load Records
  if ($attr->{'loadRecords'}) { $attr->{'pageInfo'} = &{$attr->{'loadRecords'}}($attr); }
  else {

    &IT::FileLock::getLock('shared');
    eval {
      $attr->{'pageInfo'} = &IT::DB::listPage({   # execute rowcode for matching records
        'tableName'       => $attr->{'tableNameWithPrefix'},
        'whereQuery'      => $attr->{'whereQuery'},
        'userSearchQuery' => $attr->{'userSearchDbQuery'},
        'sortBy'          => $attr->{'sortBy'},
        'pageNumber'      => $attr->{'pageNumber'},
        'recordsPerPage'  => $attr->{'recordsPerPage'},
        'rowcode'         => $attr->{'outerRowCode'},
      });
    };
    &IT::FileLock::releaseLock();

    # catch invalid page number eval errors
    if ($@ =~ /Invalid page number '(\d+)'/) {
      my $selectedPageNumber = $1;
      &setMenuAttribute($menuName, 'list', 'pageNumber', '1');
      &list($menuName, "Invalid page number '$selectedPageNumber', returning to page 1!");
    }
    elsif ($@) { die $@; }
  }


  ### Generate Output
#  if ($attr->{'preOutput'}) { &{$attr->{'preOutput'}}($attr); }
#  if ($attr->{'output'})    { &{$attr->{'output'}}($attr); }
#  else {
    # set pageInfo Placeholders
    &IT::Template::setGlobalPlaceholder('_recordCount',    $attr->{'pageInfo'}{'recordCount'});
    &IT::Template::setGlobalPlaceholder('_searchCount',    $attr->{'pageInfo'}{'searchCount'});
    &IT::Template::setGlobalPlaceholder('_totalPages',     $attr->{'pageInfo'}{'totalPages'});
    &IT::Template::setGlobalPlaceholder('_pageNumber',     $attr->{'pageInfo'}{'pageNumber'});
    &IT::Template::setGlobalPlaceholder('_nextPageNumber', $attr->{'pageInfo'}{'nextPageNumber'});
    &IT::Template::setGlobalPlaceholder('_prevPageNumber', $attr->{'pageInfo'}{'prevPageNumber'});

    # Display page
    &IT::App::displayInterface({
      'template'           => $attr->{'template'},
      'templatecell'       => $attr->{'menuTemplateCell'},
      'placeholders'       => $attr->{'placeholders'},
      'selectedMenuOption' => $attr->{'selectedMenuOption'},
    });
#  }
#  if ($attr->{'postOutput'}) { &{$attr->{'postOutput'}}($attr); }

#  if ($attr->{'finish'}) { &{$attr->{'finish'}}($attr); }
}


# ------------------------------------------------------------------------
# Function    : list2
# Description : Display a list of records in a table
#               NOTE: Keep handlers commented out till we use them in an application
#               in case we need to re-arrange them later so we don't need to support
#               handlers we may have never needed or used
# ------------------------------------------------------------------------

sub list2 {
  my $menuName = shift || die "no menuName defined!";
  my $errors   = shift;
  my $attr     = &getMenuAttributes($menuName, 'list2');

  ### override function (only needed for functions than are called directly by other menu functions: list, add, edit, erase)
  if ($attr->{'function'}) { return &{$attr->{'function'}}($menuName, $errors); }

  # display alerts
  &IT::App::alert($errors);

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}               = $menuName;
  $attr->{'placeholders'}           ||= {};
  $attr->{'_pageNum'}               ||= &IT::App::Menu::List2::getSearchValueFromFormOrCookie($menuName, '_pageNum') || 1;
  $attr->{'_perPage'}               ||= &IT::App::Menu::List2::getSearchValueFromFormOrCookie($menuName, '_perPage') || 20;
  $attr->{'_keyword'}               ||= &IT::App::Menu::List2::getSearchValueFromFormOrCookie($menuName, '_keyword');
  $attr->{'menuTemplateCell'}       ||= 'list';
  $attr->{'listRowTemplateCell'}    ||= 'listrow';
  $attr->{'sortBy'}                 ||= &IT::DB::Schema::getTableAttributes($attr->{'tableNameWithPrefix'}, 'defaultSort') || ['num'];
  $attr->{'sqlOrderBy'}             ||= &IT::DB::MySQL::buildSortString($attr->{'sortBy'});
  $attr->{'sqlStatement'}           ||= "SELECT * FROM $attr->{'tableNameWithPrefix'} WHERE /*SEARCH_CONDITIONS*/";
  $attr->{'searchConditions'}       ||= "1";
  $attr->{'saveCookiesFor'}         ||= ['menuName', '_keyword', '_perPage', '_pageNum'];
  $attr->{'setValuesOnNewSearch'}   ||= { _pageNum => 1 };
  $attr->{'setValuesOnResetSearch'} ||= { _pageNum => 1, _keyword => '' };

  ### Set values on "New Search" and "Reset Search"
  if ($attr->{'action'} eq "${menuName}Search") {
    my $newSearchValues            = $attr->{'setValuesOnNewSearch'};
    @$attr{keys %$newSearchValues} = values %$newSearchValues;  # fast hash merge using hash slice
  }
  if ($attr->{'action'} eq "${menuName}ResetSearch") {
    my $resetSearchValues = $attr->{'setValuesOnResetSearch'};
    @$attr{keys %$resetSearchValues} = values %$resetSearchValues;  # fast hash merge using hash slice
  }

  ### init
  if ($attr->{'init'})  { &{$attr->{'init'}}($attr); }

  ### Set global placeholders (override these in init if needed)
  if ($attr->{'preSetGlobalPlaceholders'}) { &{$attr->{'preSetGlobalPlaceholders'}}($attr); }
  {
    foreach my $fieldname (@{$attr->{'saveCookiesFor'}}) {
      &IT::Template::setGlobalPlaceholder($fieldname, $attr->{$fieldname});
    }
    &IT::Template::setGlobalPlaceholder('_menuName', $attr->{'menuName'});

  }
  if ($attr->{'postSetGlobalPlaceholders'}) { &{$attr->{'postSetGlobalPlaceholders'}}($attr); }

  ### Save search values cookie
  my %cookiesToSave = map { $_ => $attr->{$_} } @{$attr->{'saveCookiesFor'}};
  &IT::App::Menu::List::saveLastQueryToCookie(\%cookiesToSave);


  ### create count sql statements
  my $dbh           = &IT::DB::MySQL::connectIfNeeded();

  my $recordCountQuery = $attr->{'sqlStatement'};
  $recordCountQuery    =~ s|/\*SEARCH_CONDITIONS\*/|1|g                       || die "Couldn't find required placeholder /*SEARCH_CONDITIONS*/ in sql statement!\n";
  $recordCountQuery    =~ s/^SELECT.+?FROM/SELECT COUNT(*) FROM/          || die "Couldn't insert 'SELECT COUNT(*) FROM' into sql statement!\n";

  my $searchCountQuery = $attr->{'sqlStatement'};
  $searchCountQuery    =~ s|/\*SEARCH_CONDITIONS\*/|$attr->{'searchConditions'}|g || die "Couldn't find required placeholder /*SEARCH_CONDITIONS*/ in sql statement!\n";
  $searchCountQuery    =~ s/^SELECT.+?FROM/SELECT COUNT(*) FROM/              || die "Couldn't insert 'SELECT COUNT(*) FROM' into sql statement!\n";

  ### Get Result Counts

  my $recordCount    = $dbh->selectrow_array($recordCountQuery); # || die $dbh->errstr;
  my $searchCount    = $dbh->selectrow_array($searchCountQuery); # || die $dbh->errstr;
  my $totalPages     = &IT::Common::mathCeiling($searchCount / $attr->{'_perPage'}) || 1;
  my $pageNumber     = &IT::Common::mathMin($attr->{'_pageNum'}, $totalPages);
  my $nextPageNumber = ($pageNumber < $totalPages) ? ($pageNumber + 1) : 1;
  my $prevPageNumber = ($pageNumber > $totalPages || $pageNumber == 1) ? $totalPages : ($pageNumber - 1);

  ### Set Placeholders
  &IT::Template::setGlobalPlaceholder('_recordCount',    $recordCount);
  &IT::Template::setGlobalPlaceholder('_searchCount',    $searchCount);
  &IT::Template::setGlobalPlaceholder('_totalPages',     $totalPages);
  &IT::Template::setGlobalPlaceholder('_pageNum',        $pageNumber);
  &IT::Template::setGlobalPlaceholder('_nextPageNumber', $nextPageNumber);
  &IT::Template::setGlobalPlaceholder('_prevPageNumber', $prevPageNumber);
  &IT::Template::setGlobalPlaceholder('_listRows',       '');

  ### get results sql statement (uses modified pageNumbers)

  my $listResultsQuery = $attr->{'sqlStatement'};
  $listResultsQuery    =~ s|/\*SEARCH_CONDITIONS\*/|$attr->{'searchConditions'}|g || die "Couldn't find required placeholder /*SEARCH_CONDITIONS*/ in sql statement!\n";
  $listResultsQuery    .= " $attr->{'sqlOrderBy'} ";
  my $limitStart       = ($pageNumber - 1) * $attr->{'_perPage'};
  my $limitLength      = $attr->{'_perPage'};
  $listResultsQuery    .= "LIMIT $limitStart, $limitLength ";

  ### Query Database
  my $sth = $dbh->prepare($listResultsQuery);
  $sth->execute() or die $sth->errstr;


  while (my $record = $sth->fetchrow_hashref) {
    $attr->{'cellPlaceholders'} = {};

    if ($attr->{'preRowCode'}) { &{$attr->{'preRowCode'}}($attr, $record); }
    if ($attr->{'rowCode'})    { &{$attr->{'rowCode'}}($attr, $record); }
    else {
      &IT::App::Menu::rotateBgColorClassGlobalPlaceholder();

      my $recordPlaceholders      = &IT::App::getPlaceholdersFromRecordFields($attr->{'tableNameWithPrefix'}, $record);
      $attr->{'cellPlaceholders'} = { %{$attr->{'cellPlaceholders'}}, %$recordPlaceholders }; # recordPlaceholders override others
      my $listRow                 = &IT::Template::templateCell($attr->{'template'}, $attr->{'listRowTemplateCell'}, $attr->{'cellPlaceholders'});

      &IT::Template::appendToGlobalPlaceholder('_listRows', $listRow);
    }
    if ($attr->{'postRowCode'}) { &{$attr->{'postRowCode'}}($attr, $record); }
  }


  ### Generate Output
#  if ($attr->{'output'})    { &{$attr->{'output'}}($attr); }
#  else {

    # Display page
    &IT::App::displayInterface({
      'template'           => $attr->{'template'},
      'templatecell'       => $attr->{'menuTemplateCell'},
      'placeholders'       => $attr->{'placeholders'},
      'selectedMenuOption' => $attr->{'selectedMenuOption'},
    });
#  }
#  if ($attr->{'postOutput'}) { &{$attr->{'postOutput'}}($attr); }

#  if ($attr->{'finish'}) { &{$attr->{'finish'}}($attr); }
}

# ------------------------------------------------------------------------
# Function    : add
# Description : Display add page for a record
# Note        : When adding handlers copy examples in list()
# ------------------------------------------------------------------------

sub add {
  my $menuName = shift || die "no menuName defined!";
  my $errors   = shift;
  my $attr     = &getMenuAttributes($menuName, 'add');

  ### override function (only needed for functions than are called directly by other menu functions: list, add, edit, erase)
  if ($attr->{'function'}) { return &{$attr->{'function'}}($menuName, $errors); }

  # display alerts
  &IT::App::alert($errors);

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}              = $menuName;
  $attr->{'placeholders'}          ||= {};
  if (!$attr->{'menuTemplateCell'}) {
    if (&IT::Template::templateCellExists($attr->{'template'}, 'add')) { $attr->{'menuTemplateCell'} = 'add'; }
    else                                                               { $attr->{'menuTemplateCell'} = 'edit'; }
  }
  &_dieOnInvalidMenuAttributes($menuName, 'add', $attr);

  ### error checking
  if ($attr->{'preErrorChecking'})  { &{$attr->{'preErrorChecking'}}($attr); }
  if ($attr->{'postErrorChecking'}) { &{$attr->{'postErrorChecking'}}($attr); }

  ### Set global placeholders (override these in init if needed)
  if ($attr->{'preSetGlobalPlaceholders'}) { &{$attr->{'preSetGlobalPlaceholders'}}($attr); }
  {
    &IT::Template::setGlobalPlaceholder('_menuName',  $attr->{'menuName'});
    &IT::Template::setGlobalPlaceholder('_tableName', $attr->{'tableNameWithPrefix'});
    &IT::Template::setGlobalPlaceholder('_addOrEdit', 'add');
  }
  if ($attr->{'postSetGlobalPlaceholders'}) { &{$attr->{'postSetGlobalPlaceholders'}}($attr); }

  # Get Form Values (field default values are set below by &_createFieldListHtml)
  $attr->{'placeholders'}       = &_loadDefaultPlaceholderValues($attr->{'tableNameWithPrefix'}, $attr->{'placeholders'});
  my $recordFieldsFromFormInput = &IT::App::getRecordFieldsFromFormInput($attr->{'tableNameWithPrefix'}, $attr->{'recordFields'});
  my $recordFieldPlaceholders   = &IT::App::getPlaceholdersFromRecordFields($attr->{'tableNameWithPrefix'}, $recordFieldsFromFormInput);
  $attr->{'placeholders'}       = { %{$attr->{'placeholders'}}, %$recordFieldPlaceholders }; # override existing placeholder values with new values (if any)

  # set meta field values
  $attr->{'placeholders'}{'createdTime'}          = &IT::Template::createDatePlaceholder( &IT::App::Common::getTimeAdjustedToMatchGMT($^T) );
  $attr->{'placeholders'}{'lastUpdatedTime'}      = &IT::Template::createDatePlaceholder( &IT::App::Common::getTimeAdjustedToMatchGMT($^T) );
  $attr->{'placeholders'}{'createdByUserNum'}     = $CURRENT_USER->{'num'};
  $attr->{'placeholders'}{'lastUpdatedByUserNum'} = $CURRENT_USER->{'num'};


  ### Output

  # generate fields (field default values are assigned here)
  $attr->{'placeholders'}{'_fieldList'} = &_createFieldListHtml($attr->{'tableNameWithPrefix'}, $attr->{'placeholders'});

  # Display page
  &IT::App::displayInterface({
    'template'           => $attr->{'template'},
    'templatecell'       => $attr->{'menuTemplateCell'},
    'placeholders'       => $attr->{'placeholders'},
    'selectedMenuOption' => $attr->{'selectedMenuOption'},
  });

}

# ------------------------------------------------------------------------
# Function    : edit
# Description : Display edit page for a record
# Note        : When adding handlers copy examples in list()
# ------------------------------------------------------------------------

sub edit {
  my $menuName = shift || die "no menuName defined!";
  my $errors   = shift;
  my $attr     = &getMenuAttributes($menuName, 'edit');

  ### override function (only needed for functions than are called directly by other menu functions: list, add, edit, erase)
  if ($attr->{'function'}) { return &{$attr->{'function'}}($menuName, $errors); }

  # display alerts
  &IT::App::alert($errors);

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}              = $menuName;
  $attr->{'placeholders'}          ||= {};
  $attr->{'menuTemplateCell'}      ||= 'edit';
  $attr->{'recordNum'}             ||= $FORM->{'num'};
  &_dieOnInvalidMenuAttributes($menuName, 'edit', $attr);
  ### Note: Don't use reference FORM vars anywhere below here, use $attr instead!

  ### Load Record
  &IT::FileLock::getLock('shared');
  my $records = &IT::DB::select({
    'tableName'  => $attr->{'tableNameWithPrefix'},
    'whereQuery' => "num = '$attr->{'recordNum'}'",
    'limit'      => [0,1],
  });
  $attr->{'recordFields'} = $records->[0];
  &IT::FileLock::releaseLock();

  # error checking
  if (!$attr->{'recordFields'}) {
    my $error = "Couldn't find record #$attr->{'recordNum'}";

    my $listAttr     = &getMenuAttributes($menuName, 'list'); # if list calls edit avoid getting caught in loop
    if (!$listAttr->{'function'}) { &list($menuName, $error); }
    else                          { die $error; }
  }


  ### error checking
  if ($attr->{'preErrorChecking'})  { &{$attr->{'preErrorChecking'}}($attr); }
  if ($attr->{'postErrorChecking'}) { &{$attr->{'postErrorChecking'}}($attr); }

  ### Set global placeholders (override these in init if needed)
  if ($attr->{'preSetGlobalPlaceholders'}) { &{$attr->{'preSetGlobalPlaceholders'}}($attr); }
  {
    &IT::Template::setGlobalPlaceholder('_menuName',  $attr->{'menuName'});
    &IT::Template::setGlobalPlaceholder('_tableName', $attr->{'tableNameWithPrefix'});
    &IT::Template::setGlobalPlaceholder('_addOrEdit', 'edit');
  }
  if ($attr->{'postSetGlobalPlaceholders'})  { &{$attr->{'postSetGlobalPlaceholders'}}($attr); }


  # get placeholders
  my $modifiedRecordFields = &IT::App::overrideRecordFieldsWithFormInput($attr->{'tableNameWithPrefix'}, $attr->{'recordFields'});
  $attr->{'placeholders'}  = &IT::App::getPlaceholdersFromRecordFields($attr->{'tableNameWithPrefix'}, $modifiedRecordFields);

  ### Generate Output

  # generate fields
  $attr->{'placeholders'}{'_fieldList'} = &_createFieldListHtml($attr->{'tableNameWithPrefix'}, $attr->{'placeholders'});

  # Display page
  &IT::App::displayInterface({
    'template'           => $attr->{'template'},
    'templatecell'       => $attr->{'menuTemplateCell'},
    'placeholders'       => $attr->{'placeholders'},
    'selectedMenuOption' => $attr->{'selectedMenuOption'},
  });

}

# ------------------------------------------------------------------------
# Function    : uploadForm
# Description :
# ------------------------------------------------------------------------

sub uploadForm {
  my $errors   = shift;
  my $template = 'admin/popups/uploadForm.html';

  # define placeholders
  my $placeholders = {};
  $placeholders->{'errors'}     = &IT::Common::htmlEncode($errors);
  $placeholders->{'tablename'}  = $FORM->{'tablename'};
  $placeholders->{'fieldname'}  = $FORM->{'fieldname'};
  $placeholders->{'uploadType'} = $FORM->{'uploadType'};
  $placeholders->{'maxUploads'} = &IT::Common::mathMin($FORM->{'maxUploads'}, 10);

  # create upload fields
  $placeholders->{'uploadFields'} = '';
  for my $count (1..$placeholders->{'maxUploads'}) {
    my $rowPlaceholders = {};
    $rowPlaceholders->{'count'} = $count;
    $placeholders->{'uploadFields'} .= &templateCell($template, 'uploadField', $rowPlaceholders);
  }

  # display page
  print &IT::Common::contentType('text/html');
  print &template($template, $placeholders);
  exit;
}


# ------------------------------------------------------------------------
# Function    : submitUploadForm
# Description :
# ------------------------------------------------------------------------

sub submitUploadForm {
  my $tablename = $FORM->{'tablename'} || die "no tablename specified!";
  my $fieldname = $FORM->{'fieldname'} || die "no fieldname specified!";

  # rename accepted uploads and remove rejected ones
  my $uploadsSubmittedCount = 0;
  my $uploadsAcceptedCount = 0;
  my $uploadErrors;
  my %rejectedUserFilenames;

  foreach my $numberedUploadField (keys %$FORM_UPLOADS) {
    my $userFileName = $FORM_UPLOADS->{$numberedUploadField}{'userFileName'} || next;  # skip field if no file uploaded
    my $tempFilePath = $FORM_UPLOADS->{$numberedUploadField}{'tempFilePath'};
    $uploadsSubmittedCount++;

    my $thisUploadErrors = &IT::App::Menu::Save::getUploadValidationErrors($tablename, $fieldname, $tempFilePath, $userFileName);
    $uploadErrors .= $thisUploadErrors;

    if ($thisUploadErrors) {
      $rejectedUserFilenames{$userFileName} = 1;
      unlink $tempFilePath;   # erase rejected uploads
    }
    else {
      $uploadsAcceptedCount++;
      $FORM_UPLOADS->{$numberedUploadField}{'filePath'} = &IT::ReadForm::claimUpload($tempFilePath, $userFileName); # claim upload (renames and moves file from incoming folder)
      &IT::Upload::setExpiryCounter( $FORM_UPLOADS->{$numberedUploadField}{'filePath'} );
    }
  }

  ### resize oversized images (if enabled)
  my $isResizeFeatureAvailable        = &IT::Image::isResizeImageAvailable();
  my($maxImageHeight, $maxImageWidth) = &IT::App::Menu::getMaxImageDimensionsForField($tablename, $fieldname);
  my $isMaxImageDimensions            = ($maxImageHeight > 0 || $maxImageWidth > 0);
  if ($isMaxImageDimensions && $SETTINGS->{'resize_oversize_images'} && $isResizeFeatureAvailable) {

    # check each upload
    foreach my $name (keys %$FORM_UPLOADS) {
      my $filepath = $FORM_UPLOADS->{$name}{'filePath'} || next;  # skip rejected files (they don't have filepath)

      # is image
      unless (&IT::Image::isImage($filepath)) { next; }           # skip non-images

      # is image oversize
      my($height, $width) = &IT::Image::getImageHeightAndWidth($filepath);
      my $isOversized     = ($height > $maxImageHeight) || ($width > $maxImageWidth);
      unless ($isOversized) { next; }                             # skip images that aren't oversized

      # generate target filename
      my $targetFilepath = $filepath;
      $targetFilepath =~ s/\.(\w+)$/__resized__.$1/;
      if (-e $targetFilepath) { unlink $targetFilepath; }

      # create resized image
      &IT::Image::resizeImage($filepath, $targetFilepath, $maxImageHeight, $maxImageWidth);

      # replace original
      unless (-e $targetFilepath) { die "unable to create resized image!"; }
      unlink $filepath;
      rename $targetFilepath, $filepath;
    }
  }

  ### create thumbnails (if enabled)
  my $createThumbnails = $SETTINGS->{'auto_create_thumbnails'} && $isResizeFeatureAvailable;
  if ($createThumbnails) {
    my($maxThumbHeight, $maxThumbWidth) = &_getMaxThumbSize($tablename, $fieldname);

    # create thumbnail
    foreach my $fieldname (keys %$FORM_UPLOADS) {
      my $filepath = $FORM_UPLOADS->{$fieldname}{'filePath'};
      unless (&IT::Image::isImage($filepath)) { next; }

      # generate target filename
      my $targetFilepath = $filepath;
      $targetFilepath =~ s/\.(\w+)$/$THUMBNAIL_PREFIX.$1/;

      # create resized image
      &IT::Image::resizeImage($filepath, $targetFilepath, $maxThumbHeight, $maxThumbWidth);
    }
  }


  # If all uploads not accepted redisplay upload form with errors
  if (!$uploadsAcceptedCount) {
    if ($uploadsSubmittedCount == 0) { $uploadErrors .= "No files were selected to upload!"; }
    &uploadForm($uploadErrors);
    exit;
  }

  # If some or all uploads accepted display modify attributes page with any errors at the top
  if ($uploadsAcceptedCount) {
    &modifyUploadAttributesForm($uploadErrors);
  }

  exit;
}

# ------------------------------------------------------------------------------
# Function    : _getMaxThumbSize
# Description :
# Usage       : my($maxThumbHeight, $maxThumbWidth) = &_getMaxThumbSize($tablename, $fieldname);
# ------------------------------------------------------------------------------

sub _getMaxThumbSize {
  my $tablename = shift || die "no tablename specified!";
  my $fieldname = shift || die "no fieldname specified!";

  my $maxThumbWidth  = $SETTINGS->{'maximum_thumbnail_width'};
  my $maxThumbHeight = $SETTINGS->{'maximum_thumbnail_height'};

  # get custom thumb size restrictions (if defined)
  my $fieldValidationRules = &IT::DB::Schema::getFieldAttributes($tablename, $fieldname, 'appFieldValidationRules');
  if ($fieldValidationRules->{'uploadThumbSizeOverride'}) {
    $maxThumbHeight = $fieldValidationRules->{'uploadMaxThumbHeight'};
    $maxThumbWidth  = $fieldValidationRules->{'uploadMaxThumbWidth'};
  }

  return($maxThumbHeight, $maxThumbWidth);

}

# ------------------------------------------------------------------------
# Function    : modifyUploadAttributesForm
# Description :
# ------------------------------------------------------------------------

sub modifyUploadAttributesForm {
  my $errors    = shift;
  my $template  = 'admin/popups/uploadModifyAttributes.html';
  my $tablename = $FORM->{'tablename'} || die "no tablename specified!";
  my $fieldname = $FORM->{'fieldname'} || die "no fieldname specified!";

  # define placeholders
  my $placeholders = {};
  $placeholders->{'errors'}     = &IT::Common::htmlEncode($errors);
  $placeholders->{'tablename'}  = $FORM->{'tablename'};
  $placeholders->{'fieldname'}  = $FORM->{'fieldname'};
  $placeholders->{'uploadType'} = $FORM->{'uploadType'};
  $placeholders->{'maxUploads'} = &IT::Common::mathMin($FORM->{'maxUploads'}, 10);

  # add "add uploads" javascript (this add the upload to the parent window
  $placeholders->{'addUploadsToParentWindowJavascript'} = '';

  foreach my $fieldname (keys %$FORM_UPLOADS) {

    my $rejectedFile = !$FORM_UPLOADS->{$fieldname}{'filePath'};
    if ($rejectedFile) { next; }

    # get upload info
    my $filepath             = $FORM_UPLOADS->{$fieldname}{'filePath'};
    my $filename             = $filepath;
    $filename                =~ s|^.*[\\/]||;
    my $isImage              = &IT::Image::isImage($filepath);
    my $pathFromUploadDir    = &IT::Upload::getFormattedPathFromUploadDir($filepath);

    $filename =~ m/\.(\w+)$/;
    my $extension            = $1;

    my $thumbnailFilepath          = $filepath;
    $thumbnailFilepath             =~ s/\.(\w+)$/$THUMBNAIL_PREFIX.$1/;
    my $hasThumb                   = int (-f $thumbnailFilepath);
    my $thumbnailFilename          = $thumbnailFilepath;
    $thumbnailFilename             =~ s|^.*[\\/]||;
    my $thumbnailPathFromUploadDir = $hasThumb ? &IT::Upload::getFormattedPathFromUploadDir($thumbnailFilepath) : "";

    my($height, $width, $thumbnailHeight, $thumbnailWidth) = (0,0,0,0);
    if ($isImage && -f $filepath) {
      my($maxThumbHeight, $maxThumbWidth) = &_getMaxThumbSize($placeholders->{'tablename'}, $placeholders->{'fieldname'});
      ($height, $width)                   = &IT::Image::getImageHeightAndWidth($filepath);
      ($thumbnailHeight, $thumbnailWidth) = &IT::Image::calculateThumbnailSize($maxThumbHeight, $maxThumbWidth, $height, $width);
    }

    # define placeholders
    my %cellPlaceholders = %$placeholders;  # copy placeholder values
    $cellPlaceholders{'filename'}                   = $filename;
    $cellPlaceholders{'pathFromUploadDir'}          = $pathFromUploadDir;
    $cellPlaceholders{'title'}                      = $filename;
    $cellPlaceholders{'caption'}                    = '';
    $cellPlaceholders{'align'}                      = $DEFAULT_UPLOAD_ALIGNMENT; # left, center, right
    $cellPlaceholders{'wrapText'}                   = 1;      # 0 or 1 - whether <br clear=all> is inserted after image
    $cellPlaceholders{'linkTo'}                     = '';     # '', image, url
    $cellPlaceholders{'openLinkInNewWindow'}        = 0;      #
    $cellPlaceholders{'linkUrl'}                    = '';     # if linkto is set to url
    $cellPlaceholders{'isImage'}                    = $isImage;
    $cellPlaceholders{'useThumbnail'}               = 0;
    $cellPlaceholders{'hasThumbnail'}               = $hasThumb;
    $cellPlaceholders{'thumbnailFilename'}          = $hasThumb ? $thumbnailFilename          : $filename;
    $cellPlaceholders{'thumbnailPathFromUploadDir'} = $hasThumb ? $thumbnailPathFromUploadDir : $pathFromUploadDir;
    $cellPlaceholders{'height'}                     = $height;
    $cellPlaceholders{'width'}                      = $width;
    $cellPlaceholders{'thumbnailHeight'}            = $thumbnailHeight;
    $cellPlaceholders{'thumbnailWidth'}             = $thumbnailWidth;
    $cellPlaceholders{'extension'}                  = $extension;

    $placeholders->{'addUploadsToParentWindowJavascript'} .= &templateCell($template, 'addUploadToParentWindowJavascript', \%cellPlaceholders);
  }
  #addUploadToParent



  # display page
  print &IT::Common::contentType('text/html');
  print &template($template, $placeholders);
  exit;
}

# ------------------------------------------------------------------------
# Function    : preview (THIS IS A SAMPLE FUNCTION, copy it and call copy directly from if (menuPreview))
# Description : Display preview page with record values.  Note, usually you'd
#               have the preview page in a completely different template and
#               maybe publish it as a temp file and link to that to make all
#               the links work.
# Note        : When adding handlers copy examples in list()
# ------------------------------------------------------------------------

sub preview {
  my $menuName = shift || die "no menuName defined!";
  my $errors   = shift;
  my $attr     = &getMenuAttributes($menuName, 'preview');
  &IT::App::alert($errors);

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}              = $menuName;
  $attr->{'placeholders'}          ||= {};
  $attr->{'menuTemplateCell'}      ||= 'preview';
  &_dieOnInvalidMenuAttributes($menuName, 'preview', $attr);

  ### error checking

  ### Set global placeholders (override these in init if needed)


  ### Generate Output

  # get placeholders
  my $modifiedRecordFields = &IT::App::getRecordFieldsFromFormInput($attr->{'tableNameWithPrefix'});
  $attr->{'placeholders'}  = &IT::App::getPlaceholdersFromRecordFields($attr->{'tableNameWithPrefix'}, $modifiedRecordFields);

  # Display page
  print &IT::Common::contentType('text/html');
  print &templateCell($attr->{'template'},
                      $attr->{'menuTemplateCell'},
                      $attr->{'placeholders'});
  exit;
}


# ------------------------------------------------------------------------
# Function    : save
# Description : Save record after add/edit and optionally display "record saved" page
# Usage       : &IT::Menu::save($menuName);
# Note        : When adding handlers copy examples in list()
# ------------------------------------------------------------------------

sub save {
  my $menuName = shift || die "no menuName defined!";
  my $attr     = &getMenuAttributes($menuName, 'save');

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}              = $menuName;
  $attr->{'placeholders'}          ||= {};
  $attr->{'recordNum'}             ||= $FORM->{'num'};
  $attr->{'isNewRecord'}           = ($attr->{'recordNum'} eq '');
  &_dieOnInvalidMenuAttributes($menuName, 'save', $attr);
  ### Note: Don't use reference FORM vars anywhere below here, use $attr instead!

  ### Load Original Record
  unless ($attr->{'isNewRecord'}) {
    &IT::FileLock::getLock('shared');
    my $records = &IT::DB::select({
      'tableName'  => $attr->{'tableNameWithPrefix'},
      'whereQuery' => "num = '$attr->{'recordNum'}'",
      'limit'      => [0,1],
    });
    $attr->{'originalRecord'} = $records->[0];
    $attr->{'recordFields'}   = $records->[0];
    &IT::FileLock::releaseLock();

    # error checking
    unless ($attr->{'recordFields'}) { die "Couldn't find record #$attr->{'recordNum'}"; }
  }

  ### error checking
  if ($attr->{'preErrorChecking'})  { &{$attr->{'preErrorChecking'}}($attr); }
  {
    if (&IT::DB::Schema::existsField($attr->{'tableNameWithPrefix'}, 'lastUpdatedTime') &&
        !exists $FORM->{'lastUpdatedTime'}) { die "no value specified for 'lastUpdatedTime'!"; }
    if (!$FORM->{'_addOrEdit'})             { die "no _addOrEdit field value specified!"; }
    $attr->{'errors'} .= &IT::App::Menu::Save::getFieldValidationErrors($attr);
    if ($attr->{'errors'} ) {
      if    ($FORM->{'_addOrEdit'} eq 'add')  { &add($menuName, $attr->{'errors'} ); }
      elsif ($FORM->{'_addOrEdit'} eq 'edit') { &edit($menuName, $attr->{'errors'} ); }
      else                                    { die "hidden field _addOrEdit not set to either 'add' or 'edit'!"; }
    }
  }
  if ($attr->{'postErrorChecking'})       { &{$attr->{'postErrorChecking'}}($attr); }


  ### Set global placeholders (override these in init if needed)
  if ($attr->{'preSetGlobalPlaceholders'}) { &{$attr->{'preSetGlobalPlaceholders'}}($attr); }
  {
    &IT::Template::setGlobalPlaceholder('_menuName', $attr->{'menuName'});
  }
  if ($attr->{'postSetGlobalPlaceholders'}) { &{$attr->{'postSetGlobalPlaceholders'}}($attr); }

  ### Check if record has been updated
  unless ($attr->{'isNewRecord'}) {
    my $recordUpdatedDuringEditing = &IT::App::Menu::Save::hasRecordBeenUpdatedDuringEditing($FORM->{'lastUpdatedTime'}, $attr->{'recordFields'});
    if ($recordUpdatedDuringEditing) {
      &IT::App::Menu::Save::revisionConflictWarning($menuName, $attr);
    }
  }

  ### Get Data

  # get form input
  $attr->{'newOrUpdatedFields'} = &IT::App::getRecordFieldsFromFormInput($attr->{'tableNameWithPrefix'});

  # manually limit fields that can be updated (if specified)
  if ($attr->{'limitUpdatableFields'}) {
    my %updatableFields = map { $_ => 1 } @{$attr->{'limitUpdatableFields'}};
    foreach my $name (keys %{$attr->{'newOrUpdatedFields'}}) {
      unless ($updatableFields{$name}) { delete $attr->{'newOrUpdatedFields'}{$name}; }
    }
  }

  # set meta fields
  $attr->{'isNewRecord'} = !$attr->{'recordNum'};
  {
    my $time = time();
    if ($attr->{'isNewRecord'}) {
      if (&IT::DB::Schema::existsField($attr->{'tableNameWithPrefix'}, 'createdTime')) {
        $attr->{'newOrUpdatedFields'}{'createdTime'}        = &IT::App::Common::getTimeAdjustedToMatchGMT($time);
      }
      if (&IT::DB::Schema::existsField($attr->{'tableNameWithPrefix'}, 'createdByUserNum')) {
        $attr->{'newOrUpdatedFields'}{'createdByUserNum'}   = $CURRENT_USER->{'num'};
      }
    }

    if (&IT::DB::Schema::existsField($attr->{'tableNameWithPrefix'}, 'lastUpdatedTime')) {
      $attr->{'newOrUpdatedFields'}{'lastUpdatedTime'}      = &IT::App::Common::getTimeAdjustedToMatchGMT($time);
    }
    if (&IT::DB::Schema::existsField($attr->{'tableNameWithPrefix'}, 'lastUpdatedByUserNum')) {
      $attr->{'newOrUpdatedFields'}{'lastUpdatedByUserNum'} = $CURRENT_USER->{'num'};
    }
  }

  # Uploads: adopt new upload, and remove uploads marked as deleted
  $attr->{'newOrUpdatedFields'} = &IT::App::Menu::Save::adoptNewUploadsAndEraseRemovedUploads($attr->{'tableNameWithPrefix'}, $attr->{'newOrUpdatedFields'});

  ### Save Record

  if ($attr->{'preSave'}) { &{$attr->{'preSave'}}($attr); }

  # create new record if no record num
  &IT::FileLock::getLock('exclusive');
  if ($attr->{'isNewRecord'}) {   # NOTE: record number is added to newOrUpdatedFields hash by insert
    &IT::DB::insert($attr->{'tableNameWithPrefix'}, [ $attr->{'newOrUpdatedFields'} ]) || die "Error inserting new record! $!";
    $attr->{'newOrUpdatedFields'}{'num'} = &IT::DB::getSequence($attr->{'tableNameWithPrefix'});
    $attr->{'recordNum'}                 = $attr->{'newOrUpdatedFields'}{'num'};
  }

  # else update existing record
  else {
    $attr->{'newOrUpdatedFields'}{'num'}                  = $attr->{'recordNum'};
    my $recordNum  = int($attr->{'newOrUpdatedFields'}{'num'}) || die "no record number!";
    my $whereQuery = "num = '$recordNum'";

    &IT::DB::update($attr->{'tableNameWithPrefix'}, $whereQuery, $attr->{'newOrUpdatedFields'}) || die "Couldn't find record #$recordNum";
  }

  # call postSaveOperations for custom fields
  my @allFieldNames = &IT::DB::Schema::getFieldNames($attr->{'tableNameWithPrefix'});
  foreach my $fieldname (@allFieldNames) {
    my($appFieldType, $appCustomFieldType) = &IT::DB::Schema::getFieldAttributes($attr->{'tableNameWithPrefix'}, $fieldname, 'appFieldType', 'appCustomFieldType');
    unless ($appFieldType eq 'custom') { next; }

    my $customFieldAttributes = &IT::App::Menu::getCustomFieldType($appCustomFieldType);
    unless ($customFieldAttributes->{'postSaveOperations'}) { next; }

    &{$customFieldAttributes->{'postSaveOperations'}}($attr->{'tableNameWithPrefix'}, $fieldname, $attr->{'newOrUpdatedFields'});
  }
  &IT::FileLock::releaseLock();

  if ($attr->{'postSave'}) { &{$attr->{'postSave'}}($attr); }
#  &IT::App::alert("Record has been saved."); # this can be added in postSave handler if desired

  ### Call publish routine
  if ($attr->{'publish'})     { &{$attr->{'publish'}}($attr); }

  ### Generate Output
  &list($menuName);
  exit;

}

# ------------------------------------------------------------------------
# Function    : confirmErase
# Description : Display confirm erase page for a record
# Note        : When adding handlers copy examples in list()
# ------------------------------------------------------------------------

sub confirmErase {
  my $menuName = shift || die "no menuName defined!";
  my $attr     = &getMenuAttributes($menuName, 'confirmErase');

  # skip confirm erase page based on settings
  unless ($attr->{'alwaysRequireEraseConfirm'} || $SETTINGS->{'confirm_erase_requests'}) {
    return &erase($menuName);
  }

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}              = $menuName;
  $attr->{'placeholders'}          ||= {};
  $attr->{'sortBy'}                ||= &IT::DB::Schema::getTableAttributes($attr->{'tableNameWithPrefix'}, 'defaultSort') || ['num'];
  $attr->{'menuTemplateCell'}      ||= 'confirmErase';
  $attr->{'listRowTemplateCell'}   ||= 'confirmEraseRow';
  &_dieOnInvalidMenuAttributes($menuName, 'confirmErase', $attr);
  ### Note: Don't use reference FORM vars anywhere below here, use $attr instead!

  ### init
  if ($attr->{'init'}) { &{$attr->{'init'}}($attr); }

  ### Get Record Numbers
  if ($attr->{'getSelectedRecords'}) { &{$attr->{'getSelectedRecords'}}($attr); }
  else {
    $attr->{'selectedRecordNumbers'} = &getSelectedRecordNumbersAsHash();
  }
  $attr->{'selectedRecordsAsCSV'} = join ",", keys %{$attr->{'selectedRecordNumbers'}};  # CSV = Comma seperated values
  unless (keys %{$attr->{'selectedRecordNumbers'}}) { &list($menuName, "No records selected to erase!"); }

  ### Load records
  &IT::FileLock::getLock('shared');
  $attr->{'selectedRecords'} = &IT::DB::select({
    'tableName'  => $attr->{'tableNameWithPrefix'},
    'whereQuery' => "num IN ($attr->{'selectedRecordsAsCSV'})",
  });
  &IT::FileLock::releaseLock();

  ### error checking
  if ($attr->{'preErrorChecking'})  { &{$attr->{'preErrorChecking'}}($attr); }
  if ($attr->{'postErrorChecking'}) { &{$attr->{'postErrorChecking'}}($attr); }

  ### Set global placeholders (override these in init if needed)
  if ($attr->{'preSetGlobalPlaceholders'}) { &{$attr->{'preSetGlobalPlaceholders'}}($attr); }
  {
    &IT::Template::setGlobalPlaceholder('_menuName',  $attr->{'menuName'});
  }
  if ($attr->{'postSetGlobalPlaceholders'}) { &{$attr->{'postSetGlobalPlaceholders'}}($attr); }

  ### Rowcode - this block is executed below by listpage for each record on list page
  my $rowCode = sub {
    my $record = shift; # get record
    if ($attr->{'preRowCode'}) { &{$attr->{'preRowCode'}}($attr, $record); }

    &rotateBgColorClassGlobalPlaceholder();

    my $placeholders = &IT::App::getPlaceholdersFromRecordFields($attr->{'tableNameWithPrefix'}, $record);
    my $listRow      = &templateCell($attr->{'template'}, $attr->{'listRowTemplateCell'}, $placeholders);
    $attr->{'placeholders'}{'_confirmEraseRows'} .= $listRow;
  };


  ### Load Records
  if ($attr->{'preLoadRecords'}) { &{$attr->{'preLoadRecords'}}($attr); }
  &IT::FileLock::getLock('shared');
  $attr->{'pageInfo'} = &IT::DB::list({   # execute rowcode for matching records
    'tableName'       => $attr->{'tableNameWithPrefix'},
    'whereQuery'      => "num IN ($attr->{'selectedRecordsAsCSV'})",
    'sortBy'          => $attr->{'sortBy'},
    'rowcode'         => $rowCode,
  });
  &IT::FileLock::releaseLock();




  ### Generate Output
  $attr->{'placeholders'}{'_confirmEraseRows'} ||= '';

  # Display page
  &IT::App::displayInterface({
    'template'           => $attr->{'template'},
    'templatecell'       => $attr->{'menuTemplateCell'},
    'placeholders'       => $attr->{'placeholders'},
    'selectedMenuOption' => $attr->{'selectedMenuOption'},
  });
}

# ------------------------------------------------------------------------
# Function    : erase
# Description : Erase one or more records
# Note        : When adding handlers copy examples in list()
# ------------------------------------------------------------------------

sub erase {
  my $menuName = shift || die "no menuName defined!";
  my $attr     = &getMenuAttributes($menuName, 'erase');

  ### override function (only needed for functions than are called directly by other menu functions: list, add, edit, erase)
  if ($attr->{'function'}) { return &{$attr->{'function'}}($menuName); }

  ### set defaults (override these in init if needed)
  $attr->{'menuName'}              = $menuName;
  $attr->{'placeholders'}          ||= {};
  $attr->{'sortBy'}                ||= &IT::DB::Schema::getTableAttributes($attr->{'tableNameWithPrefix'}, 'defaultSort') || ['num'];
  &_dieOnInvalidMenuAttributes($menuName, 'erase', $attr);
  ### Note: Don't use reference FORM vars anywhere below here, use $attr instead!

  ### init
  if ($attr->{'init'}) { &{$attr->{'init'}}($attr); }

  ### Get record numbers
  if ($attr->{'getSelectedRecords'}) { &{$attr->{'getSelectedRecords'}}($attr); }
  else {
    $attr->{'selectedRecordNumbers'} = &getSelectedRecordNumbersAsHash();
  }
  $attr->{'selectedRecordsAsCSV'} = join ",", keys %{$attr->{'selectedRecordNumbers'}};  # CSV = Comma seperated values
  unless (keys %{$attr->{'selectedRecordNumbers'}}) { &list($menuName, "No records selected to erase!"); }

  ### Load records
  &IT::FileLock::getLock('shared');
  $attr->{'selectedRecords'} = &IT::DB::select({
    'tableName'  => $attr->{'tableNameWithPrefix'},
    'whereQuery' => "num IN ($attr->{'selectedRecordsAsCSV'})",
  });
  &IT::FileLock::releaseLock();

  ### error checking
  if ($attr->{'preErrorChecking'})  { &{$attr->{'preErrorChecking'}}($attr); }
  if ($attr->{'postErrorChecking'}) { &{$attr->{'postErrorChecking'}}($attr); }

  ### Set global placeholders (override these in init if needed)
  if ($attr->{'preSetGlobalPlaceholders'}) { &{$attr->{'preSetGlobalPlaceholders'}}($attr); }
  {
    &IT::Template::setGlobalPlaceholder('_menuName',  $attr->{'menuName'});
  }
  if ($attr->{'postSetGlobalPlaceholders'}) { &{$attr->{'postSetGlobalPlaceholders'}}($attr); }


  ### Erase records
  if ($attr->{'preErase'}) { &{$attr->{'preErase'}}($attr); }
  if ($attr->{'erase'}) { &{$attr->{'erase'}}($attr); }
  else {
    foreach my $recordNum (keys %{$attr->{'selectedRecordNumbers'}}) {
      my $recordsErased = &IT::App::Menu::eraseRecordAndUploads($attr->{'tableNameWithPrefix'}, $recordNum);
      $attr->{'placeholders'}{'_erasedRecordCount'} += $recordsErased;
    }
  }
  if ($attr->{'postErase'}) { &{$attr->{'postErase'}}($attr); }


  ### Call publish routine
  if ($attr->{'publish'})     { &{$attr->{'publish'}}($attr); }

  ### Generate Output
#  &IT::App::alert("$attr->{'placeholders'}{'_erasedRecordCount'} record(s) have been erased.");
  &list($menuName);
  exit;
}

# ------------------------------------------------------------------------
# Function    : eraseRecordAndUploads
# Description : Erase one or more records
# Usage       : my $recordsErased = &IT::App::Menu::eraseRecordAndUploads($tableName, $recordNum);
# ------------------------------------------------------------------------

sub eraseRecordAndUploads {
  my $tableName = shift || die "no tablename specified!";
  my $recordNum = shift || die "no recordnum specified!";

  ### Load Record
  &IT::FileLock::getLock('shared');
  my $records = &IT::DB::select({
    'tableName'  => $tableName,
    'whereQuery' => "num = '$recordNum'",
    'limit'      => [0,1],
  });
  my $record = $records->[0];
  &IT::FileLock::releaseLock();

  ### erase uploads
  &IT::App::Menu::eraseRecordsUploads($tableName, $record);

  ### erase record
  &IT::FileLock::getLock('exclusive');
  my $erasedRecordCount = &IT::DB::delete($tableName, "num = '$recordNum'");
  &IT::FileLock::releaseLock();

  ### return
  return $erasedRecordCount;
}

# ------------------------------------------------------------------------------
# Function    : eraseRecordsUploads
# Description :
# Usage       : &IT::App::Menu::eraseRecordsUploads($tableName, $record)
# ------------------------------------------------------------------------------

sub eraseRecordsUploads {
  my $tableName = shift || die "no tablename specified!";
  my $record    = shift || die "no recordnum specified!";

  ### Get upload fields
  my @allFieldNames = &IT::DB::Schema::getFieldNames($tableName);
  my @uploadFields;
  foreach my $fieldName (@allFieldNames) {
    my($appFieldType) = &IT::DB::Schema::getFieldAttributes($tableName, $fieldName, 'appFieldType');
    if ($appFieldType eq 'upload') {
      push @uploadFields, $fieldName;
    }
  }

  ### erase uploads
  foreach my $uploadFieldName (@uploadFields) {
    my $listString = $record->{$uploadFieldName} || next;
    my $listStruct   = &IT::DhtmlFieldLists::convertListStringToStruct($listString);
    foreach my $uploadNum (keys %{$listStruct->{'rows'}}) {
      my $thisUpload     = $listStruct->{'rows'}{$uploadNum};
      my $uploadFilepath = "$SETTINGS->{'upload_dir'}/$thisUpload->{'pathFromUploadDir'}";
      my $thumbnailPath  = $uploadFilepath;
      $thumbnailPath     =~ s/\.(\w+)$/$THUMBNAIL_PREFIX.$1/;

      if (-f $uploadFilepath) { unlink($uploadFilepath) || die "unable to erase upload '$uploadFilepath'! $!"; }
      if (-f $thumbnailPath)  { unlink($thumbnailPath)  || die "unable to erase upload '$thumbnailPath'! $!"; }
    }
  }

}

# ------------------------------------------------------------------------------
# Function    : getSelectedRecordNumbersAsHash
# Description : Return an array of record numbers that were selected by being
#               checked or specified in the querystring with num=NNN
# Usage       : my $selectedRecordNumbers = &IT::App::Menu::getSelectedRecordNumbersAsHash();
# ------------------------------------------------------------------------------

sub getSelectedRecordNumbersAsHash {

  # get a list of selected records
  my @selectedRecords;
  if (exists $FORM_ARRAYS->{'selectedRecordNum'}) { push @selectedRecords, @{$FORM_ARRAYS->{'selectedRecordNum'}}; }
  if (exists $FORM->{'num'})                      { push @selectedRecords, $FORM->{'num'}; }
  my %selectedRecords = map { int($_) => 1 } @selectedRecords;

  return \%selectedRecords;

}

# ------------------------------------------------------------------------------
# Function    : getMaxImageDimensionsForField
# Description : return undef if no size limits are specified
# Usage       : my($maxHeight, $maxWidth) = &IT::App::Menu::getMaxImageDimensionsForField($tablename, $fieldname);
# ------------------------------------------------------------------------------

sub getMaxImageDimensionsForField {
  my $tablename = shift || die "no tablename specified!";
  my $fieldname = shift || die "no fieldname specified!";

  ### get max height and width
  my($maxHeight, $maxWidth);

  # use field settings first (if defined)
  my $fieldValidationRules = &IT::DB::Schema::getFieldAttributes($tablename, $fieldname, 'appFieldValidationRules');
  if ($fieldValidationRules->{'uploadImageSizeOverride'}) {
    ($maxHeight, $maxWidth) = ($fieldValidationRules->{'uploadMaxImageHeight'}, $fieldValidationRules->{'uploadMaxImageWidth'});
  }

  # then use global settings (if defined)
  elsif ($SETTINGS->{'check_upload_image_dimensions'}) {
    ($maxHeight, $maxWidth) = ($SETTINGS->{'image_upload_max_height'}, $SETTINGS->{'image_upload_max_width'});
  }

  return ($maxHeight, $maxWidth);
}

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

