var MIN_POLL_INTERVAL = 60000;                  // one minute
var POLL_SCALE = 8;                             // 1 to prevent auto-growth of poll interval
var MAX_POLL_INTERVAL = 4 * 60 * 60000;         // four hours
var POST_UPDATE_INTERVAL = 5000;

var IMAGE_JQT_STARTUP = 'section-images/badges-startup.jpg';
var IMAGE_JQT_ICON = 'section-images/badges-icon-57.png';

//var DATE_FORMAT = "mmm d, 'YY";
var DATE_FORMAT = "YYYY-mm-dd";         // must check if this is supported by jdPicker

var BIMG = ".";
var YES = "<img y src=\"yes.gif\"/>";
//var YES = "&#10003;";
var DONTCARE = "&ndash;"

var WIDE = 600;
var START_OF_WEEK = 0;

var STATE_CLEAR = 0;
var STATE_SET = 1;
var FLAG_BOOKMARK = 0;      // do not EVER change!
var FLAG_COMPLETION = 1;    // do not EVER change!
var FLAG_READY = 2;         // do not EVER change!
var FLAG_AWARDED = 3;       // do not EVER change!
var ROLLUP_NONE = 0;        // do not EVER change!
var ROLLUP_HOURS = 1;       // do not EVER change!
var ROLLUP_NIGHTS = 2;      // do not EVER change!
var ROLLUP_DAYS = 3;        // do not EVER change!

var g_nextAlphabetic;
var g_prevAlphabetic;
var g_currentBadgeID = null;
var g_listInventory;
var g_iInventoryBadge = 0;
var g_listEvents = new Array();
var g_iEvent = 0;

var g_versionRemote_Schema = -1;
var g_sectionID = null;
var g_sectionArea = -1;
var g_sectionGroup = null;
var g_sectionSubGroup = null;
var g_sectionExpiry = 0;
var g_sectionTrial = true;
var g_loginID = null;
var g_loginEmail = null;
var g_lastYouth = null;
var g_lastSyncTimestamp = 0;
var g_lastTallySyncTimestamp = 0;
var g_lastOutingSyncTimestamp = 0;
var g_currentReq = null;
var g_restoreReq = null;
var g_allowSwipe = false;
var g_filter = null;
var g_outingLabelID = -1;
var g_outingYouthID = -1;
var g_report = null;
var g_reportGrouping = 'youth';
var g_tableReportGrouping = true;
var g_eventReportGrouping = false;
var g_shoppingListGrouping = true;
var g_currentUser = -1;
var g_abbreviations = new Array();
var g_nonce = null;
var g_deltaTimestamp = 0;
var g_mode = "handbook";
var g_workerSync = null;
var g_fnWorkerCallback = null;
var g_nMaxYouth = 45;
var g_isInitialized = false;
var g_floaty = null;
var g_jdPicker = null;
var g_jdPickers = {};
var g_timerNextSync = null;
var g_isExpired = false;
var g_role = null;
var g_meetingID = -1;
var g_reminderID = -1;
var g_hasAttributes = true;
var g_hasUnsavedEventEdits = false;
var g_navigationBlocker = false;
var g_allSubreqd = false;
var g_eulaAcceptTimestamp = 0;
var g_eulaUpdateTimestamp = 0;
var g_postEulaPage = 0;
var g_mapUpcomingReqs = {};
var g_nExpiryChecks = 5;        // check for one minute 10 + 5*10
var g_calendarOuting = -1;
var g_calendarOutingIDs;
var g_mapOutingScrollPositions = {};
var g_nSeasonMonth = 8;
var g_nSeasonDay = 15;
var g_dateLastAward = -1;
var g_allowHandbookEdits = false;
var g_duplicationMode = 1;

var g_setUpdateableCategories = {};
var g_mapGlyphCategories = {};
g_mapGlyphCategories['check.gif'] = "awarded";
g_mapGlyphCategories['unawarded.gif'] = "complete";
g_mapGlyphCategories['readybadge.gif'] = "ready";
g_mapGlyphCategories['star.gif'] = "favourite";
g_mapGlyphCategories['blank.gif'] = "incomplete";
     
var g_mapDemand = {};
var g_timeoutPresence = null;
var g_listCatalogIDs = new Array();

var g_creole = null;

// Cached per-youth database values
var g_mapCategoryCounts = {};
var g_mapTypeCounts = {};
var g_mapCategoryIDs = {};
var g_setDirtyBadges = {};
var g_setDirtyRequirements = {};
var g_mapRequirementCompletion = {};

var g_mapBadgeCompletion = {};

var g_setMarkedRequirements = {};
var g_setAwardedBadges = {};
var g_setBookmarkedRequirements = {};
var g_setTallies = {};
var g_setReadyRequirements = {};

var g_setYouthScorecarded = {};
var g_scorecardingYouth = null;
var g_mapOutings = {};
var g_eventUnderEdit = null;
var g_allowDefaulting = true;

var g_mapDerivedBadges = {};
var g_mapDerivedParents = {};
var g_mapBadgeRequirementWeights = {};
var g_mapBadgeRequirementIDs = {};
var g_mapBadgeListeners = {};
var g_mapRequirementListeners = {};
var g_setCategoryDependencies = {};
var g_mapAutocompletion = {};
var g_mapLoginEmails = {};
var g_mapLoginNames = {};
var g_setBadgemasters = {};
var g_mapAccessByLogin = {};
var g_mapAccessByYouth = {};
var g_mapLoginLeaders = {};
var g_mapYouthNames = {}
var g_mapLeaderNames = {}
var g_setActiveYouth = {}
var g_setActiveLeaders = {}
var g_cachedData = null;
var g_reSearchHighliter = null;
var g_reFilterHighliter = null;
var g_dbTables = null;
var g_listUpPoints = new Array();
var g_mapScrollPoints = {};
var g_listSortedLabelIDs = new Array();
var g_listSortedAttributeIDs = new Array();
var g_setLinkedReqs = {};
var g_mapLinksToReqs = {};
var g_lastLoginTimestamp = -1;
var g_forceLoginTimestamp = 0;
var g_isNoob = true;
var g_nAttendanceYear = 2011;
var g_iAttendanceLabel = -1;
var g_listExtraInventory = new Array();
var g_nTaxationYear = -1;

var g_categories = {};
var g_isServerReachable = navigator.onLine;
var g_isAttendanceContextual = false;
var g_warnNoParticipants = true;
// var g_isServerReachable = false;    // KLUDGE to fake out network problems

var g_listUniformIDs = UNIFORMS;
var g_uniformID = g_listUniformIDs[0];

var PAYPAL_SANDBOX = false;

var STRING_LOOKUPS = {};

function checkVersion_IE()
{
    var re  = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
    if ( re.exec(navigator.userAgent) != null )
    {
        var ver = parseFloat( RegExp.$1 );
        if ( ver < 8.0 )
            location.href = "./browser.html";
    }
}

function isAppleMobileDevice()
{
    return ( g_isiPhone || g_isiPod || g_isiPad );
};

function isMobileDevice()
{
    return isAppleMobileDevice() || navigator.userAgent.indexOf( "Android" ) != -1;
};

//console_log( "location.search = '" + location.search + "'" );
// this interferes with embed.html... would need to accommodate that into the test, but I actually don't think it's necessary
//if ( ! location.href.match( /mobile/ ) && ! location.href.match( "index.html" ) )
    //location.href = "index.html";   // redirect from scout-badges.com/scouts to scout-badges.com/scouts/index.html

var g_isLoginLinked = location.search.substring( 0, 6) == "?login";
var g_freshPurchase = location.search.substring( 0, 15) == "?paypal=success";
var g_isEmbedded = false;
var g_isEmbeddedSection = -1;
var g_isEmbeddedColor = "transparent";
if ( location.href.match( /-schedule-(\d+)/ ) )
{
    g_isEmbedded = true;
    g_isEmbeddedSection = RegExp.$1;
}
if ( ! g_isEmbedded )
{
    var strEmbed = location.search.substring( 1 );
    if ( strEmbed.match( /schedule=(\d+)/ ) )
    {
        g_isEmbedded = true;
        g_isEmbeddedSection = RegExp.$1;
        if ( strEmbed.match( /bg=([0-9a-fA-F]{6})/ ) )
            g_isEmbeddedColor = "#" + RegExp.$1;
    }
}
var g_isFirefox = navigator.userAgent.indexOf( "Firefox" ) != -1;
var g_isiPad = navigator.userAgent.match( /iPad/i ) != null
var g_isiPod = navigator.userAgent.match( /iPod/i ) != null
var g_isiPhone = navigator.userAgent.match( /iPhone/i ) != null
var g_isiPhone5 = g_isiPhone;
var g_isIE = /MSIE (\d+\.\d+);/.test(navigator.userAgent);  //test for IE

//console_log( "browser = '" + navigator.userAgent + "' " );
//
// If the screen orientation is defined we are in a modern mobile OS
var g_isMobileOS = typeof orientation != 'undefined' ? true : false;
// If touch events are defined we are in a modern touch screen OS
var g_isTouchOS = ('ontouchstart' in document.documentElement) ? true : false;

$.ajaxSetup( {
    type: "POST",
    cached: false,
    dataType: "json",
    async: true
} );
var g_noJQT = g_isEmbedded;

if ( isAppleMobileDevice() )
{
    var re  = new RegExp("Version/(\\d)");
    if ( re.exec(navigator.userAgent) != null )
    {
        if ( RegExp.$1 < 4 )
        {
            if ( false )
                alert( "Sorry, this app will not run on this generation of " + ( g_isiPod ? "iPod" : "iPhone" ) + "." );
            g_noJQT = true;
        }
    }
}

if ( g_isIE ) 
{
    g_noJQT = true;
    checkVersion_IE();
}

var g_useFloaty = ! g_noJQT // can't use floaty if JQT isn't supported

// ipod ver 2 does not support local storage
var g_hasLocalStorage = false;

try {
    g_hasLocalStorage = 'localStorage' in window && window['localStorage'] !== null;
} catch(e) {
    ; // nop
}

if ( ! g_hasLocalStorage )
{
    console_warn( "No local storage ('" + navigator.userAgent + ")" );
    alert( "This browser does not appear to have sufficient capabilities to run this app." );
}
else
{
    try {
        localStorage.setItem( STORAGE_PREFIX + "private", false );
    } catch( e ) {
        alert( "This browser cannot store information.  If private browsing is on, please turn it off." );
    }
}

function isBadgeOfInterest( badgeID )
{
    //return badgeID == "chiefscoutsaward" || badgeID == "placeholder-wmos";
    //return badgeID == "explorerwateractivities";
    return false;
};

function isOnline()
{
    return g_isServerReachable;
};

function setOnline( isServerReachable )
{
    var oldServerReachable = g_isServerReachable;
    g_isServerReachable = isServerReachable;

    if ( isServerReachable && ! oldServerReachable )
    {
        g_warned = false;
        console_log( "restarting sync" );
        scheduleSync( "on-line mode sync", 1000 );
    }
    else if ( ! isServerReachable && oldServerReachable )
    {
        console_log( "stopping sync" );
        workerPause();
    }

    if ( ! isServerReachable )
    {
        $('img.offline-icon').css( "display", "inline" );
        addOfflineHeaderIcons();
    }
    else
    {
        $('img.offline-icon').hide();
        removeOfflineHeaderIcons();
    }

    toggleSwitch( "#offline-mode", ! g_isServerReachable );
};

function getLocalStorage( strName )
{
    if ( g_hasLocalStorage )
        return localStorage.getItem( STORAGE_PREFIX + strName );

    return null;
};

function setLocalStorage( strName, strValue )
{
    if ( g_hasLocalStorage )
    {
        if ( strValue == null )
        {
            try {
                localStorage.removeItem( STORAGE_PREFIX + strName );
            } catch ( err ) {
                console_warn( "error removing '" + strName + "'" );
            }
        }

        else
        {
            try {
                localStorage.setItem( STORAGE_PREFIX + strName, strValue );
            } catch ( err ) {
                console_error( "error setting '" + strName + "': " + err );
            }
        }
    }
};

function persistProperty( strName, strValue )
{
    setLocalStorage( strName, strValue );
};

function getSection()
{
    return g_sectionID;
};

/* return a URL-safe section... if mode is 'handbook', or info is incomplete, return 0 */
function getURLSection()
{
    var sectionID = getSection();
    if ( g_mode != "handbook" && sectionID && getLoginID() )
        return sectionID;

    return 0;
};

function setSection( sectionID )
{
    g_sectionID = sectionID;

    // now we want to persist this value for future uses of the application
    persistProperty( 'sectionid', sectionID );
};

function getSectionExpiry()
{
    return g_sectionExpiry;
};

function setSectionExpiry( expiry )
{
    g_sectionExpiry = expiry;

    // now we want to persist this value for future uses of the application
    persistProperty( 'sectionexpiry', expiry );
};

function getSectionTrial()
{
    return g_sectionTrial;
};

function setSectionTrial( isTrial )
{
    g_sectionTrial = isTrial;

    // now we want to persist this value for future uses of the application
    persistProperty( 'sectiontrial', isTrial );
};

function getSectionArea()
{
    return ~~g_sectionArea;
};

function setSectionArea( sectionArea )
{
    g_sectionArea = ~~sectionArea;

    // now we want to persist this value for future uses of the application
    persistProperty( 'sectionarea', sectionArea );
};

function getSectionGroup()
{
    return g_sectionGroup;
};

function setSectionGroup( sectionGroup )
{
    g_sectionGroup = sectionGroup;

    // now we want to persist this value for future uses of the application
    persistProperty( 'sectiongroup', sectionGroup );
};

function getSectionSubGroup()
{
    return g_sectionSubGroup;
};

function setSectionSubGroup( sectionSubGroup )
{
    g_sectionSubGroup = sectionSubGroup;

    // now we want to persist this value for future uses of the application
    persistProperty( 'sectionsubgroup', sectionSubGroup );
};

function getLoginEmail()
{
    var strEmail = g_mapLoginEmails[ getLoginID() ];

    if ( strEmail === undefined || strEmail == null )
        strEmail = g_loginEmail;
    else
        persistProperty( "loginemail", strEmail );

    if ( strEmail == null )
        strEmail = "unknown";

    return strEmail;
};

function getLoginID()
{
    return g_loginID;
};

function getURLLoginID()
{
    var loginID = getLoginID();

    if ( loginID == null ) 
        loginID = 0;

    return loginID;
};

function setLoginID( loginID )
{
    g_loginID = loginID;

    if ( g_loginID == null )
        persistProperty( 'loginemail', null );

    // now we want to persist this value for future uses of the application
    persistProperty( 'loginid', loginID );
};

function setLastLoginTimestamp( timestamp )
{
    g_lastLoginTimestamp = timestamp;

    persistProperty( 'lastlogintimestamp', timestamp );
};

function getRole()
{
    return g_role;
};

/**
 * returns true if a resetDatabase is required
 */
function setRole( roleID )
{
    console_log( "setting role to '" + roleID + "' (old role=" + g_role + ", multiyouth=" + getLocalStorage( "multiyouth" ) + ")" );

    var oldRoleID = g_role;

    g_role = roleID;
    persistProperty( 'role', roleID );

    // is this the first time we've logged in as a leader?
    if ( oldRoleID != null && roleID != "p" && getLocalStorage( "multiyouth" ) == null )
    {
        setLocalStorage( "multiyouth", true );
        return true;
    }

    return false;
};

function setLastSyncTimestamp( timestamp )
{
    if ( isNaN( timestamp ) )
        g_lastSyncTimestamp = parseInt( timestamp );
    else
        g_lastSyncTimestamp = timestamp;

    // now we want to persist this value for future uses of the application
    persistProperty( 'lastsync', timestamp );

    updateSyncDisplay();
};

function setLastTallySyncTimestamp( timestamp )
{
    if ( isNaN( timestamp ) )
        g_lastTallySyncTimestamp = parseInt( timestamp );
    else
        g_lastTallySyncTimestamp = timestamp;

    // now we want to persist this value for future uses of the application
    persistProperty( 'lasttallysync', timestamp );

    updateSyncDisplay();
};

function updateSyncDisplay()
{
    var timestamp = getLastSyncTimestamp();
    var timestamp2 = getLastOutingSyncTimestamp();
    if ( timestamp2 > timestamp ) 
        timestamp = timestamp2;
    timestamp2 = getLastTallySyncTimestamp();
    if ( timestamp2 > timestamp ) 
        timestamp = timestamp2;

    var strDate = timestamp <= 0 ? "never" : formatTimestampRelative( timestamp );
    $('#last-sync small').html( strDate );
};

function updateSyncCheckDisplay()
{
    var timestamp = getServerTimestamp();
    var strDate = timestamp <= 0 ? "checking..." : formatTimestampRelative( timestamp );
    $('#last-sync-check small').html( strDate );
};

function setLastOutingSyncTimestamp( timestamp )
{
    if ( isNaN( timestamp ) )
        g_lastOutingSyncTimestamp = parseInt( timestamp );
    else
        g_lastOutingSyncTimestamp = timestamp;

    updateSyncDisplay();
};

function getLastSyncTimestamp()
{
    return g_lastSyncTimestamp;
};

function getLastTallySyncTimestamp()
{
    return g_lastTallySyncTimestamp;
};

function getLastOutingSyncTimestamp()
{
    return g_lastOutingSyncTimestamp;
};

function persistCurrentYouth( youthID )
{
    g_lastYouth = youthID;
    persistProperty( 'lastyouth', youthID );
};

function ncrypt( pw, nonce )
{
    var mask = nonce == null ? "" : (nonce+nonce+nonce);
    var src = hex_md5( pw );
    var dst = "";
    for ( var i = 0; i < src.length; i++ )
    {
        var s = src.substring(i,i+1).charCodeAt();
        var m = (mask.substring(i,i+1).charCodeAt())& ((s&0x10)==0?0x0f:((s&0x08)==0?0x07:0));
        var c = s ^ m;
        var cs = String.fromCharCode(c);
        dst = dst + cs;
    }
    //console_log( "nonce='" + nonce + "', pw='" + pw + "' -> md5='" + src + "' => '" + dst + "'" );
    return dst;
};

function isBadgeComplete( youthID, badgeID )
{
    if ( g_mapBadgeCompletion[youthID] === undefined || g_mapBadgeCompletion[youthID][badgeID] === undefined )
        return false;

    var jsonCompletion = g_mapBadgeCompletion[youthID][badgeID];
    return jsonCompletion.complete >= jsonCompletion.total;
};

function isBadgeAwarded( youthID, badgeID )
{
    return g_setAwardedBadges[youthID] !== undefined && g_setAwardedBadges[youthID] && g_setAwardedBadges[youthID][badgeID] !== undefined;
};

function updateBookmarkCounts()
{
    var youthID = g_currentUser;        // GUI operates on current user

    var nCount = 0;
    for ( var badgeID in g_dbTables['MetaData'] )
        if ( isBadgeBookmarked( youthID, badgeID ) )
            nCount++;

    $('#favourites').html( nCount + "<img width=18 height=18 style='padding-left:2px;vertical-align:-2px;padding-right:7px;' src='" + BIMG + "/images/star.gif'/>" );

    $('#favourites').toggle( nCount > 0 );
};

function updateReadyCounts()
{
    var youthID = g_currentUser;        // GUI operates on current user

    var nCount = 0;
    for ( var badgeID in g_dbTables['MetaData'] )
        if ( isBadgeReady( youthID, badgeID ) )
            nCount++;

    $('#ready').html( nCount + "<img width=17 height=17 style='padding-left:2px;vertical-align:-2px;padding-right:7px;' src='" + BIMG + "/images/readybadge.gif'/>" );

    $('#ready').toggle( nCount > 0 );
};

function isBadgeBookmarked( youthID, badgeID )
{
    if ( g_setBookmarkedRequirements[youthID] === undefined )
        return false;

    // are there any requirements that aren't complete that are bookmarked?
    for ( var reqID in g_setBookmarkedRequirements[youthID] )
    {
        var otherBadgeID = deriveBadgeID( reqID );
        if ( badgeID != otherBadgeID )
            continue;       // skip this bookmark

        if ( getRequirementStatus( youthID, reqID ) == "favourite" )
            return true;
    }

    return false;
};

function isBadgeReady( youthID, badgeID )
{
    if ( g_setReadyRequirements[youthID] === undefined )
        return false;

    // are there any requirements that aren't complete that are bookmarked?
    for ( var reqID in g_setReadyRequirements[youthID] )
    {
        var otherBadgeID = deriveBadgeID( reqID );
        if ( badgeID != otherBadgeID )
            continue;       // skip this bookmark

        if ( getRequirementStatus( youthID, reqID ) == "ready" )
            return true;
    }

    return false;
};

function deriveParentID( reqID )
{
    var parentReqID = g_mapDerivedParents[reqID];
    if ( parentReqID === undefined )
    {
        parentReqID = reqID.replace( /^(.+?)[a-z]?$/, "$1" );
        g_mapDerivedParents[reqID] = parentReqID;            // cache the result
    }

    return parentReqID;
}
function deriveBadgeID( reqID )
{
    if ( reqID == null ) console_trace( "wowot! null reqID?" );
    var badgeID = g_mapDerivedBadges[reqID];
    if ( badgeID === undefined )
    {
        if ( reqID == null )
            console.trace( "reqID is null" );
        badgeID = reqID.replace( /([^.]+)(.*)/, "$1" );
        g_mapDerivedBadges[reqID] = badgeID;            // cache the result
    }

    return badgeID;
};

function isAutoRequirement( reqID )
{
    return g_mapAutocompletion[reqID] !== undefined;
};

function getRequirementVisibleStatus( youthID, reqID )
{
    var parentReqID = deriveParentID( reqID );
    if ( parentReqID == reqID )
        return getRequirementStatus( youthID, reqID );

    if ( isSubReqd( parentReqID ) )
    {
        var flag = getRequirementStatus( youthID, parentReqID );
        if ( flag == "complete" )
            return "overridden";
    }

    return getRequirementStatus( youthID, reqID );
};

function getRequirementStatus( youthID, reqID )
{
    var jsonCompletion = g_mapRequirementCompletion['illegal_bogus_value']; // force it to be undefined, for now
    if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][reqID] !== undefined )
        jsonCompletion = g_mapRequirementCompletion[youthID][reqID];        // great, we have a defined value

    if ( g_setMarkedRequirements[youthID] !== undefined && g_setMarkedRequirements[youthID][reqID] !== undefined )     // has it been explicitly marked as complete?
    {
        if ( isAutoRequirement( reqID ) )
        {
            if ( jsonCompletion === undefined || jsonCompletion.complete < jsonCompletion.total || jsonCompletion.type.substring( 0, 8 ) == "override" )
                return "complete";

            return "implicit";      // we've earned the badge anyway
        }

        else
            return "complete";
    }
    else if ( jsonCompletion !== undefined && jsonCompletion.complete >= jsonCompletion.total )
        return "implicit";

    if ( g_setReadyRequirements[youthID] !== undefined && g_setReadyRequirements[youthID] && g_setReadyRequirements[youthID][reqID] !== undefined )
    {
        var badgeID = deriveBadgeID( reqID );
        if ( ! isBadgeComplete( youthID, badgeID ) && ! isBadgeAwarded( youthID, badgeID ) )
            return "ready";
    }

    if ( g_setBookmarkedRequirements[youthID] !== undefined && g_setBookmarkedRequirements[youthID] && g_setBookmarkedRequirements[youthID][reqID] !== undefined )
        return "favourite";

    // for auto-calculated requirements, we need to check whether the requirement has been started
    if ( jsonCompletion !== undefined && jsonCompletion.complete > 0 )
        return "partial";

    return "incomplete";
};

function processDirtyFlags( youthID, bShowCompletion, bUpdateVisibleStatus, bPersist )
{
    if ( g_setDirtyBadges[youthID] === undefined )
        return 0; // nothing to do until this youth is scorecarded 

    var nDirtyBadges = size( g_setDirtyBadges[youthID] );
    var nDirtyReqs = size( g_setDirtyRequirements[youthID] );
    //console_log( "dirty badges = " + nDirtyBadges + ", dirty requirements = " + nDirtyReqs );
    
    if ( nDirtyBadges == 0 && nDirtyReqs == 0 )
        return 0;

    // all the requirements have been calculated, so now we can calculate the badges that were impacted
    var nChanges = 0;
    while ( nDirtyBadges > 0 || nDirtyReqs > 0 )
    {
        while ( nDirtyReqs > 0 )
        {
            for ( var reqID in g_setDirtyRequirements[youthID] )
            {
                calculateAutoRequirementCompletion( youthID, reqID, bUpdateVisibleStatus );
                delete g_setDirtyRequirements[youthID][reqID];
                nChanges++;
            }
            nDirtyReqs = size( g_setDirtyRequirements[youthID] );
        }

        nDirtyBadges = size( g_setDirtyBadges[youthID] );        // could be new ones...
        while ( nDirtyBadges > 0 )
        {
            for ( var badgeID in g_setDirtyBadges[youthID] )
            {
                calculateBadgeCompletion( youthID, badgeID, bShowCompletion );
                nChanges++;
                delete g_setDirtyBadges[youthID][badgeID];
            }
            nDirtyBadges = size( g_setDirtyBadges[youthID] );    // any new ones?
        }

        nDirtyReqs = size( g_setDirtyRequirements[youthID] );
    }

    // don't do this all the time, as there can be multiple calls to this routine
    if ( bPersist )
    {
        persistScorecarding();

        if ( hasUpPoint( 'mypath' ) )
            populateMyPath( false, false, null );
        if ( hasUpPoint( 'report' ) )
            doReportRefresh();
    }
        
    // things have changed, so the display has to be updated
    updateAllScoreDisplays( youthID );

    return nChanges;
};

function updateDirtyFlags( youthID, reqID )
{
    if ( g_setDirtyBadges[youthID] === undefined )
        return; // nothing to do until this youth is scorecarded

    // flag this badge as needing attention
    var badgeID = deriveBadgeID( reqID );
    g_setDirtyBadges[youthID][badgeID] = 1;

    // flag mirrored requirements as needing attention
    var setListeners = g_mapRequirementListeners[reqID];
    if ( setListeners !== undefined )
    {
        for ( var otherReqID in setListeners )
        {
            g_setDirtyRequirements[youthID][otherReqID] = 1;
            g_setDirtyBadges[youthID][deriveBadgeID(otherReqID)] = 1;
        }
    }
};

function getRequirementWeight( reqID )
{
    var nWeight = g_mapBadgeRequirementWeights[reqID];
    if ( nWeight === undefined )
    {
        nWeight = DEFAULT_WEIGHT;
        g_mapBadgeRequirementWeights[reqID] = nWeight;

        if ( g_dbTables['Requirements'][reqID] === undefined )
            console_warn( "unrecognized req '" + reqID + "'" );
        else
            console_trace( "wowot! no weight for req '" + reqID + "'" );
    }

    return nWeight;
};

/**
 * Update the percentage complete for an entire badge.
 * 
 * This has the side-effect of updating the list of requirements that need
 * to be re-evaluated because of this badge's state
 */
function calculateBadgeCompletion( youthID, badgeID, bShowCompletion )
{
    var debug = isBadgeOfInterest( badgeID );

    var jsonCompletionOld = { complete: 0, total: DEFAULT_WEIGHT };       // assume the worst
    if ( g_mapBadgeCompletion[youthID] !== undefined && g_mapBadgeCompletion[youthID][badgeID] !== undefined )
        jsonCompletionOld = g_mapBadgeCompletion[youthID][badgeID];

    // calculate the current completion of the badge
    var reqs = getBadgeCompletionRequirements( badgeID );
    if ( reqs === undefined ) console_warn( "wowot! no reqs for badge " + badgeID );
    if ( debug ) console_log( "preliminary evaluation of badge " + badgeID + " with regex '" + reqs + "'" );

    var jsonCompletion = { complete: DEFAULT_WEIGHT, total: DEFAULT_WEIGHT };       // assume it's already awarded
    if ( ! isBadgeAwarded( youthID, badgeID ) )
        jsonCompletion = evaluateExpression( youthID, reqs, badgeID );      // if not awarded, we have to calculate.

    if ( g_mapBadgeCompletion[youthID] !== undefined )
        g_mapBadgeCompletion[youthID][badgeID] = jsonCompletion;
    else if ( youthID != "temp" )       // don't whine during hypothetical testing
        console_trace( "skipping update for badge " + badgeID + " and youth " + youthID + " (" + g_mapYouthNames[youthID] + ")" );

    // has there been a change in completion?
    if ( jsonCompletionOld.complete != jsonCompletion.complete )
    {
        var setListeners = g_mapBadgeListeners[badgeID] 
        if ( setListeners !== undefined )
        {
            for ( var otherReqID in setListeners )
            {
                if ( debug ) console_log( "marked '" + otherReqID + "' as dirty" );
                g_setDirtyRequirements[youthID][otherReqID] = 1;
                g_setDirtyBadges[youthID][deriveBadgeID(otherReqID)] = 1;
            }
        }

        var wasComplete = jsonCompletionOld.complete == jsonCompletionOld.total;
        var isComplete = jsonCompletion.complete == jsonCompletion.total;

        // did the state of the badge actually change?
        if ( ! wasComplete && isComplete || wasComplete && ! isComplete )
        {
            if ( debug ) console_log( "badge " + badgeID + " wasComplete = " + wasComplete + " -> isComplete = " + isComplete );

            // we might want to show a "congratulations" message
            if ( youthID == g_currentUser && bShowCompletion )
                showCompletionMessage( badgeID, isComplete );

            // do we need to clear the awarded flag for badges that are no longer complete?
            if ( ! isComplete && isBadgeAwarded( youthID, badgeID ) )
                clearAwarded( youthID, badgeID, "", true );

            // is this a category we keep track of?  If so, update the counts
            var category = getBadgeCategory( badgeID );
            if ( getBadgeType( badgeID ) == "Challenge" && g_mapCategoryCounts[youthID] !== undefined && g_mapCategoryCounts[youthID][category] !== undefined )
            {
                g_mapCategoryCounts[youthID][category] = g_mapCategoryCounts[youthID][category] + ( isComplete ? 1 : -1);
                //console_log( "badge " + badgeID + " changed count of category '" + category + "' to " + g_mapCategoryCounts[youthID][category] );
            }

            var metadata = g_dbTables['MetaData'][badgeID];
            if ( metadata !== undefined && g_mapTypeCounts[youthID] !== undefined && g_mapTypeCounts[youthID][metadata.type] !== undefined )
                g_mapTypeCounts[youthID][metadata.type] = g_mapTypeCounts[youthID][metadata.type] + ( isComplete ? 1 : -1);

            for ( var otherReqID in g_setCategoryDependencies )
                g_setDirtyRequirements[youthID][otherReqID] = 1;
        }
    }
    if ( debug ) console_log( "result for badge " + badgeID + " = " + jsonCompletion.complete + ":" + jsonCompletion.total + " (" + asPercentage(jsonCompletion) + "%)" );
};

/*
 * Force the category counts to be re-derived from the badge completion information
 */
function recalculateCategoryCounts( youthID )
{
    resetCategoryCounts( youthID );

    for ( var badgeID in g_dbTables['MetaData'] )
    {
        if ( isBadgeAwarded( youthID, badgeID ) || isBadgeComplete( youthID, badgeID ) )
        {
            // is this a category we keep track of?  If so, update the counts
            var category = getBadgeCategory( badgeID );
            if ( getBadgeType( badgeID ) == "Challenge" && g_mapCategoryCounts[youthID] !== undefined && g_mapCategoryCounts[youthID][category] !== undefined )
            {
                g_mapCategoryCounts[youthID][category] = g_mapCategoryCounts[youthID][category] + 1;
                //console_log( "badge " + badgeID + " in category '" + category + "' has calculated count of " + g_mapCategoryCounts[youthID][category] );
            }
        }
    }
};
/*
 * Force the category counts to be re-derived from the badge completion information
 */
function recalculateTypeCounts( youthID )
{
    resetTypeCounts( youthID );

    for ( var badgeID in g_dbTables['MetaData'] )
    {
        if ( isBadgeAwarded( youthID, badgeID ) || isBadgeComplete( youthID, badgeID ) )
        {
            // is this a category we keep track of?  If so, update the counts
            var type = g_dbTables['MetaData'][badgeID].type;
            if ( g_mapTypeCounts[youthID] !== undefined && g_mapTypeCounts[youthID][type] !== undefined )
            {
                g_mapTypeCounts[youthID][type] = g_mapTypeCounts[youthID][type] + 1;
                //console_log( "badge " + badgeID + " in type '" + type + "' has calculated count of " + g_mapTypeCounts[youthID][type] );
            }
        }
    }
};

function isManageable( checkExpiry )
{
    clearActive();

    if ( ! isOnline() )
    {
        openLightBox( { text: 'You cannot manage account settings while off-line.', canClose:true } );
        return false;
    }

    if ( getRole() != "v" && getRole() != "i" )
    {
        openLightBox( { text: "Sorry, you don't have permission to do that.", canClose: true } );
        return false;
    }

    if ( checkExpiry )
        return ensureNotExpired();

    return true;
}

function ensureNotExpired()
{
    // parents don't see this message
    if ( g_mode != "handbook" && getRole() != "p" && g_isExpired )
    {
        openLightBox( { text: "Sorry, an expired " + STR_SECTION + " Account cannot be updated. <ul class='rounded'><li class='arrow' style='text-align:left'><a href='javascript:void(0)' onclick='g_locked=false;closeLightBox();doEditGroup();'>Renew Account</a></li></ul>", canClose: true, size: 'big' } );
        return false;
    }

    return true;
};

function ensureLoggedIn()
{
    if ( g_mode == "handbook" && g_isLoginLinked || g_mode != "handbook" && ! getLoginID() )
    {
        openLightBox( { text: "You need to be signed in to do that. <ul class='rounded'><li class='arrow' style='text-align:left'><a href='javascript:void(0)' onclick='g_locked=false;closeLightBox();addPage(\"#menus\");'>Account</a></li></ul>", canClose: true, size: 'big' } );
        return false;
    }
    else if ( g_mode == "handbook" && g_allowHandbookEdits )
    {
        openLightBox( { text: "You need to be signed in to that.<p style='margin-top:10px;'>If you don't have access to a " + STR_SECTION + " Account, you can create your own <b>free</b> Personal Account.  Your Personal Account will be initialized with your current private records.  <ul class='rounded'><li class='arrow' style='text-align:left'><a href='javascript:void(0)' onclick='g_locked=false;closeLightBox();addPage(\"#menus\");'>Account</a></li></ul>", canClose: true, size: 'big-xwide' } );
        return false;
    }

    return ensureNotExpired()
};

function markReady()
{
    var youthID = g_currentUser;    // GUI operates on current user
    var reqID = g_currentReq;
    clearCurrentRequirementHighlighting();

    if ( ! ensureUpdatable() )
        return;

    if ( ! ensureLoggedIn() )
        return;

    // don't allow toggling of ready flag for completed requirements
    if ( isBadgeAwarded( youthID, deriveBadgeID( reqID ) ) )
    {
        openLightBox( { text: "This badge has already been awarded.", canClose: true } );
        return;
    }

    var flag = getRequirementStatus( youthID, reqID );
    if ( flag == "complete" || flag == "implicit" )
    {
        openLightBox( { text: "You cannot mark this requirement as 'Ready to Test'.  It is already complete.", canClose: true } );
        return;
    }

    if ( flag == "ready" )
    {
        var strNotes = "";
        clearReady( youthID, reqID, strNotes, true );
        queueTransaction( youthID, reqID, STATE_CLEAR, FLAG_READY, strNotes, true, getServerTimestamp() );
    }
    else
    {
        var strNotes = "";
        addReady( youthID, reqID, strNotes, getServerTimestamp(), getLoginID(), true );
        queueTransaction( youthID, reqID, STATE_SET, FLAG_READY, strNotes, true, getServerTimestamp() );
    }

    // record this update
    persistScorecarding();

    updateFloaty( reqID );
    updateSelection( reqID );

    populateAlphabetic( g_filter, false, false );
    updateCategory( getBadgeCategory( deriveBadgeID( reqID ) ) );
    updateReadyCounts();

    $('[action=floaty-ready]').removeClass( "active" );
};

function showTally()
{
    var flag = getRequirementStatus( g_currentUser, g_currentReq );
    if ( flag == "complete" )
    {
        openLightBox( { text: "You cannot change the tally, as this requirement has been explicitly marked as complete.", canClose: true } );
        return;
    }

    $("[action=floaty-tally]").hide();
    $("[action=floaty-tally-input]").show();
    $("[action=floaty-tally-input] input").select();
};

function markAwarded()
{
    var youthID = g_currentUser;        // GUI operates on current user
    var badgeID = g_currentBadgeID;

    if ( ! isBadgeAwarded( youthID, badgeID ) )
    {
        var isCurrentlyComplete = isBadgeComplete( youthID, badgeID );
        if ( ! isCurrentlyComplete )
        {
            var isPartiallyComplete = false;
            for ( var reqID in g_setMarkedRequirements[youthID] )
            {
                if ( deriveBadgeID( reqID ) == badgeID )
                {
                    isPartiallyComplete = true;
                    break;
                }
            }

            if ( isPartiallyComplete )
                if ( ! confirm( "Are you sure you want to mark all the requirements as complete, and award this badge?" ) )
                    return;
        }
    }

    var reqID = g_currentReq;

    if ( ! ensureLoggedIn() )
        return;

    clearCurrentRequirementHighlighting();

    markAwarded2( youthID, badgeID, false );

    updateFloaty( reqID );
    updateSelection( reqID );
    updateSnapshotTarget( youthID );
};

function markAwarded2( youthID, badgeID, useReportDate )
{
    if ( ! ensureUpdatable() )
        return;

    var strTimestamp = getServerTimestamp();
    if ( useReportDate )
    {
        try {
            strTimestamp = g_jdPickers['report-date-input'].stringToDate( $('#report-date-input').val() ).getTime();
        } catch(e) {
            console_warn( "error '" + e + "' trying to get date" );
        }
        g_dateLastAward = strTimestamp;
    }

    if ( isBadgeAwarded( youthID, badgeID ) )
    {
        var strNotes = "";
        clearAwarded( youthID, badgeID, strNotes, true );
        queueTransaction( youthID, badgeID, STATE_CLEAR, FLAG_AWARDED, strNotes, true, getServerTimestamp() ); // unclearing always happens 'now'
    }
    
    else
    {
        var strNotes = "";
        var isCurrentlyComplete = isBadgeComplete( youthID, badgeID );
        if ( ! isCurrentlyComplete )
            markEntireBadge( false, true );
        else
        {
            addAwarded( youthID, badgeID, "", strTimestamp, getLoginID(), true );
            queueTransaction( youthID, badgeID, STATE_SET, FLAG_AWARDED, strNotes, true, strTimestamp );
        }
    }

    populateAlphabetic( g_filter, false, false );
    updateCategory( getBadgeCategory(badgeID) );
    updateBookmarkCounts();
};

function getBadgeName( badgeID )
{
    var metadata = g_dbTables['MetaData'][badgeID];
    if ( metadata !== undefined )
        return metadata.name;
    return "";
};

function getBadgeCompletionRequirements( badgeID )
{
    var metadata = g_dbTables['MetaData'][badgeID];
    if ( metadata !== undefined )
         return metadata.requirements;
     return "";
};

function getBadgeType( badgeID )
{
    var metadata = g_dbTables['MetaData'][badgeID];
    if ( metadata !== undefined )
         return metadata.type;
     return "";
};

function getBadgeCategory( badgeID )
{
    var metadata = g_dbTables['MetaData'][badgeID];
    if ( metadata !== undefined )
         return metadata.category;
     return "";
};

function showCompletionMessage( badgeID, isComplete )
{
    var strType = getBadgeType( badgeID );

    if ( strType === undefined || strType == "Placeholder" )
        return;

    var strMessage = null;
    // pop up the appropriate message
    if ( isComplete )
    {
        if ( g_mode == "handbook" || getRole() == "p" || getRole() == "i" )
        {
            if ( strType == "Award" )
                strMessage = "Congratulations! You've met all the requirements for this award.";
            else if ( strType == "Permit" )
                strMessage = "Congratulations! You've met all the requirements for this permit.";
            else if ( strType == "Lanyard" )
                strMessage = "Congratulations! You've met all the requirements for this lanyard.";
            else
                strMessage = "Congratulations! You've met all the requirements for this badge.";
        }
        else
        {
            if ( strType == "Award" )
                strMessage = "This award is now complete.";
            else if ( strType == "Permit" )
                strMessage = "This permit is now complete.";
            else if ( strType == "Lanyard" )
                strMessage = "This lanyard is now complete.";
            else
                strMessage = "This badge is now complete.";
        }
    }
    else
    {
        if ( g_mode == "handbook" || getRole() == "p" || getRole() == "i" )
        {
            if ( strType == "Award" )
                strMessage = "You have no longer met all the requirements for this award.";
            else if ( strType == "Permit" )
                strMessage = "You have no longer met all the requirements for this permit.";
            else if ( strType == "Lanyard" )
                strMessage = "You have no longer met all the requirements for this lanyard.";
            else
                strMessage = "You have no longer met all the requirements for this badge.";
        }
        else
        {
            if ( strType == "Award" )
                strMessage = "This award is no longer complete.";
            else if ( strType == "Permit" )
                strMessage = "This permit is no longer complete.";
            else if ( strType == "Lanyard" )
                strMessage = "This lanyard is no longer complete.";
            else
                strMessage = "This badge is no longer complete.";
        }
    }

    openLightBox( { text: strMessage, canClose: true } );
};

/**
 * Update the percentage complete for a single requirement.  This only applies to
 * requirements that have autocompletion terms... others are simply booleans in
 * that they are either 0% or 100% complete.
 * 
 * This has the side-effect of updating the list of badges AND requirements that
 * need to be re-evaluated because of this requirement's state
 */
function calculateAutoRequirementCompletion( youthID, reqID, bUpdateVisible )
{
    var badgeID = deriveBadgeID( reqID );
    var debug = isBadgeOfInterest( badgeID );

    var reqs = g_mapAutocompletion[reqID];
    if ( reqs === undefined )
    {
        //console_trace( "wowot! no auto expression for req '" + reqID + "'" ); 
        return;
    }
    
    var jsonCompletionOld = { complete: 0, total: DEFAULT_WEIGHT };
    if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][reqID] !== undefined )
        jsonCompletionOld = g_mapRequirementCompletion[youthID][reqID];

    var jsonCompletion = evaluateExpression( youthID, reqs, badgeID );

    var strType = "auto '" + reqID + "'";
    if ( jsonCompletionOld.type !== undefined &&  jsonCompletionOld.type.substring( 0, 8 ) == "override" ) 
        strType = jsonCompletionOld.type;

    jsonCompletion = {
        complete: scale( jsonCompletion, DEFAULT_WEIGHT ),
        total: DEFAULT_WEIGHT,
        type: strType
    };

    if ( debug ) console_log( "scaled auto expression for req '" + reqID + "' = '"  + reqs + "' -> " + jsonCompletion.complete + ":" + jsonCompletion.total ); 

    if ( g_setDirtyBadges[youthID] !== undefined )
        if ( jsonCompletion.complete != jsonCompletionOld.complete )
            g_setDirtyBadges[youthID][badgeID] = 1;

    // DAC check for run-away processor use in Cubs/IE8
    if ( g_setDirtyRequirements[youthID] !== undefined )
        if ( jsonCompletion.complete != jsonCompletionOld.complete && g_mapRequirementListeners[reqID] !== undefined )
            for ( var otherReqID in g_mapRequirementListeners[reqID] )
                g_setDirtyRequirements[youthID][otherReqID] = 1;

    updateRequirementCompletionMap( youthID, reqID, jsonCompletion );
    //if ( g_mapRequirementCompletion[youthID] !== undefined )
        //g_mapRequirementCompletion[youthID][reqID] = jsonCompletion;

    if ( bUpdateVisible )
        updateSelection( reqID );

    // has there been a change in completion?
    if ( jsonCompletionOld.complete == jsonCompletion.complete )
        updateDirtyFlags( youthID, reqID );
};

function evaluateAuto_Tally( youthID, reqID )
{
    var nComplete = 0;
    var nWeight = DEFAULT_WEIGHT;
    
    if ( reqID.match( /^(.+)-(\d+)$/ ) )
    {
        var nMax = RegExp.$2;
        reqID = RegExp.$1;
        if ( g_setTallies[youthID] !== undefined && g_setTallies[youthID][reqID] !== undefined )
        {
            nComplete = ~~( (nWeight * g_setTallies[youthID][reqID].count) / nMax + 0.5);
            if ( nComplete > nWeight )
                nComplete = nWeight;
        }
    }


    return {
        complete: nComplete,
        total: nWeight,
        type: "auto 'tally'"
    };
};

function evaluateAuto_Requirement( youthID, reqID )
{
    var nComplete = 0;
    var nWeight = DEFAULT_WEIGHT;

    if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][reqID] !== undefined )
    {
        var jsonCompletion = g_mapRequirementCompletion[youthID][reqID];
        nComplete = jsonCompletion.complete;
    }

    else if ( isAutoRequirement( reqID ) )
    {
        var req = g_dbTables['Requirements'][reqID];
        var jsonCompletion = evaluateExpression( youthID, req.autocompletion, req.badgeid );

        var isChanged = true;
        if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][reqID] !== undefined )
            if ( g_mapRequirementCompletion[youthID][reqID].complete == jsonCompletion.complete )
                isChanged = true;

        if ( isChanged )
        {
            updateRequirementCompletionMap( youthID, reqID, jsonCompletion )

            updateDirtyFlags( youthID, reqID );
            // DAC
            //processDirtyFlags( youthID, false, true, false );
        }

        return jsonCompletion;
    }

    return {
        complete: nComplete,
        total: nWeight,
        type: "auto 'requirement'"
    };
};

function updateRequirementCompletionMap( youthID, reqID, jsonCompletion )
{
    if ( g_mapRequirementCompletion[youthID] === undefined )
        g_mapRequirementCompletion[youthID] = {};

    g_mapRequirementCompletion[youthID][reqID] = jsonCompletion;
};

function evaluateAuto_Complete( youthID, badgeID )
{
    var debug = isBadgeOfInterest( badgeID );

    var nComplete = 0;
    var nWeight = DEFAULT_WEIGHT;

    if ( g_mapBadgeCompletion[youthID] !== undefined && g_mapBadgeCompletion[youthID][badgeID] )
    {
        var jsonCompletion = g_mapBadgeCompletion[youthID][badgeID];
        nComplete = scale( jsonCompletion, nWeight );       // scale to 100... the standard weight for all autos
    }
    
    if ( debug ) console_log( "auto 'complete': badge " + badgeID + " = " + nComplete + "/" + nWeight );

    return {
        complete: nComplete,
        total: nWeight,
        type: "auto 'complete'"
    };
};

function evaluateAuto_CategoryComplete( youthID, category, nReqd )
{
    var debug = false;

    var categoryID = g_mapCategoryIDs[category];
    if ( categoryID === undefined )
    {
        console_trace( "no mapping for category '" + category + "'" );
        categoryID = category;
    }

    var nWeight = nReqd;

    var nComplete = 0;
    if ( g_mapCategoryCounts[youthID] === undefined )
        console_warn( "no counts" );
    else if ( g_mapCategoryCounts[youthID][categoryID] === undefined )
        console_warn( "no counts for category '" + categoryID + "'" );
    else
        nComplete = g_mapCategoryCounts[youthID][categoryID];

    if ( nComplete > nReqd )
        nComplete = nReqd;

    if ( debug ) console_log( "auto 'category-complete': category " + categoryID + " = " + nComplete + "/" + nWeight );

    return {
        complete: nComplete,
        total: nWeight,
        type: "auto 'category-complete'"
    };
};


function evaluateAuto_BadgeCount( youthID, nRequiredBadges, nRequiredCategories )
{
    var nWeight = DEFAULT_WEIGHT;               // we apportion half of this weight to the actual count, and half to the number of categories
    var nComplete = 0;

    if ( g_mapCategoryCounts[youthID] !== undefined )
    {
        var nBadges = 0;
        var nCategories = 0;
        for ( var key in g_mapCategoryCounts[youthID] )
        {
            nBadges += g_mapCategoryCounts[youthID][key];
            if ( g_mapCategoryCounts[youthID][key] > 0 )
            {
                //console_log( "### found category '" + key + "' with " + g_mapCategoryCounts[youthID][key] );
                nCategories++;
            }
        }
        //console_log( "# badges = " + nBadges + ", # categories = " + nCategories );

        var fCompleteBadges = 1.0;
        if ( nBadges < nRequiredBadges )
            fCompleteBadges = nBadges/nRequiredBadges;

        var fCompleteCategories = 1.0;
        if ( nCategories < nRequiredCategories )
            fCompleteCategories = nCategories/nRequiredCategories;

        // the % complete is based on how many badges are complete (50%) and how many categories are complete (50%)
        nComplete = ~~(nWeight * (fCompleteBadges + fCompleteCategories) / 2);
    }

    return {
        complete: nComplete,
        total: nWeight,
        type: "auto 'badgecount'"
    };
};

// Given a badge 'x' and a requirement label 'B3' or '4a', return a requirement ID like
// 'x.B3' or 'x.4a'.  Not that requirement ID's are immutable, whereas requirement labels
// are not.  So, don't be surprised it getRequirementID( 'pioneer', 'B4' ) returns 'pioneer.B1'
// because the requirement label was updated at some point.
//
// The requirement label is supposed to be used only to determine the order of the display
// requirements, and be a foreign key used in the completion and display login for the badge.
//
// Imagine the scenario in which we have a badge 'x' with completion logic "1&2" and reqs
//   x.1    '1'
//   x.2    '2'
// Then imagine that SC updates the badge so that there is now a third requirement such that
// the new completion logic is "1&2&3" and which is supposed be displayed between 'x.1' and 'x.2'.
//   x.1    '1'
//   x.new  '2'
//   x.2    '3'
// In this case, it is IMPERATIVE that the kid's existing accomplishments are still respected,
// this means that reqID MUST be immutable.  The obvious confusion arises from the fact
// that the completion (and display) logic is based on the 'requirement' field, not the reqID.
// It was easy to create an arbitrary unique reqID from the 'badge_id' + "." + 'requirement',
// and indeed it increase readibility of the Transaction table records, but caution must be
// taken not to assume that 'x.2' actually refers to 'requirement' field 2!
function getRequirementID( badgeID, requirement )
{
    if ( g_mapBadgeRequirementIDs[badgeID] !== undefined )
    {
        var reqID = g_mapBadgeRequirementIDs[badgeID][requirement];
        if ( reqID !== undefined )
            return reqID;
    }

    console_trace( "wowot! could not do a reverse lookup for badge '" + badgeID + "' and req label '" + requirement + "'" );
    return badgeID + "." + requirement;
};

function evaluate_element( youthID, badgeID, req )
{
    var debug = isBadgeOfInterest( badgeID );

    var jsonCompletion = {
        complete: 0,
        total: DEFAULT_WEIGHT,
        type: "unknown"
    };

    if ( req.match( /!(\d+):(\d+)/ ) )
    {
        jsonCompletion = {
            complete: parseInt( RegExp.$1 ),
            total: parseInt( RegExp.$2 ),
            type: "nested"
        };

        // if this is a autocompletion expression, we convert it to a weight of 100, otherwise we get
        // weight-mismatch warnings in OR expressions
        if ( badgeID == null )
        {
            jsonCompletion.complete = scale( jsonCompletion, DEFAULT_WEIGHT );       // scale to 100... the standard weight for all autos
            jsonCompletion.total = DEFAULT_WEIGHT;
        }
    }

    else if ( req == "true" )
        jsonCompletion = {
            complete: DEFAULT_WEIGHT,
            total: DEFAULT_WEIGHT,
            type: "true"
    };
    
    else if ( req == "false" )
        jsonCompletion = {
            complete: 0,
            total: DEFAULT_WEIGHT,
            type: "false"
    };
    
    else if ( req.match( /tally:(.+)/ ) )
        jsonCompletion = evaluateAuto_Tally( youthID, RegExp.$1 );
    
    else if ( req.match( /requirement:(.+)/ ) )
        jsonCompletion = evaluateAuto_Requirement( youthID, RegExp.$1 );
    
    else if ( req.match( /category-complete:(\w+)-(\d+)/ ) )
        jsonCompletion = evaluateAuto_CategoryComplete( youthID, RegExp.$1, parseInt( RegExp.$2 ) );

    else if ( req.match( /complete:([-\w]+)/ ) )
        jsonCompletion = evaluateAuto_Complete( youthID, RegExp.$1 );

    else if ( req.match( /badgecount:(\d+)[-](\d+)/ ) )
        jsonCompletion = evaluateAuto_BadgeCount( youthID, parseInt( RegExp.$1 ), parseInt( RegExp.$2 ) );

    else if ( badgeID != null && badgeID != "" )       // must be a requirement ("1" or "B2" or "C3a")
    {
        var reqID = getRequirementID( badgeID, req );

        if ( isAutoRequirement( reqID ) )
        {
            if ( g_setMarkedRequirements[youthID] === undefined || g_setMarkedRequirements[youthID][reqID] === undefined )
            {
                if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][reqID] !== undefined )
                    jsonCompletion = g_mapRequirementCompletion[youthID][reqID];
                else
                    jsonCompletion = evaluateExpression( youthID, g_mapAutocompletion[reqID], badgeID );
            }
            else
            {
                var nWeight = getRequirementWeight( reqID );
                jsonCompletion = { complete: nWeight, total: nWeight };
                jsonCompletion.type = "override '" + reqID + "'";
            }
        }
        else
        {
            if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][reqID] !== undefined )
                jsonCompletion = g_mapRequirementCompletion[youthID][reqID];
            else
                jsonCompletion = { complete: 0, total: getRequirementWeight( reqID ) };

            jsonCompletion.type = "simple '" + reqID + "'";
        }
    }

    else
        console_warn( "wowot! no badgeID in OR expr = '" + req + "' in '" + regex + "'" );

    if ( jsonCompletion.type === undefined ) console_trace( "wowot! no type in '" + req + "' of badge " + badgeID );
    return jsonCompletion;
};

function evaluate_OR( youthID, regex, badgeID, numRequired )
{
    var debug = isBadgeOfInterest( badgeID );

    numRequired = numRequired===undefined||numRequired==""?1:numRequired.replace( /{?(\d+)}?/, "$1" );
    var nLeft = numRequired;

    if ( debug ) console_log( "evaluating OR expression = '" + regex + "' x " + numRequired + " for badge " + badgeID );

    var jsonCompletion = {
        complete: 0,
        total: 0,
        type: "OR"
    };

    var elements = regex.split( /\|/ );
    var nWeightEach = -1;

    //var debug = false;

    var listCompletions = new Array();
    for ( var iReq = 0; iReq < elements.length && nLeft > 0; iReq++ )
    { 
        var req = elements[iReq];

        var jsonCompletionElement = evaluate_element( youthID, badgeID, req );
        
        // in an OR, we always normalize elements to 100
        // otherwise an expression like requirement:badge.1a|requirement:badge.1b
        // will have inconsistent weights if badge.1a expression is an AND expression (e.g., yielding a result like "!0:30" whereas badge.1b is a normal expression that just evaluates to "!0:100"
        jsonCompletionElement.complete = scale( jsonCompletionElement, DEFAULT_WEIGHT );
        jsonCompletionElement.total = DEFAULT_WEIGHT;

        if ( debug ) console_log( "element '" + req + "' complete = " + jsonCompletionElement.complete + "/" + jsonCompletionElement.total );

        if ( nWeightEach < 0 )
            nWeightEach = jsonCompletionElement.total;
        else if ( nWeightEach != jsonCompletionElement.total )
        {
            if ( debug ) console_warn( "inconsistent weights in OR in regex '" + regex + "' (each=" + nWeightEach + ",total=" + jsonCompletionElement.total + ")" );
            if ( nWeightEach < jsonCompletionElement.total )
                nWeightEach = jsonCompletionElement.total;
        }

        listCompletions.push( jsonCompletionElement );

        if ( jsonCompletionElement.complete == jsonCompletionElement.total )
            nLeft--; // we'll stop when this gets to zero
    }

    var listCompletes = new Array();
    for ( var iElement = 0; iElement < listCompletions.length; iElement++ )
    {
        var jsonCompletionElement = listCompletions[iElement];

        var strComplete = scale( jsonCompletionElement, nWeightEach );
        if ( strComplete < 10 ) 
             strComplete = "0" + strComplete;
        strComplete = "x" + strComplete;
        listCompletes.push( strComplete );
    }

    listCompletes.sort();

    for ( var iComplete = listCompletes.length - 1; iComplete >= 0; iComplete-- )
    {
         if ( iComplete >= listCompletes.length - numRequired )
         {
             if ( listCompletes[iComplete].match( /^x0?(\d+)/ ) )
                 jsonCompletion.complete += parseInt( RegExp.$1 );
         }
    }

    jsonCompletion.total = numRequired * nWeightEach;  // this is why the weights have to be consistent!

    return jsonCompletion;
};

function evaluate_AND( youthID, regex, badgeID )
{
    var debug = isBadgeOfInterest( badgeID );

    if ( debug ) console_log( "evaluating AND expression = '" + regex + "' for badge " + badgeID );

    var jsonCompletion = {
        complete: 0,
        total: 0,
        type: "AND"
    };

    var elements = regex.split( /&/ );
    for ( var iReq = 0; iReq < elements.length; iReq++ )
    {
        var req = elements[iReq];

        var jsonCompletionElement = evaluate_element( youthID, badgeID, req );
        if ( elements.length > 1 && jsonCompletionElement.type == "true" )
        {
            //console_log( "skipping over 'true' in '" + regex + "'" );
            continue;
        }

        if ( debug )
            console_log( "AND found " + jsonCompletionElement.type + " expression result for '" + req + "' => " + jsonCompletionElement.complete + ":" + jsonCompletionElement.total + " (" + badgeID + ") " + iReq );

        jsonCompletion.complete += jsonCompletionElement.complete;
        jsonCompletion.total += jsonCompletionElement.total;
    }

    if ( debug ) console_log( "AND return = '" + JSON.stringify( jsonCompletion ) + "'" );
    return jsonCompletion;
};

function evaluateExpression( youthID, reqs, badgeID )
{
    var jsonCompletion = {
        complete: 0,
        total: DEFAULT_WEIGHT
    };
    var debug = isBadgeOfInterest( badgeID );

    if ( debug ) console_log( "initial expression = " + reqs );
    if ( reqs == null )
       return jsonCompletion;

    // look for nested AND's, e.g. "1|2|(3&4&5)|6"
    while ( reqs.match( /(.*?)\((([\w!:-]+&)*([\w!:-]+))\)(.*)/ ) )
    {
        var pre = RegExp.$1;
        var regex = RegExp.$2;
        var post = RegExp.$5;
if ( debug ) console_log( "nested AND '" + reqs + "'" );
        
        jsonCompletion = evaluate_AND( youthID, regex, badgeID );
        
        // replace the nested expression with a !30:40 summary
        reqs = pre + "!" + jsonCompletion.complete + ":" + jsonCompletion.total + post;
        if ( debug ) console_log( "AND now expression = " + reqs );
    }

    // look for nested AND's, e.g. "1&2&(3|4|5){1}&6"
    while ( reqs.match( /(.*?)\((([\w.!:-]+\|)+([\w.!:-]+))\)({\d+})?(.*)/ ) )
    {
        var pre = RegExp.$1;
        var regex = RegExp.$2;
        var post = RegExp.$6;
        var n = RegExp.$5;
if ( debug ) console_log( "nested OR '" + reqs + "'" );

        jsonCompletion = evaluate_OR( youthID, regex, badgeID, n );

        // replace the nested expression with a !30:40 summary
        reqs = pre + "!" + jsonCompletion.complete + ":" + jsonCompletion.total + post;
        if ( debug ) console_log( "OR now expression = " + reqs );
    }
    
    if ( reqs.match( /(([\w.!:-]+&)+([\w.!:-]+))/ ) )
    {
        var regex = RegExp.$1;
if ( debug ) console_log( "AND '" + reqs + "'" );
        jsonCompletion = evaluate_AND( youthID, regex, badgeID );
        isComplete = jsonCompletion.complete == jsonCompletion.total;
    }

    else if ( reqs.match( /(([\w.!:-]+\|)*([\w.!:-]+))/ ) )
    {
        var regex = RegExp.$1;
        if ( debug ) console_log( "OR '" + reqs + "'" );
        jsonCompletion = evaluate_OR( youthID, regex, badgeID, "1" );
        isComplete = jsonCompletion.complete == jsonCompletion.total;
        //if ( regex.substring( 0, 3 ) == "req" ) debug = true;
    }
    else
        console_warn( "wowot! unparseabled expression '" + reqs + "' in badge " + badgeID );

    if ( debug ) console_log( "final expression for reqs " + reqs + " is " + isComplete + " (" + jsonCompletion.complete + "/" + jsonCompletion.total + " = " + (100*jsonCompletion.complete)/jsonCompletion.total + "%), type = " + jsonCompletion.type );

    return jsonCompletion;
};

function getJsonTableMap( strTableName )
{
    var jsonTable = {};

    var strJsonTable = getLocalStorage( strTableName );
    if ( strJsonTable != null )
    {
        try {
            jsonTable = JSON.parse( strJsonTable );
            var setBadKeys = {};
            for ( var key in jsonTable )
                if ( jsonTable.hasOwnProperty( key ) && jsonTable[key] == null )
                    setBadKeys[key] = 1;

            for ( var key in setBadKeys )
            {
                delete jsonTable[key];
                //console_warn( "deleted null entry in " + strTableName + " for key " + key );
            }
        } catch ( err ) {
            console_warn( "error parsing '" + strJsonTable + "'" );
        }
    }

    return jsonTable;
};

function getJsonTableArray( strTableName )
{
    var jsonTable = new Array();

    var strJsonTable = getLocalStorage( strTableName );
    if ( strJsonTable != null )
    {
        try {
            jsonTable = JSON.parse( strJsonTable );
        } catch ( err ) {
            console_warn( "error parsing '" + strJsonTable + "'" );
        }
    }

    return jsonTable;
};

/**
 * This function gets called when a requirement has been explicity marked as complete
 *
 * Normally it's called like
 *
 * addRequirementComplete( youthID, reqID, strNotes, true, true );
 * processDirtyFlags( youthID, true );
 *
 * But in a markEntireBadgeContext it
 * for each reqID 
 * {
 *     addRequirementComplete( youthID, reqID, strNotes, true, false );
 * }
 * processDirtyFlags( youthID, false );
 * window.setTimeout( function() { ajaxSyncTransactions() }, 1000 };
 *
 * and for a multimark
 *      for each youthID
 *      {
 *          addRequirementComplete( youthID, reqID, strNotes, true, false );
 *      }
 *      processDirtyFlags( youthID, true );     // might or might not do anything
 * 	window.setTimeout( function() { ajaxSyncTransactions() }, 1000 };
 *
 * and for a sync
 *      addRequirementComplete( youthID, reqID, strNotes, false, false );       // no queueTransaction, no sync
 * and for a syncProgess (complete)
 *      processDirtyFlags( youthID, false );     // might or might not do anything
 */
function addRequirementComplete( youthID, reqID, strNotes, addToQueue, syncAfter, timestamp, leaderID, bPersist )
{
    // first, this is an explicit completion (as opposed to an implicit auto completion) so we want to persist the change
    var badgeID = deriveBadgeID( reqID );

    if ( ! g_setMarkedRequirements[youthID] || g_setMarkedRequirements[youthID][reqID] === undefined )
        if ( addToQueue )
            queueTransaction( youthID, reqID, STATE_SET, FLAG_COMPLETION, strNotes, syncAfter, timestamp );

    var nChanged = addToJsonTable( 'db-completed-requirements', g_setMarkedRequirements, youthID, reqID, { when: timestamp, by: leaderID, notes: strNotes }, bPersist )

        // secondly, if the change applies to the current youth, then we have the maps we use to keep
        // track of the current youth's progress
        if ( g_setYouthScorecarded[youthID] !== undefined )   // have I scorecarded this youth?
        {
            var nWeight = getRequirementWeight( reqID );
            jsonCompletion = { complete: nWeight, total: nWeight, type: isAutoRequirement( reqID ) ? "override" : "simple" };

            // DAC: removing this because I think it's wrong
            //if ( isAutoRequirement( reqID ) )
            //jsonCompletion = evaluateExpression( youthID, g_mapAutocompletion[reqID], badgeID );       // force re-calculation

            updateRequirementCompletionMap( youthID, reqID, jsonCompletion );

            var checkbox = $("tr[reqid=" + reqID + "] td:first-child");
            if ( checkbox.length > 0 ) 
                checkbox.attr( "title", "Completed by " + getLoginName(leaderID) + " on " + formatDate( timestamp ) );

            updateDirtyFlags( youthID, reqID );
        }

    return nChanged;
};

function clearRequirementComplete( youthID, reqID, strNotes, addToQueue, syncAfter, bPersist )
{
    // first, persist the change
    var badgeID = deriveBadgeID( reqID );

    if ( g_setMarkedRequirements[youthID] && g_setMarkedRequirements[youthID][reqID] !== undefined )
        if ( addToQueue )
            queueTransaction( youthID, reqID, STATE_CLEAR, FLAG_COMPLETION, strNotes, syncAfter, getServerTimestamp() );

    var nChanged = deleteFromJsonTable( 'db-completed-requirements', g_setMarkedRequirements, youthID, reqID, bPersist )

        // secondly, if the change applies to the current youth, then we have the maps we use to keep
        // track of the current youth's progress
        if ( g_setYouthScorecarded[youthID] !== undefined )   // have I scorecarded this youth?
        {
            var jsonCompletion = { complete: 0, total: getRequirementWeight( reqID ) };  // we know what the value is
            // make it incomplete, and update the map
            if ( isAutoRequirement( reqID ) )
                jsonCompletion = evaluateExpression( youthID, g_mapAutocompletion[reqID], badgeID );       // force re-calculation

            updateRequirementCompletionMap( youthID, reqID, jsonCompletion );
            $("tr[reqid=" + reqID + "] td:first-child").removeAttr( "title" );

            updateDirtyFlags( youthID, reqID );
        }

    return nChanged;
};

function addTally( youthID, reqID, nCount, strNotes, addToQueue, syncAfter, timestamp, loginID, bPersist )
{
    // first, this is an explicit completion (as opposed to an implicit auto completion) so we want to persist the change
    var badgeID = deriveBadgeID( reqID );

    if ( g_setTallies[youthID] !== undefined )
        delete g_setTallies[youthID][reqID];        // clear out the old record

    if ( addToQueue )
        queueTally( youthID, reqID, nCount, strNotes, syncAfter, timestamp );

    var nChanged = addToJsonTable( 'db-tallies', g_setTallies, youthID, reqID, { count: nCount, when: timestamp, by: loginID, notes: strNotes }, bPersist );

    // secondly, if the change applies to the current youth, then we have the maps we use to keep
    // track of the current youth's progress
    if ( g_setYouthScorecarded[youthID] !== undefined )   // have I scorecarded this youth?
    {
        var nWeight = getRequirementWeight( reqID );
        jsonCompletion = { complete: nWeight, total: nWeight };

        if ( isAutoRequirement( reqID ) )
            jsonCompletion = evaluateExpression( youthID, g_mapAutocompletion[reqID], badgeID );       // force re-calculation

        updateRequirementCompletionMap( youthID, reqID, jsonCompletion );

        var checkbox = $("tr[reqid=" + reqID + "] td:first-child");
        if ( checkbox.length > 0 ) 
            checkbox.attr( "title", getTallyNotes( youthID, reqID ) );

        updateDirtyFlags( youthID, reqID );
    }

    return nChanged;
};

function scale( jsonCompletion, nWeight )
{
    return ~~( ( nWeight * jsonCompletion.complete ) / jsonCompletion.total );
};

function asPercentage( jsonCompletion )
{
    return scale( jsonCompletion, 100 );
};

function queueOuting( outing, syncAfter )
{
    if ( g_mode == "handbook" )
        return;     // this doesn't apply to us

    var listOutings = getJsonTableArray( 'db-queued-outings' );

    // delete any existing updates for this record
    for ( var i = listOutings.length-1; i >= 0; i-- )
    {
        if ( listOutings[i].id == outing.id )
        {
            listOutings.splice( i, 1 );
            break;
        }
    }

    if ( outing.id > 0 || outing.date > 0 )        // don't bother queueing deletes of unsynced events
        listOutings.push( outing );

    setLocalStorage( 'db-queued-outings', listOutings.length == 0 ? null : JSON.stringify( listOutings ) );

    if ( syncAfter )
    {
        // basically, do this immediately then schedule a second sync so we get the new outing ID immediately
        scheduleSync( "outing sync'd", 200,
            function() { console_log( "outing 1st sync'd" ); scheduleSync( "outing 2nd sync'd", 200 ); } 
        );
    }
};

function queueTally( youthID, requirementID, nCount, strNotes, syncAfter, timestmp )
{
    if ( g_mode == "handbook" )
        return;     // this doesn't apply to us

    var listTallies = getJsonTableArray( 'db-queued-tallies' );

    // delete any existing updates for this record
    for ( var i = listTallies.length-1; i >= 0; i-- )
    {
        if ( listTallies.youthid == youthID && listTallies.requirementid == requirementID )
        {
            listTallies.splice( i, 1 );
            break;
        }
    }

    if ( youthID == -1 )
        openLightBox( { text: "Sorry, I am unclear about who the current " + STR_YOUTH + " is.  Please reselect the current " + STR_YOUTH + ", and retry this action.  And, please report this bug to bugs@dakemi.com", canClose: test } );

    listTallies.push( { 
            sectionid: getSection(),
            youthid: youthID,
            requirementid: requirementID,
            count: nCount,
            notes: strNotes,
            updatedby: getLoginID(),
            timestamp: timestmp
        }
    );

    setLocalStorage( 'db-queued-tallies', listTallies.length == 0 ? null : JSON.stringify( listTallies ) );

    if ( syncAfter )
        scheduleSync( "sync'd", POST_UPDATE_INTERVAL );
};

function queueTransaction( youthID, requirementID, eState, eFlag, strNotes, syncAfter, timestmp )
{
    if ( g_mode == "handbook" )
        return;     // this doesn't apply to us

    var listTransactions = getJsonTableArray( 'db-queued-transactions' );

    // delete any existing updates for this record
    for ( var i = listTransactions.length-1; i >= 0; i-- )
    {
        if ( listTransactions.youthid == youthID && listTransactions.requirementid == requirementID )
        {
            listTransactions.splice( i, 1 );
            break;
        }
    }

    if ( youthID == -1 )
        openLightBox( { text: "Sorry, I am unclear about who the current " + STR_YOUTH + " is.  Please reselect the current " + STR_YOUTH + ", and retry this action.  And, please report this bug to bugs@dakemi.com", canClose: test } );

    listTransactions.push( { 
            sectionid: getSection(),
            youthid: youthID,
            requirementid: requirementID,
            state: eState,
            flag: eFlag,
            notes: strNotes,
            updatedby: getLoginID(),
            timestamp: timestmp
        }
    );

    setLocalStorage( 'db-queued-transactions', listTransactions.length == 0 ? null : JSON.stringify( listTransactions ) );

    if ( syncAfter )
        scheduleSync( "sync'd", POST_UPDATE_INTERVAL );
};

function size( map )
{
    if ( map === undefined )
        return 0;

    var nElements = 0;
    for ( var key in map )
        if ( map.hasOwnProperty( key ) )
            nElements++;

    return nElements;
};

function formatTimestamp( timestamp )
{
    var now = new Date(parseInt(timestamp));

    var nMonth = now.getMonth() + 1;
    var nHours = now.getHours();
    var nDay = now.getDate();
    var nMinutes = now.getMinutes();
    var nSeconds = now.getSeconds();

    return now.getFullYear() + "-" + (nMonth<=9?"0":"") + nMonth + "-" + (nDay<=9?"0":"") + nDay + " " + (nHours<=9?"0":"") + nHours + ":" + (nMinutes<=9?"0":"") + nMinutes + ":" + (nSeconds<=9?"0":"") + nSeconds;
};

function formatTimestampRelative( timestamp )
{
    var now = new Date();
    var nNowEpoch = now.getTime();
    var nNowDay  = now.getDate()

    now.setTime( timestamp );

    var strDate = "unknown";

    // within the last 6 days?
    if ( nNowEpoch < timestamp + 6*24*3600000 )
    {
        var nDay = now.getDate();
        var nHours = now.getHours();
        var ampm = "am";
        if ( nHours >= 12 )
        {
            ampm = "pm";
            nHours -= 12;
        }

        if ( nHours == 0 )
            nHours = 12;

        var nMinutes = now.getMinutes();

        if ( nNowDay == nDay )
            strDate = nHours + ":" + (nMinutes<=9?"0":"") + nMinutes + " " + ampm;
        else if ( nNowDay == nDay + 1 )
            strDate = "Yesterday, " + nHours + ":" + (nMinutes<=9?"0":"") + nMinutes + " " + ampm;
        else
            strDate = DOW_LONG[now.getDay()] + ", " + nHours + ":" + (nMinutes<=9?"0":"") + nMinutes + " " + ampm;
    }
    else
    {
        // within the last 364 days?
        if ( nNowEpoch < timestamp + 364*24*3600000 )
            strDate = MONTHS[now.getMonth()] + " " + now.getDate();
        else
        {
            var nMonth = now.getMonth() + 1;
            var nDay = now.getDate();
            strDate = now.getFullYear() + "-" + (nMonth<=9?"0":"") + nMonth + "-" + (nDay<=9?"0":"") + nDay + " ";
        }
    }

    return strDate;
};

function addAwarded( youthID, badgeID, strNotes, timestamp, leaderID, bPersist )
{
    var nChanged = addToJsonTable( 'db-awarded', g_setAwardedBadges, youthID, badgeID, { when: timestamp, by: leaderID, notes: strNotes }, bPersist );

    if ( bPersist )
    {
        if ( youthID == g_currentUser )
        {
            updateCategory( getBadgeCategory(badgeID) );
            if ( badgeID == g_currentBadgeID )
                setBadgeStatusText( getCurrentDetailsPage(), youthID, badgeID );
        }

        if ( getCurrentPage() == 'report' && g_report == 'awarded' )
            doReportRefresh();

        var uniformID = getYouthUniform( youthID );
        var inventory = g_dbTables['Inventory'][uniformID][badgeID];
        if ( inventory !== undefined && inventory.stock > 0 )
            g_dbTables['Inventory'][uniformID][badgeID].stock--;
    }

    return nChanged;
};

function getYouthUniform( youthID )
{
    if ( g_dbTables == null )
        return g_listUniformIDs[0];

    var youth = g_dbTables['Youth'][youthID];
    if ( youth === undefined || youth.uniformid === undefined )
        return g_listUniformIDs[0];

    return youth.uniformid;
}

function clearAwarded( youthID, badgeID, strNotes, bPersist )
{
    var nChanged = deleteFromJsonTable( 'db-awarded', g_setAwardedBadges, youthID, badgeID, bPersist )

        if ( bPersist )
        {
            if ( youthID == g_currentUser )
            {
                updateCategory( getBadgeCategory(badgeID) );
                if ( badgeID == g_currentBadgeID )
                    setBadgeStatusText( getCurrentDetailsPage(), youthID, badgeID );
            }

            if ( getCurrentPage() == 'report' && g_report == 'awarded' )
                doReportRefresh();

            var uniformID = getYouthUniform( youthID );
            var inventory = g_dbTables['Inventory'][uniformID][badgeID];
            if ( inventory !== undefined )
                g_dbTables['Inventory'][uniformID][badgeID].stock++;
        }

    return nChanged;
};

function addReady( youthID, reqID, strNotes, timestamp, updatedByID, bPersist )
{
    var nChanged = addToJsonTable( 'db-ready', g_setReadyRequirements, youthID, reqID, { when: timestamp, by: updatedByID, notes: strNotes }, bPersist );

    if ( bPersist )
    {
        if ( youthID == g_currentUser )
            updateReadyCounts();

        if ( getCurrentPage() == 'report' && g_report == 'ready' )
            doReportRefresh();
    }

    return nChanged;
};

function clearReady( youthID, reqID, strNotes, bPersist )
{
    var nChanged = deleteFromJsonTable( 'db-ready', g_setReadyRequirements, youthID, reqID, bPersist )

        if ( bPersist )
        {
            if ( youthID == g_currentUser )
                updateReadyCounts();

            if ( getCurrentPage() == 'report' && g_report == 'ready' )
                doReportRefresh();
        }

    return nChanged;
};

function addToJsonTable( strTableName, setTable, youthID, id, newRow, bPersist )
{
    // don't add entries that already exist
    if ( setTable[youthID] === undefined  || ! setTable[youthID] || setTable[youthID][id] === undefined )
    {
        if ( setTable[youthID] === undefined || ! setTable[youthID] )
            setTable[youthID] = {};

        setTable[youthID][id] = newRow;

        if ( bPersist )
            setLocalStorage( strTableName, JSON.stringify( setTable ) );

        return 1;
    }

    return 0;
};

function deleteFromJsonTable( strTableName, setTable, youthID, id, bPersist )
{
    // don't add entries that already exist
    if ( setTable[youthID] !== undefined && setTable[youthID] && setTable[youthID][id] !== undefined )
    {
        delete setTable[youthID][id];
        if ( size( setTable[youthID] ) == 0 )
            delete setTable[youthID];

        if ( bPersist )
            setLocalStorage( strTableName, JSON.stringify( setTable ) );

        return 1;
    }

    return 0;
};

function addBookmark( youthID, reqID, strNotes, timestamp, updatedByID, bPersist )
{
    var nChanged = addToJsonTable( 'db-bookmarks', g_setBookmarkedRequirements, youthID, reqID, { when: timestamp, by: updatedByID, notes: strNotes }, bPersist );

    if ( bPersist )
        if ( youthID == g_currentUser )
            updateBookmarkCounts();

    return nChanged;
};

function clearBookmark( youthID, reqID, strNotes, bPersist )
{
    var nChanged = deleteFromJsonTable( 'db-bookmarks', g_setBookmarkedRequirements, youthID, reqID, bPersist )

        if ( bPersist )
            if ( youthID == g_currentUser )
                updateBookmarkCounts();

    return nChanged;
};

/**
 * This is the main function in which we read in the badge requirements and establish
 * the completion dependency
 */
function buildBadgeMap()
{
    buildBadgeSelector( g_dbTables['MetaData'] );
    updateOtherDisplay();

    g_mapBadgeRequirementWeights = {};
    g_setLinkedReqs = {};
    g_mapLinksToReqs = {};
    g_mapAutocompletion = {};

    for ( var category in g_categories )
        g_setUpdateableCategories[category] = 1;

    // make sure ALL categories are updated
    for ( var badgeID in g_dbTables['MetaData'] )
        g_setUpdateableCategories[getBadgeCategory(badgeID)] = 1;

    // all all custom badges to the invariants list
    for ( var badgeID in g_dbTables['MetaData'] )
    {
        var metadata = g_dbTables['MetaData'][badgeID];
        // don't offer variants for specialty or custom badges (not including ScoutCubVenSys
        if ( $.inArray( metadata.catalogid, g_listCatalogIDs ) != -1 || metadata.sectionid != 0 )
            MAP_INVENTORY_INVARIANTS[badgeID] = 1;
    }

    for ( var reqID in g_dbTables['Requirements'] )
    {
        var row = g_dbTables['Requirements'][reqID];

        if ( row.autolinks !== undefined && row.autolinks )
        {
            g_setLinkedReqs[reqID] = row;
            var listKeys = row.autolinks.split( "," );
            for ( var iKey = 0; iKey < listKeys.length; iKey++ )
            {
                var key = trim( listKeys[iKey] );
                if ( g_mapLinksToReqs[key] === undefined )
                    g_mapLinksToReqs[key] = {};

                g_mapLinksToReqs[key][reqID] = 1;
            }
        }

        g_mapBadgeRequirementWeights[reqID] = parseInt( row.weight ); // record this relationship for easy access

        // build the inverse look up map that binds "pioneer" + "B4" to "pioneer.B1".
        if ( g_mapBadgeRequirementWeights[reqID] > 0 )
        {
            var badgeID = deriveBadgeID( reqID );
            if ( g_mapBadgeRequirementIDs[badgeID] === undefined )
                g_mapBadgeRequirementIDs[badgeID] = {};

            g_mapBadgeRequirementIDs[badgeID][row.requirement] = reqID;
        }

        // take the opportunity to also attach listeners to any any autocompletion requirements
        if ( row.autocompletion )
        {
            g_mapAutocompletion[reqID] = row.autocompletion;     // record this relationship for easy access

            // get a list of all the constituent elements in this expression...
            // we don't care about the actual expression, just the elements contained within
            var elements = row.autocompletion.replace( /{\d+}/g, "" ).replace( /[()]/g, "" ).split( /[&|]/ );

            // parse the autocompletion elements, and record the resulting dependencies
            for ( var i = 0; i < elements.length; i++ )
            {
                if ( elements[i].match( /category-complete:(\w+)-(\d+)/ ) )
                    g_setCategoryDependencies[reqID] = 1;              // record that when a badge is (un)marked as complete, this autocompletion needs to be re-run

                else if ( elements[i].match( /complete:(\w+)/ ) )
                {
                    var otherBadgeID = RegExp.$1;
                    if ( ! g_mapBadgeListeners[otherBadgeID] )
                        g_mapBadgeListeners[otherBadgeID] = {};
                    g_mapBadgeListeners[otherBadgeID][reqID] = 1;    // record that when an other badge/award is (un)marked as complete, this autocompletion needs to be re-run

                    var otherReqID = deriveParentID( reqID );
                    if ( otherReqID != reqID )
                        g_mapBadgeListeners[otherBadgeID][otherReqID] = 1;    // record that when an other badge/award is (un)marked as complete, the parent autocompletion needs to be re-run
                }
                else if ( elements[i].match( /requirement:([^.]+\..+)/ ) )
                {
                    var otherReqID = RegExp.$1;
                    if ( ! g_mapRequirementListeners[otherReqID] )
                        g_mapRequirementListeners[otherReqID] = {};
                    g_mapRequirementListeners[otherReqID][reqID] = 1;    // record that when an other requirement is (un)marked as complete, this autocompletion needs to be re-run
                }
                else if ( elements[i].match( /badgecount:(\d+)[-](\d+)/ ) )
                {
                    g_setCategoryDependencies[reqID] = 1;              // record that when a badge is (un)marked as complete, this autocompletion needs to be re-run
                }
            }
        }
    }

    if ( isAppleMobileDevice() && g_noJQT )
        doLogout( false );
    else
        setMode( false );

    if ( ! isOnline() )
    {
        readYouth( function() { /* nop */ } );
        updateLoginLists( g_dbTables, function() { /* nop */ } );

        initializationComplete();
    }

    else
    {
        g_cachedData = g_dbTables;

        // now proceed with the loading of all the youth from the cached data
        ajaxGetYouth( function() { ajaxGetLogins( function() {
            // see if there any update that need applying
            ajaxSyncTransactions( true, 
                function() { 
                    closeLightBox();
                    initializationComplete();
                    showHomeScreenReminder();
                }
            );
        } ) } );
    }
};

function doResetReportSelection()
{
    $('#table-report-badge-selector').val('');        // set the selection to the first (empty) value
    $('#table-report-event-selector').val('');        // set the selection to the first (empty) value
    $('#table-report-attendance-selector').val('');        // set the selection to the first (empty) value
    // $('#table-report-attendance-year-selector').val('');        // set the selection to the first value
};

function editTableReport()
{
    setInPlaceEdit( true, false );          // set this flag
    buildTableReport( "badge" );    // re-generate the table with all the editing stuff in place
};

function editTableAttendanceReport()
{
    setInPlaceEdit( true, false );          // set this flag
    buildTableReport( "attendance" );    // re-generate the table with all the editing stuff in place
};

function showBulkReport()
{
    $('#table-report-badge-selector').val( g_currentBadgeID );
    editTableReport();
};

function doSelectTableReportBadge()
{
    setInPlaceEdit( false, false );

    var badgeID = $('#table-report-badge-selector').val();
    if ( badgeID == '' )
        return;

    buildTableReport( "badge" );
};

function doSelectTableReportEvent()
{
    var labelID = $('#table-report-event-selector').val();
    if ( labelID == '' )
        return;

    buildTableReport( "event" );
};

function doSelectTableReportAttendance()
{
    setInPlaceEdit( false, false );

    var labelID = $('#table-report-attendance-selector').val();
    if ( labelID == '' )
        return;

    if ( labelID.match( /^combo:/ ) )
    {
        if ( labelID == "combo:all" )
            labelID = -1;
        else
            labelID = -2;
    }

    g_iAttendanceLabel = labelID;
    g_isAttendanceContextual = false;

    buildTableReport( "attendance" );
};

function doSelectTableReportAttendanceYear()
{
    g_nAttendanceYear = $('#table-report-attendance-year-selector').val();

    buildTableReport( "attendance" );
};

function buildBadgeSelector( mapMetaData )
{
    var htmlSelector = "<option value='' selected><b>--- Select ---<b></option>";

    var badgePicker = $( '#event-badges-picker ul' );
    badgePicker.empty();

    for ( var iType = 0; iType < BADGE_SELECTOR_TYPES.length; iType++ )
    {
        var type = BADGE_SELECTOR_TYPES[iType];
        if ( type == "Other" )
            continue;

        var setBadges = {};

        for ( var badgeID in g_dbTables['MetaData'] )
        {
            var metadata = mapMetaData[badgeID];
            if ( metadata.type == type )
                setBadges[badgeID] = 1;
        }

        // todo: is there a better way to test for this (to prevent the Combo divider from being added)?
        if ( type != "Pseudo" )
            badgePicker.append( "<li class='divider'>" + BADGE_SELECTOR_TYPE_LABELS[iType] + "</li>" );

        var htmlSelectorGuts = "";
        var listBadges = sortSelectorBadges( setBadges, type );  // this does section-specific sorting

        for ( var iBadge = 0; iBadge < listBadges.length; iBadge++ )
        {
            var badgeID = listBadges[iBadge];
            var strName = "";
            var metadata = mapMetaData[badgeID];
            if ( metadata == null ) {
                // SCOUT/VENTURER specific
                if ( listBadges[iBadge] == "activity:voyageur" )
                    strName = "Voyageur Level";
                else if ( listBadges[iBadge] == "activity:pathfinder" )
                    strName = "Pathfinder Level";
                else if ( listBadges[iBadge] == "activity:pioneer" )
                    strName = "Pioneer Target";
                else if ( listBadges[iBadge] == "activity:explorer" )
                    strName = "Explorer Target";
                else if ( listBadges[iBadge] == "activity:adventurer" )
                    strName = "Adventurer Target";
                else if ( listBadges[iBadge] == "activity:venturer" )
                    strName = "Venturer Level";
                else if ( listBadges[iBadge] == "activity:queensventurer" )
                    strName = "Queen's Venturer Level";
                else if ( listBadges[iBadge] == "activity:youinguiding" )
                    strName = "You in Guiding Challenges";
                else if ( listBadges[iBadge] == "activity:youandothers" )
                    strName = "You and Others Challenges";
                else if ( listBadges[iBadge] == "activity:discoveringyou" )
                    strName = "Discovering You Challenges";
                else if ( listBadges[iBadge] == "activity:beyondyou" )
                    strName = "Beyond You Challenges";
            }
            else
                strName = metadata.name;

            htmlSelectorGuts += '<option value="' + badgeID + '">' + htmlEncode( strName ) + '</option>';

            // badge picker doesn't support pseudo badges
            if ( metadata != null )
                badgePicker.append( "<li class='checker' badgeid='" + badgeID + "'><a href='javascript:void(0)' onclick='g_hasUnsavedEventEdits=true;toggleBadgeSelection( \"" + badgeID + "\" );'><img glyph src='" + BIMG + "/images/blank.gif' style='border:none;'/>" + htmlEncode( strName ) + "</a></li>" );
        }

        htmlSelector += '<optgroup label="' + BADGE_SELECTOR_TYPE_LABELS[iType] + '">' +  htmlSelectorGuts + '</optgroup>';
    }

    $( "#table-report-badge-selector" ).html( htmlSelector );
};

function buildEventReportSelector()
{
    var EVENT_SELECTOR_TYPES = [ "Pseudo", "Individual"];
    var EVENT_SELECTOR_TYPE_LABELS = [ "Event Combos", "Individual"];

    var htmlSelector = "<option value='' selected><b>--- Select ---<b></option>";

    var htmlSelectorGuts = "";
    htmlSelectorGuts += "<option value='combo:all'>All Events</option>";
    htmlSelectorGuts += "<option value='combo:outings'>Outings</option>";
    htmlSelector += "<optgroup label='Label Combos'>" +  htmlSelectorGuts + "</optgroup>";

    htmlSelectorGuts = "";
    for ( var iLabel = 0; iLabel < g_listSortedLabelIDs.length; iLabel++ )
    {
        var label = g_dbTables['Labels'][g_listSortedLabelIDs[iLabel]];
        if ( label.id != g_reminderID )
            htmlSelectorGuts += "<option value='" + label.id + "'>" + htmlEncode( label.name ) + "</option>";
    }

    htmlSelector += "<optgroup label='By Label'>" +  htmlSelectorGuts + "</optgroup>";

    $( "#table-report-event-selector" ).html( htmlSelector );
    $( "#table-report-attendance-selector" ).html( htmlSelector );
};

function validateBadgeContent()
{
    var bad = true;

    if ( size( g_dbTables['Labels'] ) > 0 )
        for ( var labelID in g_dbTables['Labels'] )
            if ( labelID != -1 )
                bad = false;

    if ( bad )
    {
        console_log( "missing labels" );
        return false;
    }

    bad = true;

    if ( size( g_dbTables['Attributes'] ) > 0 )
        for ( var attributeID in g_dbTables['Attributes'] )
            if ( attributeID != -1 )
                bad = false;

    if ( bad && g_hasAttributes )
    {
        console_log( "missing attributes" );
        return false;
    }

    // do account specific checks
    if ( getLoginID() )
    {
        if ( size( g_dbTables['Patrols'] ) == 0 )
        {
            console_log( "missing patrols" );
            return false;
        }

        if ( size( g_dbTables['Youth'] ) == 0 )
        {
            console_log( "missing youth" );
            return false;
        }

        if ( size( g_dbTables['Logins'] ) == 0 )
        {
            console_log( "missing logins" );
            return false;
        }

        if ( size( g_dbTables['Access'] ) == 0 )
        {
            console_log( "missing access" );
            return false;
        }
    }

    for ( var badgeID in g_dbTables['MetaData'] )
    {
        var metadata = g_dbTables['MetaData'][badgeID];
        if ( metadata === undefined )
            continue;

        if ( metadata.type != "Placeholder" && getBadgeCompletionRequirements( metadata.id ) === undefined )
        {
            console_log( "no requirements for badge '" + metadata.id + "'" );
            return false;
        }
    }

    return true;
};

function compareVersions( versionLocal_Schema, data )
{
    if ( ! isOnline() || data == null )
    {
        readFromDbTables();
        buildBadgeMap();
    }

    else if ( versionLocal_Schema > 0 && g_versionRemote_Schema > versionLocal_Schema )
    {
        console_log( "db schema needs rebuilding (version " + versionLocal_Schema + " vs " + g_versionRemote_Schema + ")" );

        // this will reforce a get of everything 
        doResetDatabase( false );
    }

    else
    {
        persistProperty( "schema-version", g_versionRemote_Schema );
        var wasSeaScouts = getLocalStorage( "sea-scouts" ) == "true";
        var allowSeaScouts = false;
        var needsReplacement = false;
        g_listCatalogIDs = new Array();
        g_listExtraInventory = new Array();

        for ( var key in data['Properties'] )
        {
            var property = data['Properties'][key];
            if ( property.name == "MAX_YOUTH" )
            {
                try {
                    g_nMaxYouth = parseInt( property.value );
                } catch ( err ) {
                    console_warn( "problems parsing '" + property.name + "' = '" + property.value + "'" );
                }
            }
            else if ( property.name == "EXPIRY" )
            {
                setSectionExpiry( property.value );
                updateGroupDisplays( getSectionGroup(), getSectionSubGroup() );
            }
            else if ( property.name == "SEA_SCOUTS" )
                persistProperty( 'sea-scouts', property.value == 1 ? "true" : "false" );
            else if ( property.name == "HAS_ATTRIBUTES" )
                g_hasAttributes = property.value == 1;
            else if ( property.name == "CATALOGS" )
            {
                g_listCatalogIDs = property.value.split(',');
                for ( var i = 0; i < g_listCatalogIDs.length; i++ )
                    g_listCatalogIDs[i] = parseInt( g_listCatalogIDs[i] );
            }
            else if ( property.name == "EXTRA_INVENTORY" )
                g_listExtraInventory.push( property.value );        // can't process these until we read the MetaData table
            else if ( property.name == "BADGES_TIMESTAMP" )
            {
                var timestampOld = getLocalStorage( "badges-timestamp" );
                if ( timestampOld == null )
                    timestampOld = 0;

                var timestampNew = parseInt( property.value );
                if ( timestampNew > timestampOld )
                {
                    setLocalStorage( "badges-timestamp", timestampNew );
                    needsReplacement = true
                }
            }
            else if ( property.name == "SEA_VARIANT" )
                allowSeaScouts = property.value == 1;
            else if ( property.name == 'SEASON_START' )
            {
                if ( property.value.match( /(\d+)-(\d+)/ ) )
                {
                    g_nSeasonMonth = RegExp.$1;
                    g_nSeasonDay = RegExp.$2;
                    persistProperty( 'seasonstart-month', g_nSeasonMonth );
                    persistProperty( 'seasonstart-day', g_nSeasonDay );
                }
            }
        }


        $('#pref-seascouts').toggle( allowSeaScouts );      // does this section have specialty badges?
        var isSeaScouts = getLocalStorage( "sea-scouts" ) == "true";
        toggleSwitch( "#pref-seascouts", isSeaScouts )

        populateDbTables( data ); // update our persisted DB tables with the latest info, if any

        // if there was a change in the type, then we have to reload to get all the vocabulary correct
        if ( isSeaScouts != wasSeaScouts )
            window.location.reload( true );

        readFromDbTables();

        $('#pref-extrabadges').toggle( g_listCatalogIDs.length > 0 );       // does this section have a "sea" variant
        var isSubscribed = false;
        if ( g_listCatalogIDs.length > 0 )
        {
            for ( var badgeID in g_dbTables['MetaData'] )
            {
                var catalogID = g_dbTables['MetaData'][badgeID].catalogid;
                if ( $.inArray( catalogID, g_listCatalogIDs ) != -1 )
                {
                   isSubscribed = true;
                   break;
               }
            }
        }
        toggleSwitch( "#pref-extrabadges", ! isSubscribed )

        buildBadgeMap();

        if ( data['Metadata'] !== undefined || data['Requirements'] !== undefined )
        {
            console_log( "replaced badge data" );
            clearCachedImages();
        }
        else if ( needsReplacement || ! validateBadgeContent() )
        {
            if ( needsReplacement )
                console_warn( "db content is out-of-date" );
            else
                console_warn( "db content is invalid" );

            persistProperty( 'last-modified', 0 );  // force reget of metadata/requirements
            ajaxGetVersions( 3 );
        }
    }
};

function parseConnectionResponse( data )
{
    persistProperty( 'last-modified', getServerTimestamp() );

    var versionLocal_Schema = getLocalStorage( 'schema-version' );
    if ( versionLocal_Schema == null )
        versionLocal_Schema = 0;

    compareVersions( versionLocal_Schema, data );
};

var jQT = null;
if ( ! g_noJQT )
{
    if ( g_isiPad )
    {
        IMAGE_JQT_STARTUP = "section-images/badges-startup-ipad" + (isLandscape()?"landscape":"portrait") + ".jpg";
        IMAGE_JQT_ICON = 'section-images/badges-icon-72.png';
    }
    else if ( g_isiPhone5 )
    {
        IMAGE_JQT_ICON = 'section-images/badges-icon-114.png';
        IMAGE_JQT_STARTUP = "section-images/badges-startup-retina.jpg";
    }

    jQT = new $.jQTouch({
        //icon: IMAGE_JQT_ICON,
        addGlossToIcon: false,
        //startupScreen: IMAGE_JQT_STARTUP,
        backSelector: '.goback, .cancel',
        slideSelector: 'body > * > ul li a',
        cubeSelector: 'a[data-icon=check], a[data-icon=gear], a[data-icon=delete], a[data-icon=cancel]',
        dissolveSelector: '.sidebar a', 
        touchSelector: '#badge-details-0 *, #badge-details-1 *, #view-event *, .touch',
        statusBar: 'black',
        preloadImages: [
            'section-theme/img/back_button_clicked.png',
            'section-theme/img/back_button.png',
            'section-theme/img/button_clicked.png',
            'section-theme/img/button.png',
            'section-theme/img/chevron_circle.png',
            'section-theme/img/chevron.png',
            'section-theme/img/toolbar.png',
            'theme/img/chevron_clicked.png',
            'theme/img/grayButton.png',
            'theme/img/loading.gif',
            'theme/img/on_off.png',
            'theme/img/rowhead.png',
            'theme/img/toggleOn.png',
            'theme/img/toggle.png',
            'theme/img/whiteButton.png',
            'images/incomplete.gif',
            'images/incomplete-dep.gif',
            'images/incomplete-tally.gif',
            'images/incomplete-dep-tally.gif',
            'images/complete.gif',
            'images/complete-dep.gif',
            'images/complete-tally.gif',
            'images/complete-dep-tally.gif'
        ]
    });
}

function initializeIfReady()
{
    var nStatus = 0;
    if ( window.applicationCache )
        nStatus = window.applicationCache.status;

    if ( nStatus == 3 )
        ;       // we're not going to initialize... there's a download in progress
    else if ( nStatus == 2 )
        window.setTimeout( initializeIfReady, 100 );        // we'll try again shortly
    else
        initialize();       // we're either uncached, noupdate, or updateready
};

function replaceSectionStrings( page, selector )
{
    var body = page.html();

    if ( body == null )
    {
        console_trace( "wowot! page '" + selector + "' is null" );
        return;
    }

    body = body.replace( /\$([_A-Z]+)/g, function( $0, $1 ) { 
        if ( STRING_LOOKUPS[$1] !== undefined )
            return STRING_LOOKUPS[$1];

        console_warn( "page '" + page.attr('id') + "' contains unidentified string reference '" + $1 + "'" );
        return "$" + $1;
    });

    page.html( body );
};

jQuery(window).load( function() {
    initializeDocument();
} );

function initialize()
{
    if ( isOnline() )
        openLightBox( { text: "Connecting..." } );

    ajaxGetVersions( 3 );
};

function doLogin( email, passwd )
{
    clearActive();

    if ( g_mode != "group" )
    {
        if ( isAppleMobileDevice() && g_noJQT )
        {
            openLightBox( { text: "Sorry, this device cannot support a " + STR_SECTION + " Account", canClose: true } );
            return;
        }
    }

    if ( email == null || trim( email ) == "" )
    {
        openLightBox( { text: "Please provide your email address.", canClose: true } );
        return;
    }
    else if ( passwd == null || trim( passwd ) == "" )
    {
        openLightBox( { text: "Please provide your password.", canClose: true } );
        return;
    }

    openLightBox( { text: "Signing in..." } );

    var epw = ncrypt( passwd, g_nonce );
    jQuery.ajax( {
        url: BADGES_AUTHENTICATE,
        data: "uid=" + getLoginID() + "&email=" + email + "&pw=" + epw,
        error: function( request, textStatus, errorThrown ) {
            g_nonce = null;
            console_warn( "Error connecting to Authentication servlet: '" + textStatus + "', " + errorThrown );
            openLightBox( { text: "Error signing in", canClose: true } );
            setOnline( false );
        },
        success: function( data ) 
        {
            setOnline( true );  // by definition, we must be online if we got a response

            //console_log( "login ajax returned: " + JSON.stringify( data ) );

            if ( data == null || data.loginid == null )
            {
                g_nonce = null;
                openLightBox( { text: "Unknown user.  Ask one of your leaders to create a login for this email.", canClose: true } );
            }
            else if ( data.sectionid == null )
            {
                if ( g_nonce == null )
                {
                    g_nonce = data.nonce;
                    doLogin( email, passwd );
                }
                else
                {
                    g_nonce = null;
                    openLightBox( { text: "Login failed for<div style='font-weight:bold;font-size:90%;padding:5px 0;'>" + email + "</div>Check your email and password, and try again.<div style='margin-top:10px;' class='button' onclick='if(confirm(\"Reset password?\")){doResetPassword(\"" + email + "\");}'>Reset Password</div>", canClose: true, size: 'big-wide' } );
                }
            }
            else
            {
                var oldSectionID = getSection();
                setSection( data.sectionid );
                setSectionGroup( data.sectiongroup );
                setSectionSubGroup( data.sectionsubgroup );
                setSectionArea( data.sectionarea );
                setSectionExpiry( data.expiry );
                setSectionTrial( data.istrial );
                setLoginID( data.loginid );
                setMode( false );
                var isResetRequired = setRole( data.role );
                setServerTimestamp( parseInt( data.timestamp ) );

                updateLoginNameDisplays();
                updateGroupDisplays( data.sectiongroup, data.sectionsubgroup );

                setLastLoginTimestamp( data.timestamp ); 
                g_cachedData = null;

                // is this a different sectionID?  Scrub the whole freakin' DB
                if ( isResetRequired || ( oldSectionID != null && oldSectionID != getSection() ) )
                {
                    if ( isResetRequired )
                        console_log( "first leader login... scrubbing database" );
                    else
                        console_log( "different section... scrubbing database" );

                    persistProperty( 'last-modified', 0 );  // force reget of metadata/requirements
                    persistProperty( 'sea-scouts', null );       // clear this
                    doResetDatabase( false );
                }
                else
                {
                    ajaxGetYouth( function() { ajaxGetLogins(
                        function() {
                            if ( g_mode == "group" && getLoginID() && getRole() == "p" && g_mapAccessByLogin[getLoginID()] === undefined )
                            {
                                doLogout( false );
                                openLightBox( { text: "This login does not access any " + STR_YOUTHS, canClose: true } );
                            }
                            else
                            {
                                updateLoginDisplay();
                                resetUpPoint();
                            }
                            scheduleSync( "initial login sync", 1000 );
                        });
                    });
                }
            }
        }
    });
};

function syncCallback()
{
    updateSyncCheckDisplay();

    if ( g_setYouthScorecarded[g_currentUser] === undefined )
        window.setTimeout( function() { scorecard( g_currentUser, true ); }, 100 );

    if ( g_fnWorkerCallback )
    {
        g_fnWorkerCallback();
        g_fnWorkerCallback = null;
    }
    else
        closeLightBox( 1000 );
};

function cancelSync()
{
    if ( g_fnWorkerCallback )
        console_log( "cancelling extant callback '" + g_fnWorkerCallback + "'" );

    g_fnWorkerCallback = null;
};

function syncProgress( nProgress, nTotal )
{
    var nPercent = ~~((100*nProgress)/nTotal);

    $('table.progress td:first-child').css( 'width', nPercent + "%" );
    $('table.progress + div > span').text( nProgress );

    // are we finished?  (and did we do anything?)
    if ( nTotal == 0 )
    {
        //console_log( "nothing sync'd" );
        closeLightBox();
    }
    else if ( nProgress == nTotal )
    {
        // write back the cached values for FireFox (sigh...)
        setLocalStorage( 'db-completed-requirements', JSON.stringify( g_setMarkedRequirements ) );

        // iterate all the scorecarded youths, and make sure they're up-to-date
        for ( var youthID in g_setYouthScorecarded )
            processDirtyFlags( youthID, false, true, true );

        if ( ! hasUpPoint( "view-event" ) )
            populateAlphabetic( g_filter, false, false );
    }
};

function doLogout( bFromGUI )
{
    clearActive();

    if ( ! isOnline() )
    {
        if ( bFromGUI )
            openLightBox( { text: "You cannot sign out while off-line", canClose: true } );
        return;
    }

    setLoginID( null );
    setCurrentYouth( "0" );
    setMode( false );

    g_nonce = null;

    $('#input-email').val( "" );
    $('#input-password input').val( "" );

    updateLoginDisplay();
    setCurrentCouncil( -1 );

    workerPause();
};

function showSidebarName( bShow )
{
    var isVisible = $('#snapshot select').length > 0;
    if ( bShow && ! isVisible )
    {
        $('#snapshot').prepend( $('#snapshot-name').detach() );
    }
    else if ( ! bShow && isVisible )
    {
        // put the select in the hidden UL that follows '#snapshot'
        $('#snapshot + ul').prepend( $('#snapshot-name').detach() );
    }
};

function updateLoginDisplay()
{
    if ( g_isEmbedded || g_mode == "handbook" )
    {
        $('form[name=form1], form[name=form2]').hide();
        $('.role-i').hide();
        $('.role-i-v').hide();
        $('.role-i-vn-v').hide();
        $('.role-v').hide();
        $('.role-vn').hide();
        $('.role-vn-v').hide();
        $('.role-p').hide();
        $('.loggedin').hide();
        $('.notloggedin').show();
        $('#current-youth-container').hide();

        $('[action=floaty-complete]').removeAttr( 'disabled' );

        showSidebarName( false );
    }

    // logged in
    else 
    {
        updateLoginNameDisplays();

        $('form[name=form1], form[name=form2]').show();

        $('.notloggedin').hide();
        $('.loggedin').show();

        $('#current-youth-container').toggle( ! hasSidebar() );

        if ( getRole() == "i" ) 
            $('#troop-details a').html( "Upgrade to " + STR_SECTION + " Account" );
        else
            $('#troop-details a').html( STR_SECTION + " Details / Payment" );

        if ( getRole() == "v" || getRole() == "i" )
        {
            $('.role-p').hide();
            $('.role-vn').hide();
            $('.role-i').toggle( getRole() == "i" );
            $('.role-v').toggle( getRole() == "v" );
            $('.role-vn-v').toggle( getRole() == "v" );
            $('.role-i-vn-v').show();
            $('.role-i-v').show();

            $('[action=floaty-complete]').removeAttr( 'disabled' );
        }
        else if ( getRole() == "n" )       // non-badgemaster
        {
            $('.role-p').hide();
            $('.role-v').hide();
            $('.role-i-v').hide();
            $('.role-i').hide();
            $('.role-vn').show();
            $('.role-vn-v').show();
            $('.role-i-vn-v').show();

            $('[action=floaty-complete]').removeAttr( 'disabled' );
        }
        else
        {
            $('.role-i-v').hide();
            $('.role-i').hide();
            $('.role-v').hide();
            $('.role-vn').hide();
            $('.role-vn-v').hide();
            $('.role-i-vn-v').hide();
            $('.role-p').show();

            $('[action=floaty-complete]').attr( 'disabled', true );
        }

        // parents can't see reports
        $('.link-reports').toggle( ! hasSidebar() && getLoginID() != null && getRole() != "p" );

        showSidebarName( true );

        updateGroupDisplays( getSectionGroup(), getSectionSubGroup() );
    }

    updateQuickLinksVisibility();
};

function isLandscape()
{
    if ( window.orientation === undefined )
        return true;

    return Math.abs( window.orientation ) == 90;
};

function initAbbreviations()
{
    // SCOUT SPECIFIC
    var abbreviationWords = {};
    abbreviationWords['Year Round Camper'] = 'Y.R. Camper';               // do not re-order  abbrev-0
    abbreviationWords['Voyageur -'] = 'V.';                 // do not re-order  abbrev-1
    abbreviationWords['Pathfinder -'] = 'P.';               // do not re-order  abbrev-2
    abbreviationWords['Voyageur Scout Award'] = 'Voyageur';       // do not re-order  abbrev-3
    abbreviationWords['Pathfinder Scout Award'] = 'Pathfinder';   // do not re-order  abbrev-4
    abbreviationWords["Chief Scout's Award"] = "Chief Scout's";   // do not re-order  abbrev-5
    abbreviationWords["Pioneer Scout"] = "Pioneer";             // do not re-order  abbrev-6
    abbreviationWords['Challenge Badges'] = 'Badges';             // do not re-order  abbrev-7
    abbreviationWords['Achievement Awards'] = 'Achievements';             // do not re-order  abbrev-8
    abbreviationWords['Venturer Award'] = 'Venturer';             // do not re-order  abbrev-9
    abbreviationWords['Personal Fitness'] = 'Per. Fitness';             // do not re-order  abbrev-10
    abbreviationWords['Personal Interest'] = 'Per. Interest';             // do not re-order  abbrev-11
    abbreviationWords['Social, Cultural and Spiritual'] = 'Soc, Cul & Spir';             // do not re-order  abbrev-12
    abbreviationWords['Lady B-P Challenge'] = 'Lady B-P';             // do not re-order  abbrev-13
    abbreviationWords['Venturer Award'] = 'Venturer';             // do not re-order  abbrev-14
    abbreviationWords['Alphabetic Listing'] = 'Alphabetic';
    abbreviationWords['Challenge Badge'] = 'Badge';
    abbreviationWords['Activity Badge'] = 'Activity';
    abbreviationWords['Pioneer Scout'] = 'Pioneer';
    abbreviationWords['Ready to Test'] = 'Ready';
    abbreviationWords['Completion By Badge'] = 'Completion';

    for ( var s in abbreviationWords )
    {
        g_abbreviations.push( {
            full: s,
            abbreviated: abbreviationWords[s],
            re: new RegExp( "(" + s + ")", "g" )
        } );
    }
};

function tagAbbreviations( strText )
{
    var strTextNew = strText;
    if ( strTextNew === undefined )
        console_trace( "undefined text" );
    for ( var iRE = 0; iRE < g_abbreviations.length; iRE++ )
    {
        var json = g_abbreviations[iRE];
        strTextNew = strTextNew.replace( json.re, "<span class='abbrev-" + iRE + "'>$1</span>" );
    }

    return strTextNew;
};

function updateNextPrevBadgeList( listBadges ) 
{
    var lastID = null;

    g_nextAlphabetic = {};
    g_prevAlphabetic = {};

    for ( var iBadge = 0; iBadge < listBadges.length; iBadge++ )
    {
        var badgeID = listBadges[iBadge];
        if ( lastID )
        {
            g_prevAlphabetic[badgeID] = lastID;
            g_nextAlphabetic[lastID] = badgeID;
        }
        lastID = badgeID;
    }
};

function buildAlphabetic( listBadges, filter, bNavigate )
{
    var showIcons = false;
    var youthID = g_currentUser;   // GUI operates on current user

    updateNextPrevBadgeList( listBadges );

    $('#list-alphabetic').empty();
    for ( var iBadge = 0; iBadge < listBadges.length; iBadge++ )
    {
        var badgeID = listBadges[iBadge];

        var strName = tagAbbreviations( highliteSearch( getBadgeName( badgeID ) ) );

        var strGlyph = "";
        var flag = "";
        var strType = getBadgeType( badgeID );
        var isComplete = isBadgeComplete( youthID, badgeID );
        var isPlaceholder = strType == 'Placeholder';
        var flag = getBadgeStatus( youthID, badgeID );

        if ( flag == "awarded" )
        {
            strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/check.gif'/>";
            flag = "complete";
            showIcons = true;
        }
        else if ( flag == "complete" )
        {
            strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/unawarded.gif'/>";
            showIcons = true;
        }
        else if ( flag == "ready" )
        {
            strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/readybadge.gif'/>";
            showIcons = true;
        }
        else if ( flag == "favourite" )
        {
            strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:4px;' src='" + BIMG + "/images/star.gif'/>";
            showIcons = true;
        }
        else
            strGlyph = "<img class='glyph' style='margin-right:24px;' src='" + BIMG + "/images/blank.gif'/>";

        var li = document.createElement( "li" );
        li.className = "forward";
        li.innerHTML = "<a badgeid='" + badgeID + "' class='" + flag + "' href=\"javascript:void(0);\" onclick=\"saveUpPoint();setCurrentBadge( '" + badgeID + "', true, 'dissolve' );\"><span class='stunted'>" + strGlyph + strName + "</span> <small class='counter'></small></a>";
        $('#list-alphabetic').append( li );

        annotatePercentage( badgeID );
    }

    if ( filter == '' )
    {
        var span = document.createElement('span');
        span.innerHTML = tagAbbreviations( "Alphabetic Listing" );
        $('#alphabetic .name').empty().append( span );
        $('#alphabetic div[data-role=header] a').text( "Home" );
    }
    else if ( filter == 'search' )
    {
        $("#alphabetic .name").text( "Search Results" );
        $('#alphabetic div[data-role=header] a').text( "Home" );
    }
    else if ( filter == 'ready' )
    {
        var span = document.createElement('span');
        span.innerHTML = tagAbbreviations( "Ready to Test" );
        $('#alphabetic .name').empty().append( span );
        $('#alphabetic div[data-role=header] a').text( "Home" );
    }
    else if ( filter == 'favourite' )
    {
        $("#alphabetic .name").text( "Bookmarked" );
        $('#alphabetic div[data-role=header] a').text( "Home" );
    }
    else if ( lastUpPoint().match( /^badge-details-/ ) )
    {
        $("#alphabetic .name").text( filter );
        $('#alphabetic div[data-role=header] a').text( "Award" );
    }
    else
    {
        $("#alphabetic .name").text( filter );
        // GUIDE specific
        var lup = getCurrentPage();
        if ( lup == "youinguiding-category-unit"
                || lup == "youandothers-category-unit"
                || lup == "discoveringyou-category-unit"
                || lup == "beyondyou-category-unit" )
            $('#alphabetic div[data-role=header] a').text( "Area" );
        else if ( lup.match( /badge-details-/ ) )
            $('#alphabetic div[data-role=header] a').text( "Award" );
        else if ( SECTION_TYPE == "troop_au" && filter == "Patrol" )
            $('#alphabetic div[data-role=header] a').text( "Home" );
        else if ( SECTION_TYPE == "colony" || SECTION_TYPE == "colony_uk" )
        {
            $('#alphabetic .name').text( filter=="Staged"?"Stages":(filter=="Activity"?"Activities":"Challenges") );
            $('#alphabetic div[data-role=header] a').text( "Home" );
        }
        else
            $('#alphabetic div[data-role=header] a').text( STR_CATEGORIES );
    }

    $('#list-alphabetic .glyph').toggle( showIcons );
    $('#list-alphabetic-events').hide();
    $('#alphabetic > h1').hide();
    $('#list-noresults').hide();
    $('#list-alphabetic-header').hide();
    $('#list-alphabetic').css( "margin-top", 15 );

    clearActive();

    if ( filter == "favourite" && listBadges.length == 0 )
    {
        $('#list-nofavourites').text( "There are no bookmarked badges or awards." ).show();
        $('#list-alphabetic').hide();
    }
    else if ( filter == "ready" && listBadges.length == 0 )
    {
        $('#list-nofavourites').text( "There are no badges ready to test." ).show();
        $('#list-alphabetic').hide();
    }
    else if ( filter == "search" )
    {
        $('#list-nofavourites').hide();
        $('#alphabetic > h1').show();
        $('#list-alphabetic-header').show();
        $('#list-alphabetic').css( "margin-top", 0 );
        $('#list-alphabetic').toggle( listBadges.length > 0 );
        $('#list-noresults').toggle( listBadges.length == 0 );

        $('#list-alphabetic-events').show( g_mode != "handbook" );

        findOutings( getSearch().toLowerCase() );
    }
    else
    {
        $('#list-nofavourites').hide();
        $('#list-alphabetic').show();
    }

    if ( ! isLandscape() )
        updateAbbreviations( '#alphabetic', true );

    if ( bNavigate )
        addPage( '#alphabetic', g_report == "search" ? 'dissolve' : 'slide' );
};

function buildFoundEvents( mapOutings )
{
    var listOutingIDs = sortOutingsByDate( mapOutings );

    $('#list-alphabetic-events ul').empty();
    for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
    {
        var outingID = listOutingIDs[iOuting];
        var outing = mapOutings[outingID];

        var li = document.createElement( "li" );
        li.className = "forward";

        var htmlOuting = buildEventReportRow( getOutingYouthLabels( outing, g_outingYouthID ), true, true );
        if ( outing.shared !== undefined && ! outing.shared )
            htmlOuting = "<img src='./images/notshared.gif' style='padding-right:5px;width:15px;' title='This event is not shared with " + STR_YOUTHS + "/parents'>" + htmlOuting;
        if ( isOutingUnresolved( outing, true ) )
            htmlOuting = "<img src='./images/questionmark.gif' style='padding-right:5px;width:18px;margin-bottom:-1px;' title='This event needs to be finalized'>" + htmlOuting;

        var annotation = getOutingAnnotation( outing );
        var htmlDate = "<span class=\"subtext\">" + formatOutingDateRange( outing ) + "</span>";

        li.innerHTML = "<a outingid=" + outing.id + " href='javascript:void(0)' onclick='doViewEvent(" + outing.id + ");'><span class='stunted'>" + htmlOuting + highliteSearch( getOutingName( outing ) ) + annotation.text + htmlDate + "</span></a>";
        $('#list-alphabetic-events ul').append( li );
    }

    if ( g_filter == "search" ) 
        g_listEvents = listOutingIDs.reverse();

    $('#list-alphabetic-events ul').toggle( listOutingIDs.length > 0 );
    $('#list-noresults-events').toggle( listOutingIDs.length == 0 );
};

function findOutings( strSearch )
{
    var mapOutings = {};

    if ( g_mode == "handbook" || getLoginID() )
    {
        for ( var key in g_mapOutings )
        {
            var outing = g_mapOutings[key];
            if ( outing === undefined || outing == null )
                continue;       // could be gaps in what looks like an array

            if ( ! isMeetingVisible( outing ) )
                continue;

            if ( outing.displayname.toLowerCase().indexOf( strSearch ) != -1 )
                mapOutings["" + outing.id] = outing;

            else if ( outing.notes != undefined && outing.notes.toLowerCase().indexOf( strSearch ) != -1 )
                mapOutings["" + outing.id] = outing;

            else if ( g_mode == "group" && getRole() != "p" && getRole() != "i" && outing.leadernotes !== undefined && outing.leadernotes.toLowerCase().indexOf( strSearch ) != -1 )
                mapOutings["" + outing.id] = outing;
        }
    }

    buildFoundEvents( mapOutings );
};

function getBadgePercentComplete( youthID, badgeID )
{
    var nPercent = 0;
    if ( g_mapBadgeCompletion[youthID] != undefined && g_mapBadgeCompletion[youthID][badgeID] )
    {
        var jsonCompletion = g_mapBadgeCompletion[youthID][badgeID];
        nPercent = asPercentage( jsonCompletion );
    }
    return nPercent;
};

function annotatePercentage( badgeID )
{
    var youthID = g_currentUser;        // GUI operates on current user
    var nPercent = getBadgePercentComplete( youthID, badgeID );

    var strPercentageComplete = "";
    if ( true || nPercent > 0 && nPercent < 100 )
    {
        strPercentageComplete = (nPercent <= 9 ? "&nbsp;&nbsp;" : "" ) + nPercent + "%";
        strPercentageComplete += "<img width=1 height=18 style='vertical-align:-2px;' src='" + BIMG + "/images/blank.gif'/>";
    }

    var counter = $("a[badgeid=" + badgeID + "] small.counter" );
    counter.html( strPercentageComplete ).attr( "complete", nPercent );

    if ( counter.is( ":visible" ) )
        $("a[badgeid=" + badgeID + "] span.stunted").addClass( "shrink" );
    else
        $("a[badgeid=" + badgeID + "] span.stunted").removeClass( "shrink" );

    return nPercent;
};

function annotateYouthPercentage( youthID, badgeID, nPercent )
{
    var strPercentageComplete = "";
    if ( nPercent > 0 && nPercent < 100 )
    {
        strPercentageComplete = (nPercent <= 9 ? "&nbsp;&nbsp;" : "" ) + nPercent + "%";
        strPercentageComplete += "<img width=1 height=18 style='vertical-align:-2px;' src='" + BIMG + "/images/blank.gif'/>";
    }

    $("#report a[youthid=" + youthID + "] small.counter").html( strPercentageComplete ).attr( "complete", nPercent );

    if ( strPercentageComplete == "" )
        $("#report a[youthid=" + youthID + "] span.stunted").removeClass( "shrink" );
    else
        $("#report a[youthid=" + youthID + "] span.stunted").addClass( "shrink" );
};

function getCurrentPage()
{
    return $(".current").attr( "id" );
};

function restorePage()
{
    return restoreUpPoint();
};

function addPage( selector, animation )
{
    if ( g_navigationBlocker && ! checkEventNavigation( selector ) )
    {
        clearActive();
        return false;
    }

    // don't save the up point if we are going from badge to badge or event to event
    var isSaveable = true;

    if ( getCurrentPage() !== undefined )
    {
        var newPageBase = selector.replace( /(.+)-\d$/, "$1" );
        var oldPageBase = getCurrentPage().replace( /(.+)-\d$/, "$1" );
        if ( newPageBase == "#" + oldPageBase )
            isSaveable = false;
    }
    else
        isSaveable = false;

    if ( isSaveable )
        saveUpPoint();

    gotoPage( selector, animation );

    return true;
};

function addPage2( selector, strBackText )
{
    if ( g_navigationBlocker && ! checkEventNavigation( selector ) )
    {
        clearActive();
        return false;
    }

    setBackText( selector, strBackText );
    addPage( selector );

    return true;
};

function resetUpPoint( selector )
{
    if ( g_navigationBlocker && ! checkEventNavigation( selector ) )
        return false;

    g_listUpPoints = new Array();
    dumpUpPoints( "RESET (" + selector + "): " );
    if ( selector !== undefined && selector )
        return addPage( selector );
    else
        return restorePage();
};

function hasUpPoint( selector )
{
    if ( getCurrentPage() == selector ) 
        return true;

    for ( var i = 0; i < g_listUpPoints.length; i++ )
        if ( g_listUpPoints[i] == selector )
            return true;

    return false;
};

function restoreUpPoint()
{
    var selector = '#startpage';
    if ( g_listUpPoints.length > 0 )
        selector = '#' + g_listUpPoints[g_listUpPoints.length-1];

    if ( g_navigationBlocker && ! checkEventNavigation( selector ) )
    {
        clearActive();
        return false;
    }

    g_listUpPoints.pop();

    dumpUpPoints( "RESTORE:" );
    clearCurrentRequirementHighlighting();

    gotoPage( selector, null, true );   // don't save scrolling

    // Mobile Safari bug with fuzzy text mandates re-creating of some pages
    if ( selector.match( /badge-details-/ ) )
    {
        setCurrentBadge( g_currentBadgeID, false, "slideback" );
    }

    if ( selector == "#report" )
        doReportRefresh();
    else if ( selector == "#table-report" )
        doTableReportRefresh();
    else if ( selector == "#table-event-report" )
        doTableEventReportRefresh();
    else if ( selector == "#reports" )
        doResetReportSelection();

    restoreScroll();

    return true;
};

function forceUpPoints( strListPoints )
{
    g_listUpPoints = strListPoints.split(',');
};

function lastUpPointParent()
{
    if ( g_listUpPoints.length > 1 )
        return g_listUpPoints[g_listUpPoints.length-2];

    return lastUpPoint();
};

function lastUpPoint()
{
    if ( g_listUpPoints.length > 0 )
        return g_listUpPoints[g_listUpPoints.length-1];
    return '#startpage';
};

function saveUpPoint()
{
    var strCurrentPage = getCurrentPage();
    if ( strCurrentPage != lastUpPoint() )
    {
        g_listUpPoints.push( strCurrentPage );
        dumpUpPoints( "SAVED:" );
    }
};

function dumpUpPoints( strPrefix )
{
    //console_log( strPrefix + " up points = '" + g_listUpPoints.join( ", " ) + "'" );
};

function trimNull( strText )
{
    if ( strText === undefined && strText == null )
        return strText;

    return trim( strText );
};

function trim( strText )
{
    if ( strText !== undefined && strText != null )
        return strText.replace(/^\s+|\s+$/g,"");

    console_trace( "wowot! string is " + strText );
    return "";
};

function clearHighliting( selector )
{
    if ( selector === undefined )
    {
        for ( var i = 0; i < g_listUpPoints.length; i++ )
            clearHighliting( g_listUpPoints[i] );

        clearHighliting( getCurrentPage() );
    }

    else
        $("#" + selector ).html( $("#" + selector ).html().replace( /<span class=['"]search['"][^>]*>(.+?)<\/span>/g, "$1" ) );
};

function populateAlphabetic( filter, bNavigate, bSaveUpPoint )
{
    var youthID = g_currentUser;    // GUI operates on current user

    if ( bSaveUpPoint )
        saveUpPoint();

    clearActive();

    g_filter = filter;
    g_reSearchHighliter = null;

    var listBadges = null;
    var setBadges = {};
    var sort = true;

    if ( filter != '' )
    {
        if ( filter == "search" )
        {
            var strSearch = getSearch();
            if ( strSearch != null )
            {
                strSearch = trim( strSearch );
                setSearch( strSearch );
            }

            if ( strSearch == null || strSearch.length == 0 )
            {
                openLightBox( { text: "Please provide a search term", canClose: true } );
                return;
            }

            if ( strSearch.length < 3 )
            {
                openLightBox( { text: "Please provide a longer search term", canClose: true } );
                return;
            }
            strSearch = strSearch.toLowerCase();

            for ( var reqID in g_dbTables['Requirements'] )
            {
                var req = g_dbTables['Requirements'][reqID];
                if ( req.description != null && req.description.toLowerCase().indexOf( strSearch ) != -1 )
                    setBadges[req.badgeid] = 1;
            }

            for ( var badgeID in g_dbTables['MetaData'] )
            {
                var metadata = g_dbTables['MetaData'][badgeID];
                if ( metadata.name != null && metadata.name.toLowerCase().indexOf( strSearch ) != -1 )
                    setBadges[badgeID] = 1;
                else if ( metadata.purpose != null && metadata.purpose.toLowerCase().indexOf( strSearch ) != -1 )
                    setBadges[badgeID] = 1;
                else if ( metadata.notes != null && metadata.notes.toLowerCase().indexOf( strSearch ) != -1 )
                    setBadges[badgeID] = 1;
            }

            g_reSearchHighliter = new RegExp( "(" + strSearch + ")", "gi" );
        }
        else if ( filter == "ready" )
        {
            for ( var reqID in g_setReadyRequirements[youthID] )
            {
                if ( getRequirementStatus( youthID, reqID ) == "ready" )
                {
                    var badgeID = deriveBadgeID( reqID );
                    setBadges[badgeID] = 1;
                }
            }
        }
        else if ( filter == "favourite" )
        {
            for ( var reqID in g_setBookmarkedRequirements[youthID] )
            {
                if ( getRequirementStatus( youthID, reqID ) == "favourite" )
                {
                    var badgeID = deriveBadgeID( reqID );
                    setBadges[badgeID] = 1;
                }
            }
        }

        // CUB specific
        else if ( SECTION_TYPE == "pack" && filter == "type=awards,emblems" )
        {
            listBadges = sortSelectorBadges( null, "Award" );
            sort = false;
        }

        // SCOUTS-AU specific
        else if ( SECTION_TYPE == "troop_au" && filter == "Patrol" )
        {
            listBadges = sortSelectorBadges( null, "Patrol" );
            sort = false;
        }

        else  // alphabetic listing of badges in a specific category (e.g., "Athletics" or "Voyageur")
        {
            var selector = "#no-match-ever";
            if ( filter )
            {
                var id = filter.toLowerCase().replace( /[ ']/g, "" );
                selector = "#" + id + "-category-" + SECTION_TYPE;
            }

            if ( $(selector).length > 0 )
            {
                listBadges = new Array();
                $( selector + " div.page-content li > a[badgeid]" ).each( function() {
                    var badgeID = $(this).attr( "badgeid" );
                    var metadata = g_dbTables['MetaData'][badgeID];
                    if ( metadata === undefined )
                        console_trace( "found no metadata for '" + badgeID + "'" );
                    else if ( metadata.type != 'Placeholder' )
                        listBadges.push( badgeID );
                });
                sort = false;
            }
            else
            {
                for ( var badgeID in g_dbTables['MetaData'] )
                {
                    var metadata = g_dbTables['MetaData'][badgeID];
                    if ( SECTION_TYPE == "colony" || SECTION_TYPE == "colony_uk" )
                    {
                        if ( metadata.type == filter )
                            setBadges[badgeID] = 1;
                    }
                    else if ( metadata.category == filter && metadata.type != 'Placeholder' )
                        setBadges[badgeID] = 1;
                }
            }
        }
    }
    else        // alphabetic list of all badges
    {
        for ( var badgeID in g_dbTables['MetaData'] )
        {
            var metadata = g_dbTables['MetaData'][badgeID];
            if ( SECTION_TYPE == "unit" )
            {
                if ( metadata.type == 'Interest' )
                    setBadges[badgeID] = 1;
            }
            else
            {
                if ( metadata.type == 'Challenge' )
                    setBadges[badgeID] = 1;
            }
        }
    }

    if ( sort ) 
        listBadges = sortBadgesByName( setBadges );

    buildAlphabetic( listBadges, filter, bNavigate );
};

function sortBadgesByName( setBadges )
{
    // make the results be sorted by name
    var listSortedNames = new Array();
    var mapBadgesByName = {};
    for ( var badgeID in setBadges )
    {
        if ( badgeID === undefined )
            continue;
        if ( badgeID == null ) 
            console_trace( "badgeID is null" );
        if ( badgeID == "null" ) 
            console_trace( "badgeID = " + badgeID );
        var strName = g_dbTables['MetaData'][badgeID].name;
        listSortedNames.push( strName );      // append this name to the end of a simple list
        mapBadgesByName[strName] = badgeID;
    }
    listSortedNames.sort();

    var listBadges = new Array();
    for ( var iBadge = 0; iBadge < listSortedNames.length; iBadge++ )
    {
        var strName = listSortedNames[iBadge];
        listBadges.push( mapBadgesByName[strName] );
    }

    return listBadges;
};

function sortOutingsByDate( mapOutings )
{
    // make the results be sorted by name
    var listSortedDates = new Array();
    var mapOutingsByDate = {};
    for ( var outingID in mapOutings )
    {
        if ( outingID === undefined )
            continue;

        var i = "" + Math.abs( outingID );
        var d = "x" + mapOutings[outingID].date + "-" + (new Array(13 - i.length + 1).join("0" || '0') + i);
        listSortedDates.push( d );      // append this date
        mapOutingsByDate[d] = outingID;
    }
    listSortedDates.sort().reverse();

    var listOutingIDs = new Array();

    for ( var iDate = 0; iDate < listSortedDates.length; iDate++ )
    {
        var d = listSortedDates[iDate];
        listOutingIDs.push( mapOutingsByDate[d] );
    }

    return listOutingIDs;
};

function sortMembersByName( tableID, setYouth )
{
    // make the results be sorted by name
    var listSortedNames = new Array();
    var mapYouthByName = {};
    for ( var youthID in setYouth )
    {
        var youth = g_dbTables[tableID][youthID];

        var strName = getDisplayName( youth, youthID );
        strName = strName.toLowerCase();

        listSortedNames.push( strName + ":" + youthID );      // append this name to the end of a simple list
        mapYouthByName[strName + ":" +  youthID] = youthID;
    }
    listSortedNames.sort();

    var listYouth = new Array();
    for ( var iYouth = 0; iYouth < listSortedNames.length; iYouth++ )
    {
        var strNamePlusID = listSortedNames[iYouth];
        listYouth.push( mapYouthByName[strNamePlusID] );
    }

    return listYouth;
};

function sortTableKeysByDisplayName( table )
{
    // make the results be sorted by name
    var listSortedNames = new Array();
    var mapKeyByName = {};

    for ( var key in table )
    {
        var row = table[key];

        listSortedNames.push( row.displayname );
        mapKeyByName[ row.displayname ] = key;
    }

    listSortedNames.sort();

    var listKeys = new Array();
    for ( var iRow = 0; iRow < listSortedNames.length; iRow++ )
    {
        var strName = listSortedNames[iRow];
        listKeys.push( mapKeyByName[strName] );
    }

    return listKeys;
};

function hideFloaty()
{
    if ( ! g_useFloaty )
        closeReqFloaty();

    else
    {
        if ( g_floaty != null )
            g_floaty.hideFloaty();
    }
};

function showFloaty()
{
    if ( ! g_useFloaty )
        openReqFloaty();

    else
    {
        if ( g_floaty == null )
        {
            // OMFG... in Chrome, the floaty shows up underneath all the divs, unless you negate it's z-index.
            // But then, none of the items are clickable because technically they're under everything else (but
            // strangely visible).  However, if you detach the floaty and re-add it WITHIN the current page,
            // everything seems to work well
            reattachFloaty();

            g_floaty.makeFloaty( { spacing:5, time: '1s' } );

            $('.reqbased').toggle( g_currentReq != null );
            $('.badgebased').toggle( g_currentReq == null );

            if ( ! window.navigator.standalone )
            {
                resizeFloaty();
                g_floaty.scrollFloaty();
            }
        }
        else
        {
            reattachFloaty();
            resizeFloaty();
            g_floaty.scrollFloaty();
        }
    }
};

function reattachFloaty()
{
    if ( $('.floaty').length > 0 ) 
        g_floaty = $('.floaty').detach();
    $("#" + getCurrentPage()).append( g_floaty );
};

function toggleFloaty()
{
    if ( ! g_useFloaty )
    {
        if ( $('.floaty-background').length == 0 )
            showFloaty();
        else
            closeReqFloaty();
    }

    else
    {
        if ( g_floaty == null )
            showFloaty();
        else
        {
            resizeFloaty();
            reattachFloaty();
            g_floaty.toggleFloaty();
        }
    }
};

function selectStatusText()
{
    g_restoreReq = null;

    // parents and non-badgemasters can't do this 
    if ( g_mode == "handbook" || g_mode == "group" && getLoginID() && ( getRole() == "v" || getRole() == "i" ) )
    {
        updateBadgeFloaty();
        toggleFloaty();
    }
    else
        hideFloaty();
};

function closeFloaty()
{
    $('.curreq').removeClass('curreq');
    g_currentReq = null;
    g_restoreReq = null;
    hideFloaty();
};

function restoreSelection()
{
    var currentPage = getCurrentPage();
    if ( ! currentPage.match( /^badge-details-/ ) )
        return;

    if ( g_restoreReq == null )
        selectStatusText();
    else
        selectRequirementID( g_restoreReq );
};

function selectRequirementID( id )
{
    $('.curreq').removeClass('curreq');

    if ( g_mode == "handbook" && ! g_allowHandbookEdits )
        return;

    updateFloaty( id );

    if ( g_currentReq == id )
    {
        g_currentReq = null;
        hideFloaty();
    }
    else
    {
        g_currentReq = id;
        g_restoreReq = g_currentReq;
        $('[reqid='+id+']').addClass( 'curreq' );
        var currentPage = getCurrentPage();

        if ( currentPage.match( /^badge-details-/ ) )
        {
            // it's possible that we could have clicked on a link
            // to a another badge.  In this case, the routine will get 
            // called AFTER the navigation to the new page happens.
            //
            // Can we find the selected req on the current page
            var el2 = $('#' + currentPage + " tr[reqid=" + id + "]");
            if ( el2.length == 0 )
            {
                g_currentReq = null;
                hideFloaty();
            }
            else
                showFloaty();
        }
    };

    $('#list-youth-complete').attr( 'reqid', g_currentReq );
};

function selectRequirement( el )
{
    var id = $(el).closest("tr").attr( 'reqid' );
    selectRequirementID( id );
}

function getCurrentDetailsPage()
{
    var currentPage = getCurrentPage();
    var iCurrentPage = 1;
    if ( currentPage == "badge-details-0" )
        iCurrentPage = 0;

    return iCurrentPage;
};

function ajaxGetImage( selector, badgeID )
{
    if ( ! isOnline() )
    {
        console_log( "can't get image for badge '" + badgeID + "'... not online" );
        return;
    }

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&sectionid=" + getURLSection() + "&worksheet=BadgeImage&select=badge_id.eq.'" + badgeID + "'",
        error: function( request, textStatus, errorThrown ) {
            console_warn( "Error fetching image" );
        },
        success: function( data ) 
        {
            //console_log( "ajax returned: " + JSON.stringify( data ) );
            for ( var i in data['Images'] )
            {
                if ( data['Images'].hasOwnProperty( i ) )
                {
                    var image = data['Images'][i];
                    setLocalStorage( "img-" + image.badgeid, image.data );
                    $(selector).attr( 'src', image.data );
                }
            }
        }
    });
};

function deriveBadgeImage( badgeID, uniformID )
{
    var metadata = g_dbTables['MetaData'][badgeID];

    // any section-specific morphing?
    badgeID = translateBadgeImage( badgeID, uniformID );

    if ( MAP_INVENTORY_INVARIANTS[badgeID] !== undefined )
        return badgeID;

    if ( uniformID === undefined )
        uniformID = g_listUniformIDs[0];

    if ( MAP_INVENTORY_VARIANTS[badgeID] !== undefined )
        uniformID = 1-MAP_INVENTORY_VARIANTS[badgeID];            // TODO better logic that doesn't assume id's are 0 and 1

    if ( uniformID != 0 )
        badgeID += "_" + uniformID;

    return badgeID;
};

function asyncLoadImage( selector, badgeID, uniformID )
{
    var imageID = deriveBadgeImage( badgeID, uniformID );

    $(selector).attr( 'src', BIMG + "/images/blank.gif" );
    $(selector).attr( 'badgeid', imageID );

    var dataURI = getLocalStorage( "img-" + imageID );
    if ( dataURI === undefined || dataURI == null )
        window.setTimeout( function() { ajaxGetImage( selector+"[badgeid="+imageID+"]", imageID ) }, 100 );
    else
        $(selector).attr( 'src', dataURI );
};

function getBadgeStatus( youthID, badgeID )
{
    var isComplete = isBadgeComplete( youthID, badgeID );
    var isPlaceholder = getBadgeType(badgeID) == 'Placeholder';

    if ( isBadgeAwarded( youthID, badgeID ) || ( isPlaceholder && isComplete ) )
        return "awarded";

    if ( isComplete )
        return "complete";

    if ( isBadgeReady( youthID, badgeID ) )
        return "ready";

    if ( isBadgeBookmarked( youthID, badgeID ) )
        return "favourite";

    return "incomplete";
};

function formatDate( t, strFormat )
{
    return new Date(parseInt(t)).format( strFormat );
};

function getAwardedDate( t )
{
    var strDate = "";
    if ( t > 1000 ) // this should be enough to differentiate an epoch from a "1"
        strDate = '<span style="font-size:90%;color:#999;padding-left:5px;">(' + formatDate( t ) + ")</span>";  // wrap it

    return strDate;
};

function setBadgeStatusText( iPage, youthID, badgeID )
{
    if ( g_mode == "handbook" && ! g_allowHandbookEdits )
    {
        $("#badge-details-" + iPage + " [details-role=status-header]" ).hide();
        $("#badge-details-" + iPage + " [details-role=status]" ).hide();
        return;     // quit early
    }
    var strStatusHeader= "Status"; 
    if ( size( g_mapYouthNames ) > 1 )
        strStatusHeader += "<span style='font-weight:normal;color:#000;font-style:italic;font-size:90%;'> &ndash; <span id='badge-status-youth-" + iPage + "'>" + htmlEncode( g_mapYouthNames[youthID] ) + "</span></span>";

    $("#badge-dewails=" + iPage + " [details-role=status-header]" ).html( strStatusHeader ).show();
    $("#badge-details-" + iPage + " [details-role=status]" ).show();

    var span = document.createElement('span');
    var strText;
    var strTooltip = "";

    if ( isBadgeAwarded( youthID, badgeID ) )
    {
        var strDate = getAwardedDate( g_setAwardedBadges[youthID][badgeID].when );
        strText = '<img style="margin-bottom:-2px;" src="' + BIMG + '/images/check.gif"/> Awarded' + strDate;
        strTooltip = "Awarded by: " + getLoginName(g_setAwardedBadges[youthID][badgeID].by);
    }

    else if ( isBadgeComplete( youthID, badgeID ) )
        strText = '<img style="margin-bottom:-2px;" src="' + BIMG + '/images/unawarded.gif"/> Complete, but not awarded';

    else 
    {
        var nPercent = 0;
        if ( g_mapBadgeCompletion[youthID] != undefined && g_mapBadgeCompletion[youthID][badgeID] )
        {
            var jsonCompletion = g_mapBadgeCompletion[youthID][badgeID];
            nPercent = asPercentage( jsonCompletion );
        }

        if ( nPercent > 0 )
            strText = "In progress";
        else
            strText = "Not started";

        strText += " <span class='final' youthid=" + youthID + " badgeid='" + badgeID + "'></span>";
    }

    // youth and non-badgemasters should not have a 'Change' button
    var isClickable = g_mode == "handbook" || (g_mode == "group" && (getRole() == "v" || getRole() == "i"));
    if ( isClickable )
        strText += "<div class='button inline' style='float:none;padding:6px 12px;'>Change</div>";

    span.innerHTML = strText;
    $("#badge-details-" + iPage + " [details-role=status]" ).html( span ).attr( "title", strTooltip ).css( "cursor", isClickable ? "pointer" : "default" );

    if ( g_setReadyRequirements[youthID] != null )
    {
        var setHypotheticalBadges = {};
        setHypotheticalBadges[badgeID] = 1;
        testHypotheticalCompletion( youthID, setHypotheticalBadges, g_setReadyRequirements[youthID] );
    }
};

function linkifyDescription( strDescription, youthID, translateRoman )
{
    // strip out all the links
    var listLinks = new Array();
    if ( strDescription === undefined  )
        return "";

    strDescription = strDescription.replace( /\n/g, ' <!-- --> ' );
    while ( strDescription.match( /(\[\[.+?\]\])/ ) )
    {
        var strLink = RegExp.$1;

        var strPlaceholder = "~%" + listLinks.length + "%~";
        strDescription = strDescription.replace( /(\[\[.+?\]\])/, strPlaceholder );

        // external links #1 (href + text)
        strLink = strLink.replace( /\[\[(http[^|]+?)\|(.+?)\]\]/g, function( $0, $1, $2 ) {
            return "<a target=_blank href='" + $1 + "'>" + highliteSearch( $2 ) + "</a>"
        }); 
        // external links #2 (href only)
        strLink = strLink.replace( /\[\[(http[^\]]+?)\]\]/g, "<a target=_blank href='$1'>$1</a>" ); 

        // external links (relative) #1 (href + text)
        strLink = strLink.replace( /\[\[(\/[^|]+?)\|(.+?)\]\]/g, function( $0, $1, $2 ) {
            return "<a target=_blank href='" + $1 + "'>" + highliteSearch( $2 ) + "</a>";
        });
        // internal links #1 (always href + text)
        strLink = strLink.replace( /\[\[badge:([^|]+?)\|(.+?)\]\]/g, function( $0, $1, $2 ) { 
            // DAC test bNavigate
            //return "<a href='javascript:void(0);' onclick='setCurrentBadge( \"" + $1 + "\", false, \"dissolve\" );'>" + highliteSearch( $2 ) + "</a>"
            return "<a href='javascript:void(0);' onclick='setCurrentBadge( \"" + $1 + "\", true, \"dissolve\" );'>" + highliteSearch( $2 ) + "</a>"
        });
        // internal links #2 (always href + text)
        // CUB specific
        if ( SECTION_TYPE == "pack" ) 
            strLink = strLink.replace( /\[\[category:([^|]+?)\|(.+?)\]\]/g, function( $0, $1, $2 ) {
                return "<a href='javascript:void(0);' onclick='forceUpPoints(\"startpage\");populateActivity( \"" + $1 + "\", true, false );'>" + highliteSearch( $2 ) + "</a>"
            });
        else
            strLink = strLink.replace( /\[\[category:([^|]+?)\|(.+?)\]\]/g, function( $0, $1, $2 ) {
                return "<a href='javascript:void(0);' onclick='forceUpPoints(\"startpage,categories\");populateAlphabetic( \"" + $1 + "\", true, false );'>" + highliteSearch( $2 ) + "</a>"
            });
        // internal counts #3
        strLink = strLink.replace( /\[\[category-count:(.+?)\]\]/, function( $0, $1 ) { 
            if ( g_mapCategoryCounts[youthID] === undefined )
            {
                console_trace( "wowot? no category counts for youth " + youthID );
                return "";
            }
            else
            {
                var nCount = g_mapCategoryCounts[youthID][$1];
                if ( nCount === undefined )
                    nCount = g_mapCategoryCounts[youthID][g_mapCategoryIDs[$1]];

                if ( nCount !== undefined && nCount > 0 )
                    return " <span style='white-space:nowrap;'>(" + nCount + "<img height=17 width=17 src='" + BIMG + "/images/check.gif' style='padding-right:0px;vertical-align:-2px;'>)</span>";
                else
                    return " <span style='white-space:nowrap;color:#a43;font-weight:bold;'>(none)</span>";
            }
        });
        strLink = strLink.replace( /\[\[complete:(.+?)\]\]/g, function( $0, $1 ) {
            if ( isBadgeAwarded( youthID, $1 ) || isBadgeComplete( youthID, $1 ) )
                return " <span style='white-space:nowrap;'>(<img height=17 width=17 src='" + BIMG + "/images/check.gif' style='padding-right:0px;vertical-align:-2px;'>)</span>";
            else
                return "";
        });

        listLinks.push( { link: strLink, re: new RegExp( strPlaceholder ) } );
    }

    // highlight all the instance of the search term
    strDescription = highliteSearch( strDescription );

    // turn roman numerals into separate rows
    if ( translateRoman && strDescription.match( / (([-]|i{1,3}|i?v|vi{1,3}|i?x)\))/ ) )
    {
        strDescription = strDescription.replace( / (([-]|x?i{1,3}|x?i?v|x?vi{1,3}|x?i?x)\))\s*(.+?)(?= ([-]|x?i{1,3}|x?i?v|x?vi{1,3}|x?i?x)\))/g, "dac3500<tr><td style='padding-right:5px;text-align:right;vertical-align:top;'>$2)</td><td>$3</td></tr>dac3501" );
        strDescription = strDescription.replace( / (([-]|x?i{1,3}|x?i?v|x?vi{1,3}|x?i?x)\))\s*(.+)/g, "dac3500<tr><td style='padding-right:5px;text-align:right;vertical-align:top;'>$2)</td><td>$3</td></tr>dac3501" );
        strDescription = strDescription.replace( /dac3500\s*dac3501/g, "" );
        strDescription = strDescription.replace( /dac3500(.+)dac3501/g, "<table>$1</table>" );
        strDescription = strDescription.replace( /dac350\d/g, "" );
        strDescription = strDescription.replace( /(<td[^>]*?>)-\)/g, "$1" + BULLET );
    }

    // restore all the links
    while ( listLinks.length > 0 )
    {
        var placeholder = listLinks.shift();
        strDescription = strDescription.replace( placeholder.re, placeholder.link );
    }

    return strDescription;
}

function getOutingYouthLabels( outing, youthID )
{
    var labels = null;
    if ( youthID != -1 )
    {
        for ( var iYouth = 0; iYouth < outing.youth.length; iYouth++ )
        {
            var outingYouth = outing.youth[iYouth];
            if ( outingYouth.id == youthID )
            {
                if ( outingYouth.labels !== undefined )
                    return outingYouth.labels
                else
                    return outing.labels;
            }
        };
    }

    return outing.labels;
};

function doNextPrevPage( isNext )
{
    clearCurrentRequirementHighlighting();

    if ( isNext )
        setCurrentBadge( g_nextAlphabetic[g_currentBadgeID], false, "slide" );
    else
        setCurrentBadge( g_prevAlphabetic[g_currentBadgeID], false, "slideback" );
};

function getUpcomingRequirementTooltip( reqID )
{
    var strTooltip = "";
    for ( var iOuting = 0; iOuting < g_mapUpcomingReqs[reqID].length; iOuting++ )
        strTooltip += ", '" + getOutingName( getOuting( g_mapUpcomingReqs[reqID][iOuting] ) ) + "'";
    
    if ( isTallyReq( reqID ) ) 
        strTooltip = "This requirement may be counted towards by the following event(s): " + strTooltip.substring( 2 );
    else
        strTooltip = "This requirement may be completed by the following event(s): " + strTooltip.substring( 2 );

    return strTooltip;
};

function isTallyReq( reqID )
{
    return g_mapAutocompletion[reqID] !== undefined && g_mapAutocompletion[reqID].indexOf( "tally:" ) != -1;
};

function buildCurrentBadge( metadata, resultsRequirements, bNavigate, animation, strBackText )
{
    //try
    {
        var iCurrentPage = getCurrentDetailsPage(); 
        var iPage = bNavigate ? 1-iCurrentPage : iCurrentPage;      // stay on same page if not navigating
        var newPageID = "badge-details-" + iPage;
        if ( true || getCurrentPage() != newPageID )
            iPage = "z";

        var youthID = g_currentUser;        // GUI works on current user

        if ( g_prevAlphabetic !== undefined && g_nextAlphabetic !== undefined )
        {
            $('#badge-details-'+iPage+' div.nextprev-page > div:first-child').toggleClass( "invisible", g_prevAlphabetic[g_currentBadgeID] === undefined );
            $('#badge-details-'+iPage+' div.nextprev-page > div:first-child + div').toggleClass( "invisible", g_nextAlphabetic[g_currentBadgeID] === undefined );
        }
        else
        {
            $('#badge-details-'+iPage+' div.nextprev-page > div:first-child').toggleClass( "invisible", true );
            $('#badge-details-'+iPage+' div.nextprev-page > div:first-child + div').toggleClass( "invisible", true );
        }

        var span = document.createElement('span');

        // CUB/SCOUT specific
        if ( SECTION_TYPE == "troop" )
            span.innerHTML = metadata.type == "Challenge" ? tagAbbreviations( "Challenge Badge" ) : (metadata.type == "Activity" ? "Activity Badge" : metadata.type);
        else
            span.innerHTML = metadata.type == "Challenge" ? "Badge" : metadata.type;

        $('#floaty-more div[data-role=header] a').text( metadata.type );
        $("#badge-details-" + iPage + " [details-role=title]").html( span );

        var span = document.createElement('span');
        span.innerHTML = highliteSearch( metadata.name );
        $("#badge-details-" + iPage + " [details-role=name]").empty();
        $("#badge-details-" + iPage + " [details-role=name]").append( span );

        var strPurpose = metadata.purpose;
        var strReqInstructions = "";
        if ( strPurpose !== undefined && strPurpose && strPurpose.match( /<requirements>(.+)<\/requirements>(.*)/ ) )
        {
            strReqInstructions = "<div class='req-instructions'>" + RegExp.$1 + "</div>";
            strPurpose = RegExp.$2;
        }

        if ( ! strPurpose || strPurpose == "" )
            $("#badge-details-" + iPage + " .badge-purpose").hide();
        else
        {
            span = document.createElement('span');
            span.innerHTML = linkifyDescription( strPurpose, youthID, true );
            $("#badge-details-" + iPage + " [details-role=purpose]" ).empty();
            $("#badge-details-" + iPage + " [details-role=purpose]" ).append( span );
            $("#badge-details-" + iPage + " .badge-purpose").show();
        }

        if ( ! metadata.notes || metadata.notes == "" )
            $("#badge-details-" + iPage + " .badge-notes").hide();
        else
        {
            span = document.createElement('span');
            var strNotes = linkifyDescription( metadata.notes, youthID, true );
            span.innerHTML = strNotes;
            $("#badge-details-" + iPage + " [details-role=notes]" ).empty();
            $("#badge-details-" + iPage + " [details-role=notes]" ).append( span );
            $("#badge-details-" + iPage + " .badge-notes").show();
        }


        if ( ! metadata.resources || metadata.resources == "" )
            $("#badge-details-" + iPage + " .badge-resources").hide();
        else
        {
            var strLink = metadata.resources.replace( /\[\[([^|]+?)\|(.+?)\]\]/g, "<a target=_blank href='$1'>$2</a>" ); 
            span = document.createElement('span');
            span.innerHTML = strLink;
            $("#badge-details-" + iPage + " [details-role=resources]" ).empty();
            $("#badge-details-" + iPage + " [details-role=resources]" ).append( span );
            $("#badge-details-" + iPage + " .badge-resources").show();
        }

        if ( ! metadata.prerequisites || metadata.prerequisites == "" )
            $("#badge-details-" + iPage + " .badge-prerequisites").hide();
        else
        {
            var strPrereq = "";
            // todo: support a full boolean syntax.  For now, the only syntax support is "|"
            var prereqs = metadata.prerequisites.replace( /\(?(.+?)\)?/, "$1" ).split( /\|/ );
            for ( var i = 0; i < prereqs.length; i++ )
            {
                if ( strPrereq.length != "" )
                    strPrereq += " <span class='conjunction'>or</span> ";

                var badgeID = prereqs[i];
                strPrereq += "<a href='javascript:void(0)' onclick='setCurrentBadge(\"" + badgeID + "\", true, \"dissolve\");'>" + getBadgeName(badgeID) + "</a>";
            }
            span = document.createElement('span');
            span.innerHTML = strPrereq;
            $("#badge-details-" + iPage + " [details-role=prerequisites]" ).empty();
            $("#badge-details-" + iPage + " [details-role=prerequisites]" ).append( span );
            $("#badge-details-" + iPage + " .badge-prerequisites").show();
        }

        var badgeID = metadata.id;
        setBadgeStatusText( iPage, youthID, badgeID );

        var strRequirements;
        if ( resultsRequirements.length == 0 )
            strRequirements = "<span class=problem>not defined</span>";

        else 
        {
            // sort the requirements
            var listRequirementIDs = new Array();
            var listRequirements = new Array();
            for ( var iRow = 0; iRow < resultsRequirements.length; iRow++ )
            {
                var req = resultsRequirements[iRow];
                var strRequirementID = req.requirement;
                if ( strRequirementID.match( /^([A-Z]?)(\d[a-z]?)$/ ) )
                    strRequirementID = RegExp.$1 + "0" + RegExp.$2;

                listRequirementIDs.push( strRequirementID );
                listRequirements[req.requirement] = req;
            }
            listRequirementIDs.sort();

            var isComplete = isBadgeComplete( youthID, badgeID );
            var isAwarded = isBadgeAwarded( youthID, badgeID );

            var strRows = "";
            for ( var i in listRequirementIDs )
            {
                var req = listRequirements[listRequirementIDs[i].replace(/^([A-Z]?)0/,"$1")];
                var strCells;
                var strDescription = linkifyDescription( req.description, youthID, true );

                var jsOnClick = "onclick='selectRequirement(this);'";

                var strRequirement = req.requirement.replace( /^[A-Z](.*)/, "$1" );
                var parentReqID = deriveParentID( req.id );
                var targetReqID = req.id;

                var flag = getRequirementVisibleStatus( youthID, targetReqID );

                var strTooltip = getRequirementTooltip( targetReqID, flag, youthID );

                if ( ! isComplete && ! isAwarded && flag != "complete" && flag != "implicit" )
                {
                    var isResolved = false;
                    if ( hasUpPoint( "view-event" ) )
                    {
                        var outing = getOuting(getCurrentEvent());
                        if ( isOutingUnresolved( outing, true ) && isRequirementAddressedByOuting( outing, req.id ) )
                        {
                            for ( var iYouth = 0; iYouth < outing.youth.length; iYouth++ )
                            {
                                if( outing.youth[iYouth].id == g_currentUser )
                                {
                                    var strAnnotation = "Completed?";
                                    strTooltip = "This requirement may have been addressed by the event '" + getOutingName(outing) + "'";
                                    if ( isTallyReq( req.id ) )
                                    {
                                        strAnnotation =  "Counted towards?";
                                        strTooltip = "This requirement may have been counted towards by the event '" + getOutingName(outing) + "'";
                                    }
                                    strDescription = "<span style='font-weight:bold;color:#a43;padding-right:5px;'>(" + strAnnotation + ")</span>" + strDescription;

                                    if ( flag == "incomplete" || flag == "partial" )
                                        flag = "unresolved";

                                    isResolved = true;
                                    break;
                                }
                            }
                        }
                    }

                    if ( flag == "overridden" )
                        strTooltip = "This subrequirement's parent was marked as complete.";

                    else if ( ! isResolved && g_mapUpcomingReqs[req.id] !== undefined && g_mapUpcomingReqs[req.id].length > 0 )
                    {
                        strDescription = "<span style='font-weight:bold;color:#a43;padding-right:5px;'>(Scheduled)</span>" + strDescription;
                        strTooltip = getUpcomingRequirementTooltip( req.id );

                        if ( flag == "incomplete" || flag == "partial" )
                            flag = "planned";
                    }
                }

                if ( strTooltip !== undefined && strTooltip )
                    strTooltip = " title='" + strTooltip.replace( /'/g, "&apos;" ) + "'";

                // is this a section header (e.g., "A", "B")
                if ( strRequirement == "" )
                {
                    if ( strDescription == "" )
                        continue;       // this is a blank header... skip it

                    if ( strDescription.match( / - / ) )
                        strDescription = strDescription.replace( /(.+)( - .+)/, "<span class='sectionheader'>$1</span>$2" );
                    else if ( strDescription.match( /<p>/ ) )
                        strDescription = strDescription.replace( /(.+?)(<p>.+)/, "<span class='sectionheader'>$1</span>$2" );
                    else
                        strDescription = "<span class='sectionheader'>" + strDescription + "</span>";
                    strCells = "<td colspan=4 style='padding-top:9px;padding-bottom:5px;'>" + strDescription + "</td>";
                    jsOnClick = null;
                }

                // is this a requirement (e.g., "2", "B1")
                else if ( strRequirement.match( /^\d+$/ ) )
                {
                    // Highlight dependent reqs by using a box with a dashed border.
                    // Parent/Youth accouts can't complete reqs, so there's no point 
                    // in showing this distinction
                    if ( g_mode == "group" && getRole() != "p" )
                    {
                        if ( isTallyReq( targetReqID ) )
                        {
                            flag += " tally";
                            flag += hasAutoLinks( targetReqID ) ? " dependent" : "";
                        }
                        else if ( isAutoRequirement( targetReqID ) )
                            flag += " dependent";
                    }

                    // if a requirement has a weight of zero, then it does not merit a checkbox
                    if ( getRequirementWeight( targetReqID ) == 0 )
                    {
                        strCells = "<td colspan=4 style='padding-top:9px;'>" + strDescription + "</td>";
                        jsOnClick = null;
                    }
                    else
                        strCells = "<td glyph" + strTooltip + " class='" + flag + "' " + jsOnClick + "></td><td class='" + flag + "' " + jsOnClick + " style='padding-top:3px;padding-left:3px;padding-right:3px;'>" + strRequirement + ".</td><td class='" + flag + "' " + jsOnClick + " colspan=2>" + strDescription + "</td>";
                }
                //
                // is this a subequirement (e.g., "2a", "B1c")
                else
                {
                    // is it part of a sub-req'd requirement?
                    if ( g_mode == "group" && isSubReqd( parentReqID ) )
                    {
                        strRequirement = strRequirement.replace(/\d+/,"") + ")";
                        if ( flag == "overridden" )
                        {
                            flag = "complete";

                            var glyphFlag = "complete";
                            if ( g_mode != "group" || getRole() != "p" )
                            {
                                flag += " dependent";
                                if ( isTallyReq( targetReqID ) )
                                {
                                    glyphFlag += " tally";
                                    glyphFlag += hasAutoLinks( targetReqID ) ? " dependent" : "";
                                }
                                else if ( isAutoRequirement( targetReqID ) )
                                    glyphFlag += " dependent";
                            }

                            strCells = "<td subreq glyph" + strTooltip + " class='" + glyphFlag + "' " + jsOnClick + " colspan=2></td><td class='" + flag + "' " + jsOnClick + " style='padding-top:3px;padding-left:3px;padding-right:3px;'>" + strRequirement + "</td><td class='" + flag + "' " + jsOnClick + ">" + strDescription + "</td>";
                        }

                        else
                        {
                            var extraFlag = "";
                            if ( g_mode != "group" || getRole() != "p" )
                            {
                                if ( isTallyReq( targetReqID ) )
                                {
                                    extraFlag += " tally";
                                    flag += hasAutoLinks( targetReqID ) ? " dependent" : "";
                                }
                                else if ( isAutoRequirement( targetReqID ) )
                                {
                                    //extraFlag += " dependent";
                                    flag += " dependent";
                                }
                            }

                            strCells = "<td subreq glyph" + strTooltip + " class='" + flag + extraFlag + "' " + jsOnClick + " colspan=2></td><td class='" + flag + "' " + jsOnClick + " style='padding-top:3px;padding-left:3px;padding-right:3px;'>" + strRequirement + "</td><td class='" + flag + "' " + jsOnClick + ">" + strDescription + "</td>";
                        }
                    }
                    else
                    {
                        targetReqID = parentReqID;
                        flag = getRequirementStatus( youthID, targetReqID );

                        // Highlight dependent reqs by using a box with a dashed border.
                        // Parent/Youth accouts can't complete reqs, so there's no point 
                        // in showing this distinction
                        var extraFlag = "";
                        if ( g_mode == "group" && getRole() != "p" )
                        {
                            if ( isTallyReq( targetReqID ) )
                            {
                                extraFlag += " tally";
                                extraFlag += hasAutoLinks( targetReqID ) ? " dependent" : "";
                            }
                            else if ( isAutoRequirement( targetReqID ) )
                                extraFlag = " dependent";
                        }

                        strCells = "<td class='" + flag + "' colspan=2>&nbsp;</td><td class='" + flag + extraFlag + "' " + jsOnClick + " style='padding-right:5px;'>" + strRequirement.replace(/\d+/,"") + ")</td><td class='" + flag + extraFlag + "' " + jsOnClick + ">" + strDescription + "</td>";
                    }
                }

                var regex1 = new RegExp( "\\|\\(?" + req.requirement + "[&\\|\\)]" );
                var addConjunction = false;
                if ( regex1.test( metadata.displaylogic ) )
                    addConjunction = true;
                else
                {
                    var regex2 = new RegExp( "\\|" + req.requirement + "$" );
                    if ( regex2.test( metadata.displaylogic ) )
                        addConjunction = true;
                }
                if ( addConjunction )
                {
                    var reqid = parentReqID != req.id ? "reqid='" + parentReqID + "'" : "";
                    strRows += "<tr " + reqid + "><td colspan=4 class='conjunction " + flag + "'>&mdash; or &mdash;</td></tr>";
                }

                // handbook is non-interactive, so disable all hover effects, clicks, etc
                if ( g_mode == "handbook" )
                    jsOnClick = null;

                if ( jsOnClick != null )
                    strRows += "<tr reqid='" + targetReqID + "'>" + strCells + "</tr>";
                else
                    strRows += "<tr>" + strCells + "</tr>";
            }
            strRequirements = "<table cellspacing=0>" + strRows + "</table>";
        }

        var strEvents = "";
        var nLastYear = 2100;
        var listOutingIDs = sortOutingsByDate( g_mapOutings );

        var now = getServerTimestamp();

        var youth = g_dbTables['Youth'][g_currentUser];

        for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
        {
            var outingID = listOutingIDs[iOuting];
            var outing = getOuting(outingID);

            // skip events that occured before this youth joined the section
            if ( youth === undefined || youth == null || outing.date < youth.date )
                continue;

            if ( ! isMeetingVisible( outing ) )
                continue;

            for ( var iLink = 0; iLink < outing.links.length; iLink++ )
            {
                if ( deriveBadgeID( outing.links[iLink] ) == badgeID )
                {
                    var classInactive = isUpcoming( outing ) ? "" : " inactive";

                    for ( var iYouth = 0; iYouth < outing.youth.length; iYouth++ )
                    {
                        if( outing.youth[iYouth].id == g_currentUser )
                        {
                            classInactive = "";
                            break;
                        }
                    }

                    var annotation = getOutingAnnotation( outing );

                    var nYear = getOutingYear( outing );
                    if ( nYear != nLastYear )
                    {
                        strEvents += "<li class='divider'>" + nYear + "-" + (nYear+1) + "</li>";
                        nLastYear = nYear;
                    }

                    var htmlOuting = buildEventReportRow( getOutingYouthLabels( outing, youthID ), true, true );
                    if ( outing.shared !== undefined && ! outing.shared )
                        htmlOuting = "<img src='./images/notshared.gif' style='padding-right:5px;width:15px;' title='This event is not shared with " + STR_YOUTHS + "/parents'>" + htmlOuting;
                    if ( isOutingUnresolved( outing, true ) )
                        htmlOuting = "<img src='./images/questionmark.gif' style='padding-right:5px;width:18px;margin-bottom:-1px;' title='This event needs to be finalized'>" + htmlOuting;
                    var htmlDate = "<span class=\"subtext\">" + formatOutingDateRange( outing ) + "</span>";

                    strEvents += "<li class='forward " + annotation.style + classInactive + "'><a outingid='" + outing.id + "' href='javascript:void(0)' onclick='doViewEvent(" + outing.id + ");'><span class='stunted'>" + htmlOuting + getOutingName( outing ) + annotation.text + htmlDate + "</span></a>" + "</li>";
                    break;
                }
            }
        }

        var strRelatedEvents = "Related Events";
        if ( size( g_mapYouthNames ) > 1 )
            strRelatedEvents += "<span style='font-weight:normal;color:#000;font-style:italic;font-size:90%;'> &ndash; " + htmlEncode( g_mapYouthNames[youthID] ) + "</span>";
        $("#badge-details-" + iPage + " [details-role=events] h1").html( strRelatedEvents );
        $("#badge-details-" + iPage + " [details-role=events] ul").html( strEvents );
        $("#badge-details-" + iPage + " [details-role=events]").toggle( strEvents.length > 0 );
        var divCategory = $("#badge-details-" + iPage + " [details-role=category]" );
        divCategory.css( "margin-top", strEvents.length > 0 ? 0 : 10 ).empty();

        $("#badge-details-" + iPage + " [details-role=requirements]").empty().append( strReqInstructions + strRequirements );
        
        // CUB/SCOUT specific
        if ( SECTION_TYPE == "pack" )
        {
            divCategory.append( "<span>" + STR_CATEGORY + ": <a href='javascript:void(0)' onclick='populateActivity(\"" + metadata.category + "\",true,false);g_listUpPoints.pop();'>" + metadata.category + "</a></span>" ).show();
        }
        else if ( SECTION_TYPE == "troop" && metadata.type == "Challenge" )
        {
            divCategory.append( "<span>" + STR_CATEGORY + ": <a href='javascript:void(0)' onclick='populateAlphabetic(\"" + metadata.category + "\",true,false);g_listUpPoints.pop();'>" + metadata.category + "</a></span>" ).show();
        }
        else if ( SECTION_TYPE == "unit" && metadata.type == "Interest" )
        {
            divCategory.append( "<span>" + STR_CATEGORY + ": <a href='javascript:void(0)' onclick='populateAlphabetic(\"" + metadata.category + "\",true,false);g_listUpPoints.pop();'>" + metadata.category + "</a></span>" ).show();
        }
        else
            divCategory.hide();

        var strLastUpPoint = lastUpPoint();

        if ( strBackText === undefined || ! strBackText )
        {
            strBackText = "Back";
            if ( strLastUpPoint == "alphabetic" )
            {
                if ( g_filter == "" )
                    strBackText = "Alphabetic";
                else if ( g_filter == "search" )
                    strBackText = "Search";
                else if ( g_filter == "favourite" )
                    strBackText = "Bookmarked";
                else if ( g_filter == "ready" )
                    strBackText = "Ready";
                else if ( SECTION_TYPE == "troop_au" && g_filter == "Patrol" )
                    strBackText = "Activities";
                else if ( SECTION_TYPE == "colony" || SECTION_TYPE == "colony_uk" )
                    strBackText = g_filter=="Staged"?"Stages":(g_filter=="Activity"?"Activities":"Challenges");
                else
                    strBackText = STR_CATEGORY;
            }
            else if ( getCurrentPage() == "mypath" )
                strBackText = "My Path";
            else if ( strLastUpPoint == "table-report" )
                strBackText = "Report";
            else if ( strLastUpPoint == "other-category" )
                strBackText = "Awards";
            else if ( strLastUpPoint == "report" || strLastUpPoint == "table-report" )
                strBackText = "Report";
            else if ( strLastUpPoint == "view-event" )
                strBackText = "Event";
            else if ( strLastUpPoint == "youinguiding-category-unit"
              || strLastUpPoint == "youandothers-category-unit"
              || strLastUpPoint == "discoveringyou-category-unit"
              || strLastUpPoint == "beyondyou-category-unit" )
                strBackText = "Area";
            else if ( strLastUpPoint == "core-category-troop_au" )
                strBackText = "Signposts";
            else 
                strBackText = "Award";
        }

        setBackText( '#badge-details-' + iPage, strBackText );

        updateAbbreviations( '#badge-details-' + iPage, ! isLandscape() );

        // can we afford to blow away the old page and re-create it?
        if ( iPage == "z" )
        {
            var sidebar = $('.sidebar').detach();
            if ( g_floaty != null && $('.floaty').length > 0 ) 
                g_floaty = $('.floaty').detach();
            $( "#" + newPageID ).remove();
            $( "body" ).append( "<div id='" + newPageID + "' data-role='page'>" + $("#badge-details-" + iPage ).html() + "</div>" );
            $('#' + newPageID + ' div[data-role=content]').prepend( sidebar );
            if ( g_floaty != null )
                $("#" + newPageID).append( g_floaty );
        }

        // we actually always navigate to the (possibly same) page
        addPage( "#" + newPageID, animation );

        // now we have a the right page to load the image into
        asyncLoadImage( "#" + newPageID + " [details-role=image]", metadata.id, getYouthUniform( youthID ) );

    } //catch(e) {
        //console_trace( "error building '" + JSON.stringify( metadata ) + "'" );
    //}
};

function highliteSearch( strText )
{
    if ( strText !== undefined )
        if ( g_reSearchHighliter )
            strText = strText.replace( g_reSearchHighliter, "<span class='search'>$1</span>" );

    // a search at the beginning of the string will be truncated with ellipses
    strText = strText.replace( /^<span class='search'>/, "<span class='search' style='margin-left:0;'>" );

    return strText;
};

function highliteFilter( strText )
{
    if ( strText !== undefined )
        if ( g_reFilterHighliter )
            strText = strText.replace( g_reFilterHighliter, "<span class='search'>$1</span>" );

    // a search at the beginning of the string will be truncated with ellipses
    strText = strText.replace( /^<span class='search'>/, "<span class='search' style='margin-left:0;'>" );

    return strText;
};

function setCurrentBadge( badgeID, bNavigate, animation, strBackText )
{
    clearActive();

    if ( badgeID == null )
    {
        var strMessage = "No more badges";
        if ( g_filter == "search" )
            strMessage = "No more search results";
        else if ( g_filter == "ready" )
            strMessage = "No more badges ready to test";
        else if ( g_filter == "favourite" )
            strMessage = "No more bookmarked badges";
        else if ( g_filter != "Other" || g_categories[g_filter] )       // "Other" is a SCOUT/VENTURER specific value
            strMessage = STR_NOMOREBADGES;
        else if ( g_filter != "" )
            strMessage = "No more badges in this category";

        openLightBox( { text: strMessage, timeout: 2000 } );

        return;
    }

    g_currentBadgeID = badgeID;

    var resultsRequirements = new Array();
    for ( var reqID in g_dbTables['Requirements'] )
    {
        var requirement = g_dbTables['Requirements'][reqID];
        if ( requirement.badgeid == badgeID )
            resultsRequirements.push( requirement );
    }

    var metadata = g_dbTables['MetaData'][badgeID];
    if ( metadata !== undefined )
        buildCurrentBadge( metadata, resultsRequirements, bNavigate, animation, strBackText );
};

function updateAbbreviations( selector, abbreviate )
{
    for ( var i = 0; i < g_abbreviations.length; i++ )
    {
        var json = g_abbreviations[i];
        $( selector + ' span.abbrev-' + i ).text( abbreviate ? json.abbreviated : json.full );
    }
};

window.onresize = resizeElements

window.onorientationchange = changeOrientation;

function changeOrientation()
{
    var isPortrait = ! isLandscape();
    //console_log( "orientation = " + window.orientation );
    //console_log( "width = " + windowWidth() + ", height = " + windowHeight() );
    $('body').css( 'width', 'auto' );
    $('body').css( 'height', 'auto' );

    if ( isPortrait )
        $('body').removeClass( "landscape" ).addClass( "portrait" );
    else
        $('body').removeClass( "portrait" ).addClass( "landscape" );

    resizeElements();
    updateAbbreviations( 'body', isPortrait );

    window.setTimeout( function() { window.scrollTo(0, 0); }, 100 );
};

function windowWidth()
{
    if ( window.innerWidth !== undefined )
    {
        //console_log( "width = " + window.innerWidth ); 
        return window.innerWidth;
    }

    return document.documentElement.clientWidth;
};

function windowHeight()
{
    if ( window.innerHeight !== undefined )
    {
        //console_log( "height = " + window.innerHeight ); 
        return window.innerHeight;
    }

    return document.documentElement.clientHeight;
};

function resizeCompletion( extraOffset )
{
    var offset = hasSidebar() ? ($('div.sidebar').width()+20) : 0;
    $('#related-event-selector').width( windowWidth() - 190 - offset - extraOffset );
    $('#table-report-badge-selector').width( windowWidth() - 190 - offset - extraOffset );
    $('#table-report-event-selector').width( windowWidth() - 190 - offset - extraOffset );
    $('#table-report-attendance-selector').width( windowWidth() - 190 - offset - extraOffset );
    //$('#table-report-attendance-year-selector').width( windowWidth() - 190 - offset - extraOffset );
};

function hasSidebar()
{
    return $('.sidebar').is( ":visible" )
};

function resizeFloaty()
{
    //console_log( "window.innerWidth = " + windowWidth() + ", document.body.clientWidth = " + document.body.clientWidth );
    if ( g_floaty )
    {
        var pos = g_floaty.offset();
        g_floaty.offset( { top: pos.top, left: document.body.clientWidth - (g_isFirefox?200:190) - 5 } );
    }
};

function resizeElements()
{
    resizeCompletion( 0 );

    var currentYouth = $('#current-youth-container');
    if ( ! g_isEmbedded && windowWidth() > WIDE && getCurrentPage() != "eula" )
    {
        if ( ! hasSidebar() )
        {
            $("#size-stylesheet").attr( "href", BIMG + "/style-wide.css" );  // show the sidebar
            $("#size-stylesheet-section").attr( "href", BIMG + "/section-theme/style-wide.css" );  // show the sidebar
            $("#startpage-search").hide();
            updateQuickLinksVisibility()
            resizeCompletion( 220 );

            removeOfflineHeaderIcons();     // if any exist, get rid of them
        }

        $(".link-reports").hide();
        currentYouth.hide();
    }
    else
    {
        if ( hasSidebar() )
        {
            $("#size-stylesheet").attr( "href", "" );                   // hide the sidebar
            $("#size-stylesheet-section").attr( "href", "" );           // hide the sidebar
            $("#startpage-search").show();
            updateQuickLinksVisibility()

            if ( ! isOnline() )
                addOfflineHeaderIcons();
        }

        // parents can't see reports
        $(".link-reports").toggle( getLoginID() != null && getRole() != "p" && getRole() != "i" );

        if ( ! currentYouth.is( ":visible" ) && g_mode == "group" && getLoginID() )
            currentYouth.show();
    }

    resizeLightbox( false );

    if ( ! g_useFloaty ) 
        resizeReqFloaty( false );
    else
        resizeFloaty();
};

function getRequirementTooltip( reqID, flag, youthID )
{
    var strTooltip = "";

    if ( flag == "complete" )
    {
        var leaderID = g_setMarkedRequirements[youthID][reqID].by;
        if ( leaderID == -1 )
            strTooltip = "Always complete (" + STR_SECTION + " preference)";
        else
        {
            strTooltip = "Completed by " + getLoginName(leaderID) + " on " + formatDate( g_setMarkedRequirements[youthID][reqID].when );
            var strNotes = g_setMarkedRequirements[youthID][reqID].notes;
            if ( strNotes !== undefined && strNotes )
                strTooltip += " &ndash; " + strNotes.replace( /'/g, "&apos;" );
        }
    }

    else if ( flag == "implicit" )
        strTooltip = "Complete because requirements are satisfied";

    else if ( flag == "partial" )
    {
        var jsonCompletion = g_mapRequirementCompletion[youthID][reqID];
        if ( isTallyReq( reqID ) )
            strTooltip = getTallyNotes( youthID, reqID );

        if ( ! strTooltip )
            strTooltip = "" + asPercentage( jsonCompletion ) + "% complete";
    }

    else if ( flag == "ready" )
    {
        var leaderID = g_setReadyRequirements[youthID][reqID].by;
        strTooltip = "Readied by " + getLoginName(leaderID) + " on " + formatDate( g_setReadyRequirements[youthID][reqID].when );
        var strNotes = g_setReadyRequirements[youthID][reqID].notes;
        if ( strNotes !== undefined && strNotes )
            strTooltip += " &ndash; " + strNotes.replace( /'/g, "&apos;" );
    }

    else if ( flag == "favourite" )
    {
        var leaderID = g_setBookmarkedRequirements[youthID][reqID].by;
        strTooltip = "Bookmarked by " + getLoginName(leaderID) + " on " + formatDate( g_setBookmarkedRequirements[youthID][reqID].when );
        var strNotes = g_setBookmarkedRequirements[youthID][reqID].notes;
        if ( strNotes !== undefined && strNotes )
            strTooltip += " &ndash; " + strNotes.replace( /'/g, "&apos;" );
    }

    return strTooltip;
};

function updateSelection( reqID )
{
    if ( reqID == null )
        return;

    var youthID = g_currentUser;   // GUI operates on current user
    var debug = isBadgeOfInterest( deriveBadgeID( reqID ) );

    var selector = "[reqid=" + reqID + "]";

    // clear everything
    $(selector).children().removeClass( "favourite" ).removeClass( "ready" ).removeClass( "incomplete" ).removeClass( "implicit" ).removeClass( "partial" ).removeClass( "complete" );

    var flag = getRequirementStatus( youthID, reqID );


    if ( debug ) console_log( "status for reqID = '" + reqID + "' is '" + flag + "'" );

    var strTooltip = getRequirementTooltip( reqID, flag, youthID );

    $(selector).children().addClass( flag );
    $("[reqid=" + reqID + "] td:first-child").attr( "title", strTooltip );

    // highlight dependent reqs by using a box with a dashed border
    // Parent/Youth accouts can't complete reqs, so there's no point 
    // in showing this distinction
    if ( ! ( g_mode == 'group' && getRole() == "p" ) )
    {
        if ( isTallyReq( reqID ) )
        {
            $(selector).children().toggleClass( "dependent", hasAutoLinks( reqID ) );
            $(selector).children().addClass( "tally" );
        }
        if ( isAutoRequirement( reqID )  )
            $(selector).children().addClass( "dependent" );
    }
    else
        $(selector).children().removeClass( "dependent" ).removeClass( "tally" );
};

function updateBadgeFloaty()
{
    var badgeID = g_currentBadgeID;
    var youthID = g_currentUser;   // GUI operates on current user
    var isAwarded = isBadgeAwarded( youthID, badgeID );
    var isComplete = isBadgeComplete( youthID, badgeID );

    var strDate = formatDate( getServerTimestamp() );

    if ( isAwarded )
    {
        $('[action=floaty-complete-all]').removeClass( 'checked' ).attr( 'disabled', true );
        $('[action=floaty-award-all]').addClass( 'checked' );
        strDate = formatDate( g_setAwardedBadges[youthID][badgeID].when );
    }
    else if ( isComplete )
    {
        $('[action=floaty-complete-all]').addClass( 'checked' ).removeAttr( 'disabled', true );
        $('[action=floaty-award-all]').removeClass( 'checked' );
    }
    else
    {
        $('[action=floaty-complete-all]').removeClass( 'checked' ).removeAttr( 'disabled', true );
        $('[action=floaty-award-all]').removeClass( 'checked' );
    }

    $('[action=floaty-complete]').removeClass( 'checked' ).attr( 'disabled', true );
    $('[action=floaty-ready]').removeClass( 'checked' ).attr( 'disabled', true );
    $('[action=floaty-favourite]').removeClass( 'checked' ).attr( 'disabled', true );

    $('.reqbased').hide();
    $('.badgebased').show();

    var showDate = isAwarded;
    if ( showDate )
    {
        $('#changer-date-input').val( strDate );
        g_jdPickers['changer-date-input'].selectDate();
        $('#changer-date-input').parent().parent().children( "div.jdpicker-val" ).html( strDate );
        $('ul.floaty-dater li a').html( strDate );
        $('#changer-date').show();
        $('h1.floaty-dater').text( "Awarded Date" );
        $( "#date-changer" ).removeAttr( "reqid" );
    }
    $('.floaty-dater').toggle( showDate )
};

function updateFloaty( reqID )
{
    if ( reqID == null )
    {
        updateBadgeFloaty();
        return;
    }

    var youthID = g_currentUser;   // GUI operates on current user
    var flag = getRequirementStatus( youthID, reqID );
    var badgeID = deriveBadgeID( reqID );
    var isAwarded = isBadgeAwarded( youthID, badgeID );

    var strDate = formatDate( getServerTimestamp() );

    if ( isAwarded )
    {
        $('[action=floaty-complete-all]').removeClass( 'checked' ).attr( 'disabled', true );
        $('[action=floaty-award-all]').addClass( 'checked' );
    }
    else if ( isBadgeComplete( youthID, badgeID ) )
    {
        $('[action=floaty-complete-all]').addClass( 'checked' ).removeAttr( 'disabled' );
        $('[action=floaty-award-all]').removeClass( 'checked' );
    }
    else
    {
        $('[action=floaty-complete-all]').removeClass( 'checked' ).removeAttr( 'disabled' );
        $('[action=floaty-award-all]').removeClass( 'checked' );
    }

    if ( isAwarded )
    {
        $('[action=floaty-complete]').removeClass( 'checked' ).attr( 'disabled', true );
        $('[action=floaty-ready]').removeClass( 'checked' ).attr( 'disabled', true );
        $('[action=floaty-tally]').attr( 'disabled', true );
        $('[action=floaty-favourite]').removeClass( 'checked' ).attr( 'disabled', true );
    }
    else if ( flag == "complete" || flag == "implicit" )
    {
        $('[action=floaty-complete]').addClass( 'checked' ).removeAttr( 'disabled' );
        $('[action=floaty-ready]').removeClass( 'checked' ).attr( 'disabled', true );
        $('[action=floaty-favourite]').removeClass( 'checked' ).attr( 'disabled', true );
        if ( flag == "complete" )
        {
            strDate = formatDate( g_setMarkedRequirements[youthID][reqID].when );
            $('[action=floaty-tally]').attr( 'disabled', true );
        }
        else
            $('[action=floaty-tally]').removeAttr( 'disabled' );
    }
    else if ( flag == "ready" )
    {
        $('[action=floaty-complete]').removeClass( 'checked' ).removeAttr( 'disabled' );
        $('[action=floaty-ready]').addClass( 'checked' ).removeAttr( 'disabled' );
        $('[action=floaty-tally]').removeAttr( 'disabled' );
        $('[action=floaty-favourite]').removeClass( 'checked' ).attr( 'disabled', true );
    }
    else
    {
        $('[action=floaty-complete]').removeClass( 'checked' ).removeAttr( 'disabled' );
        $('[action=floaty-ready]').removeClass( 'checked' ).removeAttr( 'disabled' );
        $('[action=floaty-tally]').removeAttr( 'disabled' );
        $('[action=floaty-favourite]').removeAttr( 'disabled' );

        if ( flag == "favourite" )
            $('[action=floaty-favourite]').addClass( 'checked' );
        else
            $('[action=floaty-favourite]').removeClass( 'checked' );
    }

    updateFloatyTally( reqID );

    // parents and non-badgemasters can't complete reqs
    if ( g_mode == 'group' && getRole() != "v" && getRole() != "i" )
        $('[action=floaty-complete]').attr( 'disabled', true );

    $('.badgebased' ).hide();
    $('.reqbased' ).show();

    var showDate = ! isAwarded && flag == "complete" && (g_mode == "handbook" || getRole() == "v" || getRole() == "i");
    if ( showDate )
    {
        $('#changer-date-input').val( strDate );
        g_jdPickers['changer-date-input'].selectDate();
        $('#changer-date-input').parent().parent().children( "div.jdpicker-val" ).html( strDate );
        $('ul.floaty-dater li a').html( strDate );
        $('#changer-date').show();
        $('h1.floaty-dater').text( "Completed Date" );
        $( "#date-changer" ).attr( "reqid", reqID );
    }
    $('.floaty-dater').toggle( showDate )
};

function doUpdateJoinedDate()
{
    var timestamp = g_jdPickers['joined-changer-date-input'].stringToDate( $('#joined-changer-date-input').val() ).getTime();
    $('.current-date').html( formatDate( timestamp ) );
    restorePage();
};

function doUpdateCompletedDate()
{
    var strTimestamp = g_jdPickers['changer-date-input'].stringToDate( $('#changer-date-input').val() ).getTime();
    var reqID = $( "#date-changer" ).attr( "reqid" );
    var youthID = g_currentUser;
    if ( reqID == null )
    {
        var badgeID = g_currentBadgeID;
        console_log( "ready to update awarded date for " + badgeID );
        var strNotes = "";
        delete g_setAwardedBadges[youthID][badgeID]; // have to clear this otherwise the data won't get queued
        addAwarded( youthID, badgeID, strNotes, strTimestamp, getLoginID(), true );     // use the new date
        queueTransaction( youthID, badgeID, STATE_SET, FLAG_AWARDED, strNotes, true, strTimestamp );
        populateAlphabetic( g_filter, false, false );
        updateCategory( getBadgeCategory(badgeID) );
    }
    else
    {
        console_log( "updating the requirement complete date for " + reqID );
        var strNotes = "";
        delete g_setMarkedRequirements[youthID][reqID]; // have to clear this otherwise the data won't get queued
        addRequirementComplete( youthID, reqID, "", true, true, strTimestamp, getLoginID(), true );
        processDirtyFlags( youthID, true, true, true );
    }
    restorePage();
    setCurrentBadge( g_currentBadgeID, false, null );       // cause the current page to be rebuilt
};

function clearCurrentRequirementHighlighting()
{
    hideFloaty();
    $('.curreq').removeClass('curreq');
    g_currentReq = null;
};

function updateSmallCounts( mapCounts, attribute )
{
    var youthID = g_currentUser;        // GUI operates on current user

    for ( var key in mapCounts[youthID] )
    {
        var id = key.toLowerCase().replace( / /g, "" );
        var nCount = mapCounts[youthID][key];

        var small = $("small[" + attribute + "=" + id + "]");
        small.html( "" + nCount + "<img style='vertical-align:-3px;padding-left:0px;padding-right:2px;' src='" + BIMG + "/images/check.gif'/>" );
        small.toggle( nCount > 0 );
    }
}
function updateCategoryCounts()
{
    updateSmallCounts( g_mapCategoryCounts, "category" );
};

function updateTypeCounts()
{
    updateSmallCounts( g_mapTypeCounts, "type" );
};

function updateCategory( category )
{
    var youthID = g_currentUser;   // GUI operates on current user

    var showIcons = false;
    for ( var badgeID in g_dbTables['MetaData'] )
    {
        var debug = isBadgeOfInterest( badgeID );
        // SCOUT specific
        if ( getBadgeCategory(badgeID) == category || category == "Chief Scout's" && badgeID == "firstaid" )
        {
            var flag = getBadgeStatus( youthID, badgeID );
            var link = $("a[badgeid=" + badgeID + "]");
            var img = $("a[badgeid=" + badgeID + "] img.glyph");

            var src = null;
            if ( img.length == 1 )
                src = img.attr('src');

            if ( src )
                src = src.replace( /(.+)\//, "" );
            var oldFlag = g_mapGlyphCategories[src];

            // is there anything to update?
            if ( img.length > 0 || link.length > 0 )
            {
                if ( flag == "awarded" )
                {
                    if ( oldFlag != flag )
                    {
                        link.addClass( "complete" );
                        img.attr( 'src', BIMG + '/images/check.gif' ).css( 'vertical-align', '-2px' ).css( 'margin-right', '8px' );
                    }
                    showIcons = true;
                }
                else if ( flag == "complete" )
                {
                    if ( oldFlag != flag )
                    {
                        link.addClass( "complete" );
                        img.attr( 'src', BIMG + '/images/unawarded.gif' ).css( 'vertical-align', '-2px' ).css( 'margin-right', '8px' );
                    }
                    showIcons = true;
                }
                else if ( flag == "ready" )
                {
                    if ( oldFlag != flag )
                    {
                        link.removeClass( "complete" );
                        img.attr( 'src', BIMG + '/images/readybadge.gif' ).css( 'vertical-align', '-2px' ).css( 'margin-right', '8px' );
                    }
                    showIcons = true;
                }
                else if ( flag == "favourite" )
                {
                    if ( oldFlag != flag )
                    {
                        link.removeClass( "complete" );
                        img.attr( 'src', BIMG + '/images/star.gif' ).css( 'vertical-align', '-2px' ).css( 'margin-right', '4px' );
                    }
                    showIcons = true;
                }
                else 
                {
                    if ( oldFlag === undefined || oldFlag !=  "incomplete" )
                    {
                        link.removeClass( "complete" );
                        img.attr( 'src', BIMG + '/images/blank.gif' ).css( 'margin-right', '24px' ).css( 'margin-top', '0px' );
                    }
                }
            }

            annotatePercentage( badgeID );
        }
    }

    var categoryID = category.toLowerCase().replace( /[ ']/g, "" );
    var selector = "#" + categoryID + "-category-" + SECTION_TYPE;
    $( selector + " div.page-content .glyph" ).toggle( showIcons );
};

function markFavourite()
{
    var youthID = g_currentUser;
    var reqID = g_currentReq;
    clearCurrentRequirementHighlighting();

    if ( ! ensureUpdatable() )
        return;

    if ( ! ensureLoggedIn() )
        return;

    // don't allow toggling of favourite flag for completed requirements
    if ( isBadgeAwarded( youthID, deriveBadgeID( reqID ) ) )
    {
        openLightBox( { text: "This badge has already been awarded.", canClose: true } );
        return;
    }

    var flag = getRequirementStatus( youthID, reqID );
    if ( flag == "complete" || flag == "implicit" )
    {
        openLightBox( { text: "You cannot bookmark this requirement. It is already complete.", canClose: true } );
        return;
    }

    if ( flag == "ready" )
    {
        openLightBox( { text: "You cannot bookmark this requirement.  It is already ready to test.", canClose: true } );
        return;
    }

    if ( flag == "favourite" )
    {
        var strNotes = "";
        clearBookmark( youthID, reqID, strNotes, true );
        queueTransaction( youthID, reqID, STATE_CLEAR, FLAG_BOOKMARK, strNotes, true, getServerTimestamp() );
    }
    else
    {
        var strNotes = "";
        addBookmark( youthID, reqID, strNotes, getServerTimestamp(), getLoginID(), true );
        queueTransaction( youthID, reqID, STATE_SET, FLAG_BOOKMARK, strNotes, true, getServerTimestamp() );
    }

    // don't do this all the time, as there can be multiple calls to this routine
    // record this update
    persistScorecarding();

    updateFloaty( reqID );
    updateSelection( reqID );

    populateAlphabetic( g_filter, false, false );
    updateCategory( getBadgeCategory(deriveBadgeID( reqID )) );
    updateBookmarkCounts();

    $('[action=floaty-favourite]').removeClass( "active" );
};

function markComplete()
{
    var youthID = g_currentUser;   // GUI operates on current user
    var reqID = g_currentReq;
    var badgeID = deriveBadgeID( reqID );
    clearCurrentRequirementHighlighting();

    if ( ! ensureUpdatable() )
        return;

    if ( ! ensureLoggedIn() )
        return;

    // parents and non-badgemasters can't complete reqs
    if ( g_mode == 'group' && getRole() != "v" && getRole() != "i" )
    {
        openLightBox( { text: "Sorry, you don't have permission to do that.", canClose: true } );
        return;
    }

    if ( isBadgeAwarded( youthID, badgeID ) )
    {
        openLightBox( { text: "This badge has already been awarded.", canClose: true } );
        return;
    }

    var strStatus = getRequirementStatus( youthID, reqID );
    if ( strStatus == "complete" )
    {
        var strNotes = "";
        clearRequirementComplete( youthID, reqID, "", true, true, true );
        processDirtyFlags( youthID, true, true, true );
    }
    else if ( strStatus == "implicit" )
    {
        openLightBox( { text: "This requirement is automatically marked as complete because all the specified conditions are met.", canClose: true, size: "big" } );
        return;
    }
    else
    {
        if ( isAutoRequirement( reqID ) )
        {
            if ( ! confirm( "This requirement is automatically marked as complete when the specified conditions are met.  Override automatic detection and mark as complete anyway?" ) )
                return;
        }

        var strNotes = "";
        addRequirementComplete( youthID, reqID, "", true, true, getServerTimestamp(), getLoginID(), true );
        processDirtyFlags( youthID, true, true, true );
    }

    updateFloaty( reqID );
    updateSelection( reqID );

    // if a subreq'd req is marked as complete, we have to tweak the subreqs. It's easiest to do so
    // just by rebuilding the whole page
    if ( isSubReqd( reqID ) )
        if ( getCurrentPage().match( /^badge-details-/ ) )
            setCurrentBadge( g_currentBadgeID, false, null );

    populateAlphabetic( g_filter, false, false );
    updateCategory( getBadgeCategory(badgeID) );

    $('[action=floaty-complete]').removeClass( "active" );

    setBadgeStatusText( getCurrentDetailsPage(), youthID, badgeID );
    updateSnapshotTarget( youthID );
};

$(document).ready( function() {
    document.title = STR_TITLE;

    if ( g_isEmbedded )
    {
        $('div[data-role=header]').hide();
        $('body').css( 'background-color', g_isEmbeddedColor );
        gotoPage( '#upcoming' );
    }

    // SEA-SCOUTS specific terminology
    if ( SECTION_TYPE == "troop" && getLocalStorage( 'sea-scouts' ) == "true" )
    {
        STR_YOUTH = "Sea Scout",
        STR_YOUTHS = "Sea Scouts",
        STR_YOUTH_POSSESSIVE = "Sea Scout's",
        STR_YOUTHS_POSSESSIVE = "Sea Scouts'",
        STR_PATROL = "Boat's Crew",
        STR_PATROLS = "Boat's Crews"
        STR_LEADER = "Officer";
        STR_LEADERS = "Officers";
        STR_LEADERS_LC = "officers";
        STR_SECTION_TERSE = "Ship's";
        STR_SECTION = "Ship's Company";
        STR_SECTION_POSSESSIVE = "Ship's Company's";
        ROLES = [
            {long:"Able Hand", short:"Able Hand", tag:""},
            {long:"Leading Seaman", short:"Leading Seaman", tag:"LS" },
            {long:"Coxswain", short:"Coxswain", tag:"C" }
        ];
        LEADER_ROLES = [
            {long:"Officer", short:"Officer", tag:"Off." },
            {long:"Skipper", short:"Skipper", tag:"Skip."}
        ];
        for ( var i = 0; i < LIST_INVENTORY_INCLUSIONS.length; i++ )
        {
            if ( LIST_INVENTORY_INCLUSIONS[i] == "extra_slide" )
                LIST_INVENTORY_INCLUSIONS[i] = "extra_slide_sea";
            if ( LIST_INVENTORY_INCLUSIONS[i] == "extra_slide_apl" )
                LIST_INVENTORY_INCLUSIONS[i] = "extra_slide_apl_sea";
            if ( LIST_INVENTORY_INCLUSIONS[i] == "extra_slide_pl" )
                LIST_INVENTORY_INCLUSIONS[i] = "extra_slide_pl_sea";
            if ( LIST_INVENTORY_INCLUSIONS[i] == "extra_epaulet" )
                LIST_INVENTORY_INCLUSIONS[i] = "extra_epaulet_sea";
            if ( LIST_INVENTORY_INCLUSIONS[i] == "extra_epaulet_apl" )
                LIST_INVENTORY_INCLUSIONS[i] = "extra_epaulet_apl_sea";
            if ( LIST_INVENTORY_INCLUSIONS[i] == "extra_epaulet_pl" )
                LIST_INVENTORY_INCLUSIONS[i] = "extra_epaulet_pl_sea";
        }
        delete MAP_INVENTORY_LABELS["extra_slide"];
        delete MAP_INVENTORY_LABELS["extra_slide_apl"];
        delete MAP_INVENTORY_LABELS["extra_slide_pl"];
        delete MAP_INVENTORY_LABELS["extra_epaulet"];
        delete MAP_INVENTORY_LABELS["extra_epaulet_apl"];
        delete MAP_INVENTORY_LABELS["extra_epaulet_pl"];
        MAP_INVENTORY_LABELS["extra_slide_sea"] = "Able Hand Slide";
        MAP_INVENTORY_LABELS["extra_slide_apl_sea"] = "Leading Seaman Slide";
        MAP_INVENTORY_LABELS["extra_slide_pl_sea"] = "Coxswain Slide";
        MAP_INVENTORY_LABELS["extra_epaulet_sea"] = "Able Hand Epaulet";
        MAP_INVENTORY_LABELS["extra_epaulet_apl_sea"] = "Leading Seaman Epaulet";
        MAP_INVENTORY_LABELS["extra_epaulet_pl_sea"] = "Coxswain Epaulet";
    }

    STRING_LOOKUPS = {
        SECTION_TYPE: SECTION_TYPE,
        SECTION: STR_SECTION,
        SECTION_LC: STR_SECTION.toLowerCase(),
        SECTION_TERSE: STR_SECTION_TERSE,
        SECTION_POSSESSIVE: STR_SECTION_POSSESSIVE,
        LEADER: STR_LEADER,
        LEADERS: STR_LEADERS,
        LEADERS_LC: STR_LEADERS_LC,
        YOUTH: STR_YOUTH,
        YOUTHS: STR_YOUTHS,
        PATROL: STR_PATROL,
        PATROLS: STR_PATROLS,
        YOUTH_POSSESSIVE: STR_YOUTH_POSSESSIVE,
        YOUTHS_POSSESSIVE: STR_YOUTHS_POSSESSIVE,
        PRODUCT: STR_PRODUCT,
        ORGANIZATION: STR_ORGANIZATION,
        COUNCIL: STR_COUNCIL,
        COUNCILS: STR_COUNCILS,
        AREA: STR_AREA,
        AREAS: STR_AREAS,
        HOST: getHost()
    };

    // build up a list of the elements
    $('div[data-role=page]').each( function() {
        replaceSectionStrings( $(this), "#" + $(this).attr( 'id' ) );
    } );

    // the floaty div doesn't have an id attribute, and doesn't need as much processing
    replaceSectionStrings( $('.floaty'), ".floaty" );

    // strip out the DIV's that don't interest us
    $.each( ["troop", "pack", "company", "colony", "unit", "troop_au", "colony_uk"], function( i, strSection ) {
        var strClass = strSection + "-only";
        if ( strSection != SECTION_TYPE )
            $("div." + strClass + "[data-role=page]").remove();
        else
            $("div." + strClass + "[data-role=page]").removeClass( strClass );
    });

    $( ".recordsheets" ).toggle( SECTION_TYPE != "troop_au" );

    $("#tax-link").toggle( PROGRAM_COSTS );

    // put in elements to support missing placeholders in Firefox and IE
    $("input[placeholder][ff]").each( function() {
        firefoxPrependPlaceholder( $(this) );
    });

    for ( var i in CATEGORIES )
        g_categories[CATEGORIES[i]] = 1;

    initAbbreviations();

    toggleSwitch( "#pref-mobile", location.href.match( /mobile/ ) );

    if ( jQT ) {
        jQT.addAnimation({
            name: 'slideback',
            selector: 'a[data-icon=arrow-l]'
        });
    }

    g_creole = new Parse.Simple.Creole( { 
        forIE: document.all,
        interwiki: {
            WikiCreole: 'http://www.wikicreole.org/wiki/',
            Wikipedia: 'http://en.wikipedia.org/wiki/'
        },
        linkFormat: ''
    } );
});

function initializeDocument()
{
    // there are some section-specific elements which are hidden by default
    changeOrientation();

    if ( g_isMobileOS )
    {
        //console_log( "mobile device with dimensions (" +
            //document.documentElement.clientWidth + "," +
            //document.documentElement.clientHeight + ")" );
    }

    if ( g_isiPad )
    {
        console_log( "resizing?" );
        $('body').width( isLandscape() ? 1024 : 768 );
        $('body').height( isLandscape() ? 768 : 1024 );
    }

    // disable report scrolling for mobile devices
    if ( g_isTouchOS )
        $('#table-report-body').css( 'overflow-x', 'visible' );

    setOnline( g_isServerReachable );

    if ( isOnline() )
    {
        openLightBox( { text: "Updating application..." } );
        initializeIfReady();
    }
    else
        window.setTimeout( initialize, 1000 );

    $('div[data-role=page]').live( 'pageAnimationEnd', function( evt, data ) {
        moveSidebar();
    });

    $('#view-event').bind( 'pageAnimationEnd', function( evt, data ) {
        g_allowSwipe = true;
    } );
    $('#view-event').bind( 'pageAnimationStart', function( evt, data ) {
        g_allowSwipe= false;
    } );
    $('#badge-details-0, #badge-details-1').live( 'pageAnimationEnd', function( evt, data ) {
        clearCurrentRequirementHighlighting();
        g_allowSwipe = true;
    });
    $('#badge-details-1, #badge-details-1').live( 'pageAnimationStart', function( evt, data ) {
        clearCurrentRequirementHighlighting();
        g_allowSwipe = false;
    });

    $('#view-event').bind( 'swipe', function(evt, data) {                
        if ( ! g_allowSwipe )
            ;   // ignore, for now
        else if ( data.direction == "right" ) {
            // the timer is necessary because jqt crashes mobile safari during a swipe
            window.setTimeout( function() { 
                doViewEvent(g_listEvents[g_iEvent-1]);
            }, 100 );
        } else if ( data.direction == "left" ) {
            // the timer is necessary because jqt crashes mobile safari during a swipe
            window.setTimeout( function() { 
                doViewEvent(g_listEvents[g_iEvent+1]);
            }, 100 );
        } else {
            alert( "swipe undefined" );
        }
    } );
    $('#badge-details-0, #badge-details-1').live( 'swipe', function(evt, data) {                
        clearCurrentRequirementHighlighting();
        if ( ! g_allowSwipe )
            ;   // ignore, for now
        else if ( data.direction == "right" ) {
            // the timer is necessary because jqt crashes mobile safari during a swipe
            window.setTimeout( function() { 
                setCurrentBadge( g_prevAlphabetic[g_currentBadgeID], true, "slideback" );
            }, 100 );
        } else if ( data.direction == "left" ) {
            // the timer is necessary because jqt crashes mobile safari during a swipe
            window.setTimeout( function() { 
                setCurrentBadge( g_nextAlphabetic[g_currentBadgeID], true, "slide" );
            }, 100 );
        } else {
            alert( "swipe undefined" );
        }
    });

    $( "." + SECTION_TYPE + "-only" ).show();

    // the edit-event page has a jdPicker object that needs to be initialzed
    $('#event-date-input').jdPicker( {
        show_clearer: 0,
        show_closer: 1,
        show_inline: 1,
        start_of_week: START_OF_WEEK,
        date_format: DATE_FORMAT
    });
    // the bulk edit page has a jdPicker object that needs to be initialzed
    $('#table-report-date-input').jdPicker( {
        show_clearer: 0,
        show_closer: 1,
        show_inline: 1,
        start_of_week: START_OF_WEEK,
        date_format: DATE_FORMAT
    });
    // the ready-to-award report has a jdPicker object that needs to be initialzed
    $('#report-date-input').jdPicker( {
        show_clearer: 0,
        show_closer: 1,
        show_inline: 1,
        start_of_week: START_OF_WEEK,
        date_format: DATE_FORMAT
    });
    // the date-changer page has a jdPicker object that needs to be initialzed
    $('#changer-date-input').jdPicker( {
        show_clearer: 0,
        show_closer: 0,
        show_inline: 1,
        auto_close: 0,
        start_of_week: START_OF_WEEK,
        date_format: DATE_FORMAT
    });
    // the youth-date-changer page has a jdPicker object that needs to be initialzed
    $('#joined-changer-date-input').jdPicker( {
        show_clearer: 0,
        show_closer: 0,
        show_inline: 1,
        auto_close: 0,
        start_of_week: START_OF_WEEK,
        date_format: DATE_FORMAT
    });
    $(".jdpicker_w").hide();

    $('#edit-event').bind( 'pageAnimationEnd', function( evt, data ) {
        togglePicker( false, "#event-date" );
    });
    $('#table-report').bind( 'pageAnimationEnd', function( evt, data ) {
        togglePicker( false, "#table-report-date" );
    });
    $('#report').bind( 'pageAnimationEnd', function( evt, data ) {
        togglePicker( false, "#report-date" );
    });
    $('#date-changer').bind( 'pageAnimationEnd', function( evt, data ) {
        togglePicker( false, "#changer-date" );
    });
    $('#youth-date-changer').bind( 'pageAnimationEnd', function( evt, data ) {
        togglePicker( false, "#joined-changer-date" );
    });

    $("#edit-login .welcome-custom").parent().find( "textarea" ).bind('input paste', function() { toggleSwitch( "#edit-login .welcome-custom", false ) } );
    $("#edit-youth .welcome-custom").parent().find( "textarea" ).bind('input paste', function() { toggleSwitch( "#edit-youth .welcome-custom", false ) } );
    $("#edit-leader .welcome-custom").parent().find( "textarea" ).bind('input paste', function() { toggleSwitch( "#edit-leader .welcome-custom", false ) } );

    if ( ! g_isFirefox )
    {
        $('input[type=email]').each( function() {
            fixCopyPaste( $(this) );
        });
        $('input[type=text]').each( function() {
            fixCopyPaste( $(this) );
        });
        $('input[type=password]').each( function() {
            fixCopyPaste( $(this) );
        });
        $('textarea').each( function() {
            fixCopyPaste( $(this) );
        });
    }
    else
        $("#firefox-stylesheet").attr( "href", BIMG + "/style-firefox.css" );  // add the quirks

    if ( g_isIE ) 
    {
        $("#event-date .jdpicker-val").bind( "click", function() { togglePicker( true, "#event-date" ) } );
        $("#table-report-date .jdpicker-val").bind( "click", function() { togglePicker( true, "#table-report-date" ) } );
        $("#report-date .jdpicker-val").bind( "click", function() { togglePicker( true, "#report-date" ) } );
        $("#changer-date .jdpicker-val").bind( "click", function() { togglePicker( true, "#changer-date" ) } );
        $("#joined-changer-date .jdpicker-val").bind( "click", function() { togglePicker( true, "#joined-changer-date" ) } );
    }

    if ( isMobileDevice() )
    {
        $('.no-mobile').hide();
        $('body').removeClass( "mousable" );
    }

    if ( getSteps().length == 0 )
        $('#nextsteps-link').hide();
};

function removeOfflineHeaderIcons()
{
    $('div[data-role=header] .offline-icon').remove();
};

function addOfflineHeaderIcons()
{
    var icon = "<img class='offline-icon' style='margin-top: 6px;' src='images/offline.gif'/>";
    if ( windowWidth() <= WIDE )
    {
        $('div[data-role=page]').each( function() { 
            var selector = '#' + $(this).attr('id');
            $(selector + ' div[data-role=header] h1').before( icon );
            positionOfflineIcon( selector );
        });
    }
};

function moveSidebar()
{
    var newPage = getCurrentPage();
    if ( $('#' + newPage + ' div.sidebar').length > 0 )
        return;

    // always make sure the sidebar is visible, if it exists
    $('#' + newPage + ' div.sidebar').show();

    positionOfflineIcon( "#" + newPage );

    var sidebar = $('.sidebar').detach();
    $('#' + newPage + ' div[data-role=content]').prepend( sidebar );

    $("#sidebar-links a[pageid]").removeClass( "selected" );
    if ( newPage == "startpage" || newPage == "menus" || newPage == "reports" || newPage == "upcoming" || newPage == "events"  )
        $("#sidebar-links a[pageid=" + newPage + "]").addClass( "selected" );

    restartPollInterval();
};

function togglePicker( bShow, selector )
{
    var el = $( selector + " .jdpicker_w" );
    el.toggle( bShow );
    el.parent().children( "div.jdpicker-val" ).toggle( ! bShow );
    if ( ! bShow )
        window.setTimeout( function() { el.parent().bind( "click", function() { togglePicker( true, selector ); } ) }, 100 );
    else
        el.parent().unbind( "click" );
};

function fixCopyPaste( el )
{
    el.bind( 'paste', function( e ) {
        var element = $(this).context;
       
        var text = $(this).val();
        var start = element.selectionStart;
        var pastedText = e.originalEvent.clipboardData.getData( 'text/plain' );
        $(this).val( text.substring( 0, element.selectionStart )
            +pastedText
            +text.substring( element.selectionEnd, text.length ) );
        element.selectionStart = start + pastedText.length;
        element.selectionEnd = element.selectionStart;
    });
};

function firefoxPrependPlaceholder( element )
{
    var inputTest = document.createElement('input');
    if ( 'placeholder' in inputTest )
        ; // browser supports placeholders
    else
    {
        var strPlaceholder = element.attr( 'placeholder' );
        element.parent().prepend( "<div class='firefox'>" + strPlaceholder + "</div>" );
    }
};

function gotoPage( selector, animation, noScroll )
{
    //console_log( "old page = " + getCurrentPage() + ", desired page = " + selector );
    clearActive();

    if ( g_navigationBlocker && ! checkEventNavigation( selector ) )
        return false;

    if ( ("#"+getCurrentPage()) != selector )
    {
        if ( ! noScroll )
            saveScroll();

        rehomeLightBox( selector );

        if ( jQT )
            jQT.goTo( selector );

        else
        {
            $('[data-role=page]').removeClass( "current" );
            $(selector).addClass( "current" );
            moveSidebar();
        }
    }

    return true;
};

function saveScroll()
{
    var newY = window.pageYOffset <= 0 ? 0 : window.pageYOffset;
    //console_log( "recorded scroll[" + getCurrentPage() + "] = " + newY );
    g_mapScrollPoints[getCurrentPage()] = newY;
};

function restoreScroll()
{
    //console_log( "restoring scroll[" + getCurrentPage() + "] = " + g_mapScrollPoints[getCurrentPage()] );
    window.scrollTo( 0, g_mapScrollPoints[getCurrentPage()] );
};

function clearActive()
{
    window.setTimeout( function() {
        $('.active').removeClass( 'active' );
    }, 500 );
};

function showHomeScreenReminder()
{
    if ( isOnline() && ! window.navigator.standalone )
        if ( isAppleMobileDevice() && ! g_noJQT )
            if ( location.href.match( /mobile/ ) )
                window.setTimeout( function() { openLightBox( { text: "To run this web app when you are not connected to the internet, you must launch it from your Home Screen.<p style='margin-top:10px;'>To add this web app to your Home Screen, click on the browser's bookmark button.", canClose: true, size: 'big-wide' } ); }, 500 );
            else if ( getLocalStorage( "mobile-hinted" ) == null )
            {
                window.setTimeout( function() { openLightBox( { text: "Do you want to be able to run this web app when you are not connected to the internet?<div class='button' style='padding-top:8px;padding-bottom:7px;margin-top:10px;' onclick='location.href=\"mobile\";'>Yes</div><div class='button' style='padding-top:8px;padding-bottom:7px;margin-top:10px;' onclick='g_locked=false;openLightBox({text:\"Hint: You can enable this ability later from the \\\"Account\\\" page.\", canClose: true});'>No</div>", canClose: true, size: 'big-wide' }) }, 500 );
                setLocalStorage( "mobile-hinted", "true" );
            }
};

function scheduleSync( syncCompletionMessage, delay, callback )
{
    if ( ! isOnline() )
        return;             // don't even try...

    workerPause();                              // suspend the next regularly scheduled polling, in favour of this on-demand sync

    if ( g_timerNextSync )
    {
        //console_log( "clearing next sync timer" );
        window.clearTimeout( g_timerNextSync );     // if we'd previously scheduled a on-demand sync, then replace it with this one
    }

    if ( callback === undefined )
        callback = function() { closeLightBox();console_log( syncCompletionMessage ) };

    g_timerNextSync = window.setTimeout( function() { 
        g_timerNextSync = null;
        ajaxSyncTransactions( true, callback );
    }, delay  );
};

function ajaxSyncTransactions( bFromGUI, callback )
{
    if ( g_mode == "handbook" || ( ! g_isEmbedded && ! getLoginID() ) )
    {
        if ( ! g_isInitialized )
        {
            g_fnWorkerCallback = null;
            // console_log( "not initialized... invoking callback" );
            callback();
        }

        closeLightBox( 1000 );
        return;
    }

    if ( g_fnWorkerCallback != null )
    {
        console_log( "ignoring re-entrant ajaxSyncTransactions '" + callback + "' because of extant callback '" + g_fnWorkerCallback + "'" );
        return;
    }
    else
        g_fnWorkerCallback = callback;

    if ( getSection() == null )
    {
        if ( ! getLoginID() )
            console_log( "not logged in" );
        else
            console_warn( "no sectionID yet" );

        callback();
        return;
    }

    // if we got this far, then we are apparently able to do a sync...

    // now do the sync
    workerSync( bFromGUI, "ajaxSyncTransactions" );
};

function getDisplayName( youth, youthID )
{
    if ( youth === undefined || youth == null )
    {
        console_trace( "wowot! youth " + youthID + " = " + youth )
        return "Unknown";
    }

    strName = youth.displayname;
    if ( strName === undefined || strName == null || strName == "" )
    {
        strName = youth.firstname;
        if ( strName === undefined || strName == null )
            strName = "";

        var strLastName = youth.lastname;
        if ( strLastName !== undefined && strLastName != null && strLastName != "" )
        {
            if ( strName.length > 0 )
                strName = strName + " " + strLastName;
            else
                strName = strLastName;
        }
    }

    return strName;
};

function toggleLeaderDividerStatus( containerID, theElement, callback )
{
    var flag = getLeaderDividerStatus( containerID );
    if ( flag == "complete" )
    {
        $( containerID + " li.selected[leaderid]:visible").each( function() {
            $(this).removeClass( "selected" );
        });
        $( containerID + " li.divider.selector img").attr( "src", BIMG + "/images/incomplete.gif" );
    }
    else
    {
        $( containerID + " li[leaderid]:visible").each( function() {
            $(this).addClass( "selected" );
        });
        $( containerID + " li.divider.selector img").attr( "src", BIMG + "/images/complete.gif" );
    }
    if ( callback )
    {
        $( containerID + " li[leaderid]").each( function() {
            var leaderID = $(this).attr('leaderid');
            updateLeaderParticipation( leaderID );
        });
    }
};

function togglePatrolDividerStatus( containerID, theElement, callback )
{
    var patrolID = theElement.getAttribute( "patrolid" );
    var flag = getPatrolDividerStatus( containerID, patrolID );

    if ( flag == "complete" )
    {
        $( containerID + " li.selected[youthid][patrolid=" + patrolID + "]:visible").each( function() {
            $(this).removeClass( "selected" );
        });
        $( containerID + " li.divider.selector[patrolid=" + patrolID + "] img").attr( "src", BIMG + "/images/incomplete.gif" );
    }
    else
    {
        $( containerID + " li[youthid][patrolid=" + patrolID + "]:visible").each( function() {
            $(this).addClass( "selected" );
        });
        $( containerID + " li.divider.selector[patrolid=" + patrolID + "] img").attr( "src", BIMG + "/images/complete.gif" );
    }

    if ( callback )
    {
        $( containerID + " li[youthid][patrolid=" + patrolID + "]").each( function() {
            var youthID = $(this).attr('youthid');
            updateYouthParticipation( youthID );
        });
    }
};

function getPatrolDividerStatus( containerID, patrolID )
{
    var nTotal = 0;
    $( containerID + " li[youthid][patrolid=" + patrolID + "]").each( function() {
        if ( $(this).css('display') != 'none' ) 
            nTotal++;
    } );

    var nSelected = 0;
    $( containerID + " li.selected[youthid][patrolid=" + patrolID + "]").each( function() {
        if ( $(this).css('display') != 'none' ) 
            nSelected++;
    } );

    if ( nSelected == 0 )
        return "incomplete";
    else if ( nTotal == nSelected )
        return "complete";

    return "partial";
};

function getLeaderDividerStatus( containerID )
{
    var nTotal = 0;
    $( containerID + " li[leaderid]").each( function() {
        if ( $(this).css('display') != 'none' ) 
            nTotal++;
    } );

    var nSelected = 0;
    $( containerID + " li.selected[leaderid]").each( function() {
        if ( $(this).css('display') != 'none' ) 
            nSelected++;
    } );

    if ( nSelected == 0 )
        return "incomplete";
    else if ( nTotal == nSelected )
        return "complete";

    return "partial";
};

function toggleYouthSelection( containerID, youthID )
{
    clearActive();

    $( containerID + " li[youthid=" + youthID + "]").toggleClass("selected");

    if ( g_mode != "handbook" )
    {
        if ( g_dbTables['Youth'][youthID] !== undefined )
            setPatrolDividerStatus( containerID, g_dbTables['Youth'][youthID].patrolid );
        else
            console_trace( "wowot! no youth data for '" + youthID + "'" );
    }
};

function toggleLeaderSelection( containerID, leaderID )
{
    clearActive();

    $( containerID + " li[leaderid=" + leaderID + "]").toggleClass("selected");

    if ( g_mode != "handbook" )
    {
        if ( g_dbTables['Leaders'][leaderID] !== undefined )
            setLeaderDividerStatus( containerID );
        else
            console_trace( "wowot! no leader data for '" + leaderID + "'" );
    }
};

function setLeaderDividerStatus( containerID )
{
    $( containerID + " li.divider.selector img").attr( "src", BIMG + "/images/" + getLeaderDividerStatus( containerID ) + ".gif" );
};

function setPatrolDividerStatus( containerID, patrolID )
{
    $( containerID + " li.divider.selector[patrolid=" + patrolID + "] img").attr( "src", BIMG + "/images/" + getPatrolDividerStatus( containerID, patrolID ) + ".gif" );
};

function makePatrolDivider( containerID, patrolID, callback )
{
    var li = null;
    if ( containerID != null )      // clickable dividers have non-null containers
    {
        var strCallback = callback;
        if ( strCallback )
             strCallback = "\"" + strCallback + "\"";

        li = $( "<li class='divider' patrolid=" + patrolID + " onclick='togglePatrolDividerStatus(\"" + containerID + "\", this, " + strCallback + ");'></li>" );
        li.addClass( "selector" );
        li.html( "<img glyph src='" + BIMG + "/images/incomplete.gif'/>" + getPatrolName( patrolID, true ) );
    }
    else
    {
        li = $( "<li class='divider' patrolid=" + patrolID + ">" + getPatrolName( patrolID, true ) + "</li>" );
    }

    return li;
};

function makeLeaderDivider( containerID, callback )
{
    var strCallback = callback;
    if ( strCallback )
         strCallback = "\"" + strCallback + "\"";

    var li = $( "<li class='divider' onclick='toggleLeaderDividerStatus(\"" + containerID + "\", this, " + strCallback + ");'></li>" );
    li.addClass( "selector" );
    li.html( "<img glyph src='" + BIMG + "/images/incomplete.gif'/>Leadership Team" );

    return li;
};

function addPatrolRows( patrol, prepend )
{
    var patrolID = patrol.id;

    if ( patrolID != 0 )
    {
        var htmlRow = '<tr patrolid=' + patrolID + '><td><img src="' + BIMG + '/images/delete.gif" onclick="doDeletePatrol(' + patrolID + ');"/></td><td><ul class="rounded edit"><li class="input"><input type="text" placeholder="' + STR_PATROL + ' Name" patrolid=' + patrolID + ' value="' + (patrol.name===undefined||patrol.name==null?"":patrol.name) + '"/></li></ul></td></tr>';
        if ( prepend )
            $("#list-patrols-manage").prepend( htmlRow );
        else
            $("#list-patrols-manage").append( htmlRow );

        var element = $('#list-patrols-manage tr[patrolid=' + patrolID + '] input[placeholder]' );

        firefoxPrependPlaceholder( element );

        // make sure the original value of the patrol name is recorded, in case we have to roll back later
        element.attr( "origval", element.val() );
    }

    var strPatrolName = getPatrolName( patrolID, true );

    var liPatrol = document.createElement('li');
    liPatrol.className = "selection";
    liPatrol.innerHTML = "<a href='javascript:void(0)' onclick='doSetPatrol(\"" + patrolID + "\");restorePage();'>" + strPatrolName + "</a>";
    liPatrol.setAttribute( 'patrolid', patrolID );
    if ( prepend )
        $("#list-patrols").prepend( liPatrol );
    else
        $("#list-patrols").append( liPatrol );
};

function doEditGroup()
{
    $('form[name=group-edit] input[name=edit-account-group]').val( getSectionGroup() );
    $('form[name=group-edit] input[name=edit-account-subgroup]').val( getSectionSubGroup() );

    var isGroup = getRole() == "v";

    $('#paypal option').each( function() {
        var id = $(this).val();
        $(this).html( PRICING[id] );
    });

    $( "#group-details div[data-role=header] h1" ).text( isGroup ? "Account Details" : "Upgrade" );
    $( "#access-header + ul" ).css( "margin-top", isGroup ? "0" : "20px" );
    var button = $( "#group-details div[data-role=header] h1 + a" );
    button.attr( "data-icon", isGroup ? "delete" : "arrow-l" );
    button.text( isGroup ? "Cancel" : "Account" );

    addPage( "#group-details" );

    ajaxGetCouncils();
};

function ajaxGetCouncils()
{
    if ( SECTION_TYPE == "unit" )
    {
        $('input[name=account-subgroup]').parent().hide();
        $('input[name=edit-account-subgroup]').parent().hide();
    }

    if ( g_dbTables['Areas'] === undefined || g_dbTables['Councils'] === undefined )
    {
        jQuery.ajax( {
            url: BADGES_TABLES,
            data: "uid=" + getLoginID() + "&sectionid=0&sandbox=" + (PAYPAL_SANDBOX?1:0) + "&worksheet=Council,Area",
            error: function( request, textStatus, errorThrown ) {
                setOnline( false );
                restorePage();
                openLightBox( { text: "Error fetching " + STR_COUNCILS + " while off-line.", canClose: true } );
            },
            success: function( data ) 
            {
                setOnline( true ); 
                g_dbTables['Councils'] = data['Councils'];
                g_dbTables['Areas'] = data['Areas'];

                var listCouncils = $('#list-councils');
                listCouncils.empty();

                var listCouncilKeys = sortTableKeysByDisplayName( g_dbTables['Councils'] );
                for ( var iCouncil = 0; iCouncil < listCouncilKeys.length; iCouncil++ )
                {
                    var council = g_dbTables['Councils'][listCouncilKeys[iCouncil]];
                    var councilID = council.id;
                    var strName = council.displayname;

                    var li = document.createElement('li');
                    li.className = "selection";
                    li.setAttribute( "councilid", councilID );
                    li.innerHTML = "<a href=\"javascript:void(0);\" onclick=\"setCurrentCouncil( '" + councilID + "' );restorePage();\">" + htmlEncode(strName) + "</a>";
                    listCouncils.append( li );
                }

                resetCouncilAreas();
            }
        });
    }
    else
        resetCouncilAreas();
};

function resetCouncilAreas()
{
    var area = g_dbTables['Areas'][getSectionArea()];
    setCurrentCouncil( area === undefined ? -1 : area.councilid );
}

function setCurrentArea( areaID )
{
    $('#list-areas li').removeClass( "checked" );

    if ( g_dbTables['Areas'] !== undefined )
    {
        var area = g_dbTables['Areas'][areaID];
        if ( area !== undefined )
        {
            $('#list-areas li[areaid=' + areaID + ']').addClass( "checked" );
            $('.current-area').html( htmlEncode( area.displayname ) );
            return;
        }
    }

    $('.current-area').html( "No selection" );
}

function setCurrentCouncil( councilID )
{
    $('#list-councils li').removeClass( "checked" );
    setCurrentArea( -1 );

    if ( g_dbTables['Councils'] === undefined )
    {
        $('.current-council').html( "No selection" );
        return;
    }

    var council = g_dbTables['Councils'][councilID];

    if ( council !== undefined )
    {
        $('#list-councils li[councilid=' + councilID + ']').addClass( "checked" );
        $('.current-council').html( g_dbTables['Councils'][councilID].displayname );

        var listAreas = $('#list-areas');
        listAreas.empty();

        var mapAreas = {};
        for ( var areaID in g_dbTables['Areas'] )
        {
            var area = g_dbTables['Areas'][areaID];
            if ( area.councilid == councilID )
                mapAreas[areaID] = area;
        }


        var listAreaKeys = sortTableKeysByDisplayName( mapAreas );
        for ( var iArea = 0; iArea < listAreaKeys.length; iArea++ )
        {
            var area = g_dbTables['Areas'][listAreaKeys[iArea]];

            var areaID = area.id;
            var strName = area.displayname;

            var li = document.createElement('li');
            li.setAttribute( "areaid", areaID );
            li.className = "selection";

            li.innerHTML = "<a href=\"javascript:void(0);\" onclick=\"setCurrentArea( '" + areaID + "' );restorePage();\">" + htmlEncode(strName) + "</a>";
            listAreas.append( li );

            if ( areaID == getSectionArea() )
                setCurrentArea( areaID );
        }

        $('#paypal').attr( "action", PAYPAL_SANDBOX ? "https://www.sandbox.paypal.com/cgi-bin/webscr" : "https://www.paypal.com/cgi-bin/webscr" );
        $('input[name=hosted_button_id]').val( council.btnid );
    }

    else
        $('.current-council').html( "No selection" );
};

function gotoAreaPicker()
{
    if ( $("#list-councils li.checked").length == 1 )
        addPage( "#area-picker" );
    else
        openLightBox( { text: "Please select a council first.", canClose: true } );
};


function doUpdateGroup()
{
    var strGroup = trim( $('form[name=group-edit] input[name=edit-account-group]').val() );
    var strSubGroup = trim( $('form[name=group-edit] input[name=edit-account-subgroup]').val() );

    if ( strGroup == "" )
    {
        openLightBox( { text: "You must provide a group name.", canClose: true } );
        return;
    }

    // is there a better test?
    if ( strGroup.length < 4 )
    {
        openLightBox( { text: "That is not a valid group name.", canClose: true } );
        return;
    }

    if ( $('#list-councils li.checked').length == 0 )
    {
        openLightBox( { text: "You must select your council.", canClose: true } );
        return;
    }

    if ( $('#list-areas li.checked').length == 0 )
    {
        openLightBox( { text: "You must select your area.", canClose: true } );
        return;
    }

    ajaxUpdateGroup( strGroup, strSubGroup, $('#list-areas li.checked').attr( "areaid" ) );
};

function ajaxUpdateGroup( strGroup, strSubGroup, areaID )
{
    var groupRecord = {
        group: strGroup,
        subgroup: strSubGroup,
        areaid: areaID
    };

    var strAction = "select=id.eq." + getSection() + "&update=" + escape(JSON.stringify( groupRecord ));

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&worksheet=Section&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "section record updated" );
            openLightBox( { text: STR_SECTION + " updated", timeout: 1000 } );

            setSectionGroup( strGroup );
            setSectionSubGroup( strSubGroup );
            setSectionArea( areaID );

            updateGroupDisplays( strGroup, strSubGroup );
        }
    });

    restorePage();
};

function sortPatrols()
{
    var listSortedPatrols = new Array();
    var mapPatrolByName = {};

    for ( var patrolID in g_dbTables['Patrols'] )
    {
        var patrol = g_dbTables['Patrols'][patrolID];

        var strName = patrol.name;
        if ( strName === undefined || strName == null )
            strName = "";
        strName = strName.toLowerCase();

        listSortedPatrols.push( strName );      // append this name to the end of a simple list
        mapPatrolByName[strName] = patrol;
    }

    listSortedPatrols.sort();                   // sort, alphabetically

    // find the unassigned patrol, and make sure it is always at the end
    var strUnassignedName = g_dbTables['Patrols'][0].name.toLowerCase();
    for ( var iPatrol = 0; iPatrol < listSortedPatrols.length; iPatrol++ )
    {
        if ( listSortedPatrols[iPatrol] == strUnassignedName )
        {
            listSortedPatrols.splice( iPatrol, 1 );
            listSortedPatrols.push( strUnassignedName );    // tack on the end
            break;
        }
    }

    var listSortedPatrolIDs = new Array();
    for ( var iPatrol = 0; iPatrol < listSortedPatrols.length; iPatrol++ )
    {
        var patrol = mapPatrolByName[listSortedPatrols[iPatrol]];
        listSortedPatrolIDs.push( patrol.id );
    }

    return listSortedPatrolIDs;
};

/* This actually does Leaders AND Youth */
function readYouth( callback )
{
    var strMessage = "Your login (<a href='javascript:void(0)' onclick='gotoPage(\"#menus\");'>%1</a>) allows you to view/bookmark requirements for the following " + STR_YOUTHS + ".";
    $("#scout-picker-caveat").html( strMessage.replace( /%1/, getLoginEmail() ) );

    // create a fake entry for the 'unassigned' patrol
    var unassignedPatrol = { id: 0, name: SECTION_TYPE == "company" ? "Venturers" : "Unassigned" };
    g_dbTables['Patrols'][0] = unassignedPatrol;

    var roles = $('#list-youth-roles');
    roles.empty();
    for ( var role = ROLES.length-1; role >= 0; role-- )
        roles.append( "<li class='selection' role=" + role + "><a href='javascript:void(0)' onclick='setYouthRole(" + role + ");restorePage();'>" + ROLES[role].long + "</a></li>" );
    if ( ROLES.length <= 1 )
        $('#current-youth-role').closest("li").hide();

    // create a sorted list of the rows indices, based on the displayname logic
    var listSortedPatrolIDs = sortPatrols();

    // create a sorted list of the rows indices, based on the displayname logic
    var listSortedYouth = sortMembersByName( "Youth", g_dbTables['Youth'] );

    // invalidate bogus patrols
    $.each( listSortedYouth, function( i, youthID ) {
        var youth = g_dbTables['Youth'][youthID];
        if ( g_dbTables['Patrols'][youth.patrolid] === undefined )
        {
            console_warn( "could not find patrol ID '" + youth.patrolid + "'" );
            g_dbTables['Youth'][youthID].patrolid = 0;
        }
    });

    // pre-initialize the scoredcarded youth list, based on the values in db-scorecard-{requirements,badges}
    $.each( listSortedYouth, function( i, youthID ) {
        if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapBadgeCompletion[youthID] !== undefined )
        {
            if ( g_mapCategoryCounts[youthID] === undefined )
                resetCategoryCounts( youthID );

            if ( g_mapTypeCounts[youthID] === undefined )
                resetTypeCounts( youthID );

            g_setDirtyRequirements[youthID] = {};
            g_setDirtyBadges[youthID] = {};
            finishScorecard( youthID, true, false, false );
        }
        else if ( g_mapRequirementCompletion[youthID] !== undefined || g_mapBadgeCompletion[youthID] !== undefined )
            console_log( "skipping youth " + youthID );
    });

    finishScorecardingUpdating();

    // parents don't need to see patrols
    var showPatrols = g_mode == "group" && getLoginID() != null && getRole() != "p" && getRole() != "i" && listSortedPatrolIDs.length > 1;

    var targetYouthID = -1;
    var desiredYouthID = g_lastYouth;

    var listPatrols = $("#list-patrols");
    listPatrols.empty();

    var listPatrolsManage = $("#list-patrols-manage");
    listPatrolsManage.empty();

    var listYouth = $("#list-youth");
    listYouth.empty();
    g_mapYouthNames = {};
    g_mapLeaderNames = {};
    g_setActiveYouth = {};
    g_setActiveLeaders = {};

    var mapUniformUsage = {};

    var selectYouth = $("#snapshot-name select");
    selectYouth.empty();

    for ( var iPatrol = 0; iPatrol < listSortedPatrolIDs.length; iPatrol++ )
    {
        var patrol = g_dbTables['Patrols'][listSortedPatrolIDs[iPatrol]];

        addPatrolRows( patrol, false );

        var addPatrolDivider = showPatrols;
        var htmlYouthOptions = "";

        for ( var iName = 0; iName < listSortedYouth.length; iName++ )
        {
            var youth = g_dbTables['Youth'][listSortedYouth[iName]];
            var youthID = youth.id;

            var patrolID = youth.patrolid;
            if ( patrolID === undefined || patrolID == null || patrolID == "" )
                patrolID = 0;

            if ( patrolID != patrol.id )
                continue;       // youth is not in this patrol...

            var strName = getDisplayName( youth, youth.id );

            g_mapYouthNames[youthID] = strName;

            //console_log( "adding row for '" + youthID + "'" );
            if ( youth.active == 0 )
                continue;

            var uniformID = youth.uniformid;
            if ( uniformID === undefined )
                uniformID = 0;      // always default to "old", when the data is missing

            if ( mapUniformUsage[uniformID] === undefined )
                mapUniformUsage[uniformID] = 0;

            mapUniformUsage[uniformID] += 1;

            // is this the first scout in this patrol?  Then it's worthwhile displaying the divider element
            if ( addPatrolDivider )
            {
                listYouth.append( makePatrolDivider( null, patrolID, null ) );
                addPatrolDivider = false;
            }

            g_setActiveYouth[youthID] = 1;

            // do we need to find a good youth to start with
            if ( targetYouthID == -1 )
                targetYouthID = youthID;    // grab the first one we find
            else if ( youthID == desiredYouthID )
                targetYouthID = youthID;    // but always check to see if the current-youth is still around

            var li = document.createElement('li');
            li.className = "selection";
            li.setAttribute( "youthid", youthID );
            li.innerHTML = "<a href=\"javascript:void(0);\" onclick=\"setCurrentYouth( '" + youthID + "' );restorePage();restoreSelection();\">" + htmlEncode(strName) + getRoleTag( ROLES, youth.role ) + "</a>";
            listYouth.append( li );

            htmlYouthOptions += "<option value='" + youthID + "'>" + strName + "</option>";
        }

        if ( selectYouth.length > 0 && htmlYouthOptions.length > 0 )
        {
            if ( showPatrols )
                selectYouth.append( "<optgroup label='" + getPatrolName( patrol.id, false )+ "'>" + htmlYouthOptions + "</optgroup>" );
            else
                selectYouth.append( htmlYouthOptions );
        }
    }

    var listUsage = new Array();
    for ( var uniformID in mapUniformUsage )
        listUsage.push( parseInt( mapUniformUsage[uniformID] ) + 0.01 * parseInt( uniformID ) );     // push on, as a float
    listUsage.sort().reverse();     // sort, descending

    for ( var iUniform = 0; iUniform < listUsage.length; iUniform++ )
    {
        //console_log( "starting with listUsage[" + iUniform + "] = " + listUsage[iUniform] );
        var frac = 1.0*listUsage[iUniform] - ~~listUsage[iUniform];
        listUsage[iUniform] = ~~(100*frac + 0.5);
        //console_log( "... now = " + listUsage[iUniform] );
    }
    //console_log( "derived uniform list = " + listUsage.join(",") );
    setUniformList( listUsage.join(",") );
       
    for ( var iName = 0; iName < listSortedYouth.length; iName++ )
    {
        var youth = g_dbTables['Youth'][listSortedYouth[iName]];
        if ( g_setMarkedRequirements[youthID] === undefined )
            continue;
        if ( ! g_setMarkedRequirements[youthID] == null )
            console_log( "null for youth " + youth.id + " (" + g_mapYouthNames[ youth.id ] + ")" );
    }

    // if we are in 'handbook' mode, do nothing.  Otherwise, set the youth to the valid one we found
    if ( g_mode != "handbook" )
    {
        if ( getLoginID() )
            setCurrentYouth( targetYouthID );
        else
            scorecard( -1, true );
    }

    var containerID = "#list-youth-complete";
    listYouth = $( containerID );
    listYouth.empty();
    for ( var iPatrol = 0; iPatrol < listSortedPatrolIDs.length; iPatrol++ )
    {
        var patrol = g_dbTables['Patrols'][listSortedPatrolIDs[iPatrol]];

        var addPatrolDivider = showPatrols;

        for ( var iName = 0; iName < listSortedYouth.length; iName++ )
        {
            var youth = g_dbTables['Youth'][listSortedYouth[iName]];
            var youthID = youth.id;

            if ( youth.active == 0 )
                continue;

            var patrolID = youth.patrolid;
            if ( patrolID === undefined || patrolID == null || patrolID == "" )
                patrolID = 0;

            if ( patrolID != patrol.id )
                continue;       // youth is not in this patrol...

            // is this the first scout in this patrol?  Then it's worthwhile displaying the divider element
            if ( addPatrolDivider )
            {
                listYouth.append( makePatrolDivider( containerID, patrolID, null ) );
                addPatrolDivider = false;
            }

            var li = document.createElement('li');
            li.className = "checker";
            li.setAttribute( "youthid", youthID );
            li.setAttribute( "patrolid", patrolID );

            li.innerHTML = "<a href='javascript:void(0)' onclick='toggleYouthSelection( \"" + containerID + "\", " + youthID + " );'><img glyph src='" + BIMG + "/images/blank.gif' style='border:none;'/>" + htmlEncode( g_mapYouthNames[youthID] ) + getRoleTag( ROLES, youth.role ) + "</a>";
            listYouth.append( li );

        }

        setPatrolDividerStatus( containerID, patrol.id );
    }

    if ( g_currentUser != -1 )
        toggleYouthSelection( containerID, g_currentUser );

    var listTaxYouth = $("#list-youth-tax");
    var listSheetsYouth = $("#record-sheets-youth");

    listYouth = $("#list-youth-manage");
    listYouth.empty();
    listTaxYouth.empty();
    listSheetsYouth.empty();

    var nAdded = 0;
    for ( var iPatrol = 0; iPatrol < listSortedPatrolIDs.length; iPatrol++ )
    {
        var patrol = g_dbTables['Patrols'][listSortedPatrolIDs[iPatrol]];

        var addPatrolDivider = showPatrols;

        for ( var iName = 0; iName < listSortedYouth.length; iName++ )
        {
            var youth = g_dbTables['Youth'][listSortedYouth[iName]];
            var youthID = youth.id;

            var patrolID = youth.patrolid;
            if ( patrolID === undefined || patrolID == null || patrolID == "" )
                patrolID = 0;

            if ( patrolID != patrol.id )
                continue;       // youth is not in this patrol...

            // is this the first scout in this patrol?  Then it's worthwhile displaying the divider element
            if ( addPatrolDivider )
            {
                listYouth.append( makePatrolDivider( null, patrolID, null ) );
                listTaxYouth.append( makePatrolDivider( null, patrolID, null ) );
                listSheetsYouth.append( makePatrolDivider( null, patrolID, null ) );
                addPatrolDivider = false;
            }

            var li = document.createElement('li');
            li.className = "arrow" + (youth.active==0?" inactive":"");
            li.setAttribute( "youthid", youthID );

            li.innerHTML = "<a href='javascript:void(0)' onclick='doEditYouth(" + youthID + ", true);'>" + htmlEncode( g_mapYouthNames[youthID] ) + getRoleTag( ROLES, youth.role ) + "</a>";
            listYouth.append( li );

            listTaxYouth.append( "<li class='arrow" + (youth.active==0?" inactive":"") + "' youthid=" + youthID + "><a href='javascript:void(0)' onclick='doTaxReportDetails(true," + youthID + ");'><span class='stunted'>" + htmlEncode( g_mapYouthNames[youthID] ) + "</span> <small class='counter textonly'>$0</small></a></li>" );
            listSheetsYouth.append( "<li class='arrow" + (youth.active==0?" inactive":"") + "' youthid=" + youthID + ">" + getRecordSheetLink( youthID ) + "</li>" );

            nAdded++;
        }
    }

    $('#record-sheets-section li').html( getRecordSheetLink( -1 ) );

    if ( nAdded > 0 ) 
        listYouth.show();
    else
        listYouth.hide();

    var listYouth = $("#list-logins-youth-picker");
    listYouth.empty();

    for ( var iPatrol = 0; iPatrol < listSortedPatrolIDs.length; iPatrol++ )
    {
        var patrol = g_dbTables['Patrols'][listSortedPatrolIDs[iPatrol]];

        var addPatrolDivider = showPatrols;

        for ( var iName = 0; iName < listSortedYouth.length; iName++ )
        {
            var youth = g_dbTables['Youth'][listSortedYouth[iName]];
            var youthID = youth.id;

            var patrolID = youth.patrolid;
            if ( patrolID === undefined || patrolID == null || patrolID == "" )
                patrolID = 0;

            if ( patrolID != patrol.id )
                continue;       // youth is not in this patrol...

            var strYouthName = htmlEncode( g_mapYouthNames[youthID] );
            // is this the first scout in this patrol?  Then it's worthwhile displaying the divider element
            if ( addPatrolDivider )
            {
                listYouth.append( makePatrolDivider( null, patrolID, null ) );
                addPatrolDivider = false;
            }

            var li = document.createElement('li');
            li.className = "checker" + (youth.active==0?" inactive":"");
            li.setAttribute( "youthid", youthID );

            li.innerHTML = "<a href='javascript:void(0)' onclick='$(\"#list-logins-youth-picker li[youthid=" + youthID + "]\").toggleClass(\"selected\");clearActive();'><img glyph src='" + BIMG + "/images/blank.gif'/><span>" + strYouthName + "</span></a>";
            listYouth.append( li );
        }
    }

    containerID = "#list-event-youth-picker";
    listYouth = $( containerID );
    listYouth.empty();
    for ( var iPatrol = 0; iPatrol < listSortedPatrolIDs.length; iPatrol++ )
    {
        var patrol = g_dbTables['Patrols'][listSortedPatrolIDs[iPatrol]];

        var addPatrolDivider = showPatrols;

        for ( var iName = 0; iName < listSortedYouth.length; iName++ )
        {
            var youth = g_dbTables['Youth'][listSortedYouth[iName]];
            var youthID = youth.id;

            var patrolID = youth.patrolid;
            if ( patrolID === undefined || patrolID == null || patrolID == "" )
                patrolID = 0;

            if ( patrolID != patrol.id )
                continue;       // youth is not in this patrol...

            // is this the first scout in this patrol?  Then it's worthwhile displaying the divider element
            if ( addPatrolDivider && youth.active != 0 )
            {
                // note that the click handler for the glyph is assigned via .click() below
                listYouth.append( makePatrolDivider( containerID, patrolID, "updateYouthParticipation" ) );
                addPatrolDivider = false;
            }

            var li = $("<li class='checker' youthid=" + youthID + " patrolid=" + patrolID + "></li>" );

            li.html( "<a href='javascript:void(0)' onclick='toggleYouthSelection( \"#list-event-youth-picker\", " + youthID  + ");updateYouthParticipation( " + youthID + " );'><img glyph src='" + BIMG + "/images/blank.gif' style='border:none;height:16px;width:5px;' containerid='" + containerID + "' youthid='" + youthID + "'/>" + htmlEncode( g_mapYouthNames[youthID] ) + getRoleTag( ROLES, youth.role ) + "<small class='counter ffsux1'><span class=\"overrides\" href='javascript:void(0)' onclick='gotoEventLabelPicker(" + youthID + ");'></span></small></a>" );

            listYouth.append( li );
            li.toggle( youth.active != 0 );
        }

        setPatrolDividerStatus( containerID, patrol.id );
    }

    // it's possible that we want to restore the edit info...
    if ( getCurrentPage() == 'edit-youth' )
    {
        var element = $('form[name=youth-edit] input[name=youth-id]');
        if ( element != null )
        {
            var youthID = element.val();
            if ( youthID > 0 )
            {
                var youth = g_dbTables['Youth'][youthID];
                doEditYouth( youthID, false );
            }
        }
    }


    roles = $('#list-leader-roles');
    roles.empty();
    for ( var role = LEADER_ROLES.length-1; role >= 0; role-- )
        roles.append( "<li class='selection' role=" + role + "><a href='javascript:void(0)' onclick='setLeaderRole(" + role + ");restorePage();'>" + LEADER_ROLES[role].long + "</a></li>" );
    if ( LEADER_ROLES.length <= 1 )
        $('#current-leader-role').closest("li").hide();

    containerID = "#list-event-leader-picker";
    listLeaderPicker = $( containerID );
    listLeaderPicker.empty();

    var listLeaders = $("#list-leaders-manage");
    listLeaders.empty();

    var listSortedLeaders = sortMembersByName( "Leaders", g_dbTables['Leaders'] );
    for ( var iName = 0; iName < listSortedLeaders.length; iName++ )
    {
        var leader = g_dbTables['Leaders'][listSortedLeaders[iName]];
        var leaderID = leader.id;

        var strName = getDisplayName( leader, leader.id );
        if ( ! strName )
        {
            var login = g_dbTables['Leaders'][leader.loginid];
            if ( login !== undefined )
                strName = getLoginName(login.id);
                
        }

        if  ( ! strName )
            strName = "Unnamed";

        g_mapLeaderNames[leaderID] = strName;

        if ( leader.active == 1 )
            g_setActiveLeaders[leaderID] = 1;

        var strNoAccessClass = "";
        var strNoAccessText = "";
        if ( leader.active == 1 && leader.loginid <= 0 )
        {
            strNoAccessClass = " noaccess";
            strNoAccessText = " <span class='noaccess'>(No Login)</span>";
        }

        var li = document.createElement('li');
        li.className = "arrow" + (leader.active==0?" inactive":"") + strNoAccessClass;
        li.setAttribute( "leaderid", leaderID );

        li.innerHTML = "<a href='javascript:void(0)' onclick='doEditLeader(" + leaderID + ", true);'>" + htmlEncode( strName ) + getRoleTag( LEADER_ROLES, leader.role ) + strNoAccessText + "</a>";
        listLeaders.append( li );

        // now, do the event leader picker
        // is this the first scout in this patrol?  Then it's worthwhile displaying the divider element
        if ( listSortedLeaders.length > 1 && iName == 0 )
            listLeaderPicker.append( makeLeaderDivider( containerID, "updateLeaderParticipation" ) );

        li = $("<li class='checker' leaderid=" + leaderID + "></li>" );

        li.html( "<a href='javascript:void(0)' onclick='toggleLeaderSelection( \"#list-event-leader-picker\", " + leaderID  + ");updateLeaderParticipation( " + leaderID + " );'><img glyph src='" + BIMG + "/images/blank.gif' style='border:none;height:16px;width:5px;' containerid='" + containerID + "' leaderid='" + leaderID + "'/>" + htmlEncode( g_mapLeaderNames[leaderID] ) + getRoleTag( LEADER_ROLES, leader.role ) + "</a>" );

        listLeaderPicker.append( li );
        li.toggle( leader.active != 0 );
    }

    updateLoginLists( g_dbTables, function() { /* nop */ } );

    callback();
};

function updateYouthParticipation( youthID )
{
    g_hasUnsavedEventEdits = true;
    var isSelected = $( "#list-event-youth-picker li[youthid=" + youthID + "]").hasClass("selected");
    var outingYouth = getOutingYouth( g_eventUnderEdit.youth, youthID );

    if ( isSelected && ! outingYouth )
        g_eventUnderEdit.youth.push( { id: youthID } );
    
    else if ( ! isSelected && outingYouth )
    {
        for ( var iYouth = g_eventUnderEdit.youth.length-1; iYouth >= 0; iYouth-- )
        {
            if ( g_eventUnderEdit.youth[iYouth].id == youthID )
            {
                g_eventUnderEdit.youth.splice( iYouth, 1 );
                break;
            }
        }
    }
};

function updateLeaderParticipation( leaderID )
{
    g_hasUnsavedEventEdits = true;
    var isSelected = $( "#list-event-leader-picker li[leaderid=" + leaderID + "]").hasClass("selected");
    var outingLeader = getOutingYouth( g_eventUnderEdit.leaders, leaderID );

    if ( isSelected && ! outingLeader )
        g_eventUnderEdit.leaders.push( { id: leaderID } );
    
    else if ( ! isSelected && outingLeader )
    {
        for ( var iLeader = g_eventUnderEdit.leaders.length-1; iLeader >= 0; iLeader-- )
        {
            if ( g_eventUnderEdit.leaders[iLeader].id == leaderID )
            {
                g_eventUnderEdit.leaders.splice( iLeader, 1 );
                break;
            }
        }
    }
};

function getRecordSheetLink( youthID )
{
    var strName = youthID == -1 ? "Summary Record Sheet" : g_mapYouthNames[youthID];
    var htmlSheet = "<a href='/" + SECTION_TYPE + "/record-sheets/" + hex_md5( "" + getSection() + "+" + youthID) + ".html' target=_blank style='background-image:none;'>" + htmlEncode( strName ) + "<img style='margin-left:5px;' src='section-theme/img/external-link.gif'/></a>";
    return htmlSheet;
};

function doUpdateLogin( strAction, strPassword, youthRecord )
{
    clearActive();
    var jsonYouth = {}
    if ( youthRecord )
        jsonYouth = youthRecord;

    if ( g_nonce == null )
        strData = "email=getnonce";    // essentially try to do a login with a bogus email address... this will fail, since the nonce is null.
    else
        strData = strAction + "&youth=" + escape( JSON.stringify( jsonYouth ) );

    jQuery.ajax( {
        url: BADGES_AUTHENTICATE,
        data: strData + "&uid=" + getLoginID() + "&pw=" + ncrypt( strPassword, g_nonce ),
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error reading remote database", canClose: true } );
        },
        success: function( data ) 
        {
            //console_log( "ajax returned: " + JSON.stringify( data ) );

            if ( g_nonce == null && data != null && data.nonce != null )
            {
                g_nonce = data.nonce;
                console_log( "repeating with nonce" );
                doUpdateLogin( strAction, strPassword, youthRecord );
            }
            else if ( ! data || data.loginid == null )
            {
                openLightBox( { text: "There was a problem updating this login.  Probably the email address is already in use.", canClose: true } );
                if ( youthRecord )
                {
                    openLightBox( { text: "The login could not be created.  The email address is already in use.", canClose: true } );
                    g_cachedData = null;
                    ajaxGetYouth( function() { ajaxGetLogins( function() { console_log( "ajaxGetYouth: doSaveYouth + login failed" ); } ) } );
                    restorePage();
                    ajaxGetPresence();
                }
            }
            else
            {
                console_log( "login record inserted/updated" );
                g_cachedData = null;

                ajaxGetYouth( function() { ajaxGetLogins( function() { console_log( "ajaxGetYouth: doSaveYouth or doSaveLeader succeeded" ); } ) } );

                restorePage();
                ajaxGetPresence();
            }
        }
    });
};

function ajaxGetPresence()
{
    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "sectionid=" + getURLSection() + "&worksheet=Presence", // DO NOT pass in uid... we don't want polled checks to give the illusion of activity
        error: function( request, textStatus, errorThrown ) {
            console_log( "Error fetching presence" );
        },
        success: function( data ) 
        {
            var now = getServerTimestamp();

            $("#manage-logins li[loginid]").attr( "presence", "none" );
            for ( var key in data['Presence'] )
            {
                var presence = data['Presence'][key];
                var strStatus = "grey";
                var strTooltip = "Last signed in: ";
                if ( now - presence.timestamp < 1*86400000 )
                {
                    strStatus = "green";
                    strTooltip += "within the last 24 hours";
                }
                else if ( now - presence.timestamp < 7*86400000 )
                {
                    strStatus = "orange";
                    strTooltip += "within the last 7 days";
                }
                else
                    strTooltip += "more than last 7 days ago";

                $("#manage-logins li[loginid=" + presence.loginid + "] img.glyph").attr( "src", "./images/presence-" + strStatus + ".gif" ).attr( 'title', strTooltip );
            }

            if ( getCurrentPage() == "manage-logins" )
            {
                if ( g_timeoutPresence )
                    window.clearTimeout( g_timeoutPresence );

                g_timeoutPresence = window.setTimeout( ajaxGetPresence, 60000 );
            }
        }
    });
};

function validateEmail( strEmail )
{
    if ( strEmail == "" )
    {
        openLightBox( { text: "You must provide an email address.", canClose: true } );
        return false;
    }

    if ( ! strEmail.match( /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ ) )
    {
        openLightBox( { text: "The email '" + strEmail + "' is not valid.", canClose: true } );
        return false;
    }

    return true;
};

/**
 * Check for blanks, invalid email, password mismatch, etc.
 * Also, check that the email is not already in use.
 *
 * Return true only if everything is acceptable.
 */
function validateLoginInfo( id, strEmail, strPassword1, strPassword2 )
{
    if ( ! validateEmail( strEmail ) )
        return false;

    if ( strPassword1 != strPassword2 )
    {
        openLightBox( { text: "The passwords don't match.", canClose: true } );
        return false;
    }

    if ( id <= 0 && strPassword1 == "" )
    {
        openLightBox( { text: "A password is required.", canClose: true } );
        return false;
    }
    else if ( strPassword1 != "" && ! strPassword1.match( /^\S{4,}$/ ) )
    {
        openLightBox( { text: "The password is invalid.", canClose: true } );
        return false;
    }

    for ( var loginID in g_mapLoginEmails )
    {
        if ( g_mapLoginEmails[loginID] == strEmail )
        {
            if ( loginID != id ) 
            {
                openLightBox( { text: "There is already a login for that email address.", canClose: true } );
                return false;
            }
        }
    }

    return true;
};

function doSaveLogin( strWelcomeMessage )
{
    clearActive();

    var id = getEditLoginID();
    var strRole = $('form[name=login-edit] input[name=login-role]').val();
    if ( strRole == "v" )
        strRole = $('form[name=login-edit] input[name=login-badgemaster]').attr( "checked" ) ? "v" : "n";
    var strEmail = trim( $('form[name=login-edit] input[name=login-email]').val() ).toLowerCase();
    var strDisplayName = trim( $('form[name=login-edit] input[name=login-displayname]').val() );
    var strPassword1 = trim( $('form[name=login-edit] input[name=login-password1]').val() );
    var strPassword2 = trim( $('form[name=login-edit] input[name=login-password2]').val() );

    if ( ! validateLoginInfo( id, strEmail, strPassword1, strPassword2 ) )
        return false;

    var strAction;
    if ( id == 0 ) // is this a creation?
    {
        strAction = "action=insert&email=" + strEmail + "&sectionid=" + getURLSection() + "&role=" + strRole + "&displayname=" + escape(strDisplayName);
        if ( hasUpPoint( "edit-leader" ) )
            strAction += "&leaderid=" + $('input[name=leader-id]').val();
    }
    else
        strAction = "action=update&email=" + g_mapLoginEmails[id] + "&email2=" + strEmail + "&role=" + strRole + "&displayname=" + escape(strDisplayName);

    // did the user click the upper right Send button AND change his password?
    if ( strPassword1 != "" && strWelcomeMessage == "" )
    {
        if ( ! confirm( "If you want to send the user an email with their new password, click 'Cancel' and then 'Save and Send'." ) ) 
            return false;
    }
    if ( strWelcomeMessage != "" )
        strAction += "&welcome=" + escape( strWelcomeMessage );

    openLightBox( { text: id==0?"Creating login...":"Updating login..." } );

    doUpdateLogin( strAction, strPassword1, null );

    return true;
};

function doDeleteLogin()
{
    clearActive();

    var id = getEditLoginID();

    if ( id == getLoginID() )
    {
        openLightBox( { text: "You cannot delete your own login.", canClose: true } );
        return;
    }

    if ( ! confirm( "Deletion is permanent, and cannot be undone.  Continue?" ) )
        return;

    jQuery.ajax( {
        url: BADGES_AUTHENTICATE,
        data: "uid=" + getLoginID() + "&action=delete&email=" + g_mapLoginEmails[id],
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "login deleted" );
            g_cachedData = null;
            ajaxGetLogins( closeLightBox );
            restorePage();
        }
    });
};

function isLastActiveYouth( youthID )
{
    var nActiveYouth = size( g_setActiveYouth );

    return nActiveYouth == 1 && g_setActiveYouth[youthID] !== undefined;
};

function enableLoginFields( formName )
{
    var enable = $('form[name=' + formName + '] input[name=login-enabled]').attr( "checked" );

    $('form[name=' + formName + '] input[name=login-email]').parent().toggle( enable );
    $('form[name=' + formName + '] input[name=login-password1]').parent().parent().toggle( enable );
    $('form[name=' + formName + '] input[name=login-password2]').parent().toggle( enable );
    $('form[name=' + formName + '] input[name=login-badgemaster]').parent().toggle( enable );

    var pageID = $('form[name=' + formName + ']').closest( "div[data-role=page]" ).attr( "id" );
    $("#" + pageID + " .welcome").toggle( enable );
};

function getLeader( loginID )
{
    for ( var leaderID in g_dbTables['Leaders'] )
    {
        var leader = g_dbTables['Leaders'][leaderID];
        if ( leader.loginid == loginID )
            return leader;
    }

    return null;
};

function doDeleteLeader()
{
    clearActive();

    var leaderID = $('form[name=leader-edit] input[name=leader-id]').val();

    var otherLeader = getLeader( getLoginID() );        // get my own leader record
    if ( otherLeader && otherLeader.id == leaderID )
    {
        openLightBox( { text: "You cannot delete your own record.", canClose: true } );
        return;
    }

    if ( ! confirm( "Deletion is permanent, and cannot be undone.  Continue?" ) )
        return;

    var strAction = "select=id.eq." + leaderID + "&delete";

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&sectionid=" + getURLSection() + "&worksheet=Leaders&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "leader record deleted" );
            g_cachedData = null;
            ajaxGetYouth( function() { console_log( "doDeleteLeader" ); } );
        }
    });

    restorePage();
};

function doDeleteYouth()
{
    clearActive();

    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();

    if ( isLastActiveYouth( youthID ) )
    {
        openLightBox( { text: "You cannot delete the only active " + STR_YOUTH, canClose: true } );
        return;
    }

    if ( ! confirm( "Deletion is permanent, and cannot be undone.  Continue?" ) )
        return;

    var strAction = "select=id.eq." + youthID + "&delete";

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&sectionid=" + getURLSection() + "&worksheet=Youth&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "youth record deleted" );
            g_cachedData = null;
            ajaxGetYouth( function() { console_log( "doDeleteYouth" ); } );
        }
    });

    restorePage();
};

function getAccessedYouthSubtext( data, id, filterMatch )
{
    var strSubtext = "";
    for ( var keyAccess in data )
    {
        var access = data[keyAccess];
        if ( access.loginid != id )
            continue;

        var youthID = access.youthid;

        if ( youthID <= 0 )
            continue;

        var htmlName = htmlEncode( g_mapYouthNames[youthID] );
        if ( g_setActiveYouth[youthID] === undefined )
            htmlName = "<span class='inactive'>" + htmlName + "</span>";

        if ( strSubtext != "" ) strSubtext += ", ";
        strSubtext += htmlName;
    }

    if ( strSubtext != "" && ( ! filterMatch || strSubtext != getLoginName(id) ) )
        strSubtext = "<span class=\"subtext\">" + strSubtext + "</span>";
    else
        strSubtext = "";

    return strSubtext;
};

function getEditLoginID()
{
    return $('form[name=login-edit] input[name=login-id]').val();
};

function getLoginLeader()
{
    var leader = getLeader( getEditLoginID() );

    if ( leader != null )
        return leader.id;

    return 0;
};

function doEditLogin( id, isLeader )
{
    clearActive();

    if ( ! isOnline() )
    {
        openLightBox( { text: "You must be on-line to make changes to account settings.", canClose: true } );
        return;
    }

    var isSelf = id == getLoginID();
    $('form[name=login-edit] input[name=login-password1]').parent().css( "margin-right", isSelf ? "0" : "140px" ).css( "margin-bottom", isSelf ? "0" : "8px" );
    $('form[name=login-edit] input[name=login-password1]').parent().parent().children( "div.button" ).toggle( ! isSelf );

    $('form[name=login-edit] input[name=login-id]').val( id );
    if ( isLeader )
    {
        var strRole = "v";      // leaders default to this
        if ( id != 0 )          // if this is an existing login, we can look up the role
            strRole = g_setBadgemasters[id] === undefined ? "n" : "v";
        if ( isSelf && strRole != "v" )
        {
            console_warn( "role should be 'v'" );
            strRole = "v";
        }

        $('form[name=login-edit] input[name=login-badgemaster]').attr( "checked", strRole == "v" );
        $('form[name=login-edit] input[name=login-badgemaster]').attr( "disabled", isSelf );
        $('form[name=login-edit] input[name=login-badgemaster]').parent().toggleClass( "noaccess", isSelf );
    }
    $('form[name=login-edit] input[name=login-role]').val( isLeader ? "v" : "p" );
    $('form[name=login-edit] input[name=login-badgemaster]').parent().toggle( isLeader );

    // Setting a value to null leaves it untouched (doesn't clear it), so we pre-clear the fields
    $('form[name=login-edit] input[name=login-displayname]').val( "" );

    $('form[name=login-edit] input[name=login-email]').val( "" );
    $('form[name=login-edit] input[name=login-password1]').val( "" );
    $('form[name=login-edit] input[name=login-password2]').val( "" );

    $('form[name=login-edit] input[name=login-displayname]').parent().toggle( ! isLeader );
    $('#login-leaderlink').toggle( isLeader && ! hasUpPoint( "edit-leader" ) );

    // swap the positions so that hopefully something visible will be the first-child
    if ( isLeader )
        $('form[name=login-edit] input[name=login-displayname]').parent().before( $('#login-leaderlink').detach() );
    else
        $('#login-leaderlink').before( $('form[name=login-edit] input[name=login-displayname]').parent().detach() );

    $( '#login-hint' ).toggle( id == 0 && ! isLeader );

    if ( id == 0 )
    {
        $('#edit-login > div > h1').text( "Create Login" );
        $('form[name=login-edit] input[name=login-password1]').attr( "placeholder", "Password" );
        $('form[name=login-edit] input[name=login-password2]').attr( "placeholder", "Password (Repeat)" );

        $('#login-edit-delete').hide();
        $('#login-edit-logins').hide();
        $('#login-edit-youth').hide();
    }
    else
    {
        var leaderID = getLoginLeader( id );
        $('#login-leaderlink small').html( htmlEncode( g_mapLeaderNames[leaderID] ) );

        $('form[name=login-edit] input[name=login-displayname]').val( g_mapLoginNames[id] );
        $('form[name=login-edit] input[name=login-email]').val( g_mapLoginEmails[id] );
        $('form[name=login-edit] input[name=login-password1]').attr( "placeholder", "New Password, or Blank" );
        $('form[name=login-edit] input[name=login-password2]').attr( "placeholder", "New Password, or Blank (Repeat)" );

        $('#login-edit-delete').show();

        if ( ! isLeader && getRole() == "v" )   // if the edited login ISN'T a leader, but I AM, then add the scout picker
        {
            var nLogins = size( g_mapAccessByLogin[id] );
            $('#login-edit-youth-count').html( nLogins + "<img width=1 height=18 style='padding-left:2px;vertical-align:-2px;padding-right:7px;' src='" + BIMG + "/images/blank.gif'/>" );
            $('#login-edit-youth').show();

            var strSubtext = getAccessedYouthSubtext( g_dbTables['Access'], id, false );
            $('#login-edit-youth-count').css( 'margin-top', strSubtext == "" ? '-26px' : '-32px' );
            $('#login-edit-youth a').html( "Accessed " + STR_YOUTHS + strSubtext );
        }
        else {
            $('#login-edit-youth').hide();
        }
    }

    addPage( "#edit-login" );

    toggleWelcomeText( "#edit-login", getLocalStorage( "welcome-custom-" + (isLeader?"leader":"youth") ) );
};

function doSaveLeader()
{
    clearActive();

    if ( $('#leader-edit-save').attr( "disabled" ) )
    {
        console_log( "quitting early" );
        return false;
    }

    var leaderID = $('form[name=leader-edit] input[name=leader-id]').val();
    var strDisplayName = trim( $('form[name=leader-edit] input[name=leader-displayname]').val() );
    var strFirstName = trim( $('form[name=leader-edit] input[name=leader-firstname]').val() );
    var strLastName = trim( $('form[name=leader-edit] input[name=leader-lastname]').val() );
    var addLogin = leaderID == 0 && $('form[name=leader-edit] input[name=login-enabled]').attr( "checked" )
        var strEmail = addLogin ? trim( $('form[name=leader-edit] input[name=login-email]').val().toLowerCase() ) : "";
    var strPassword1 = addLogin ? trim( $('form[name=leader-edit] input[name=login-password1]').val() ) : "";
    var strPassword2 = addLogin ? trim( $('form[name=leader-edit] input[name=login-password2]').val() ) : "";

    var newRole = $('#list-leader-roles li.checked').attr( 'role' );
    var bActive = $('form[name=leader-edit] input[name=leader-active]').val() == 1;

    var timestamp = g_jdPickers['joined-changer-date-input'].stringToDate( $('#joined-changer-date-input').val() ).getTime();

    if ( strDisplayName == "" && strFirstName == "" && strLastName == "" )
    {
        openLightBox( { text: "You must provide a name", canClose: true } );
        return false;
    }

    // check to see if there will be confusion over this name (i.e., two kids identified as "Pat")
    var strNewName = strDisplayName;
    if ( ! strNewName || strNewName === undefined || strNewName == "" )
        strNewName = trim( strFirstName + " " + strLastName );

    for ( var otherLeaderID in g_mapLeaderNames )
    {
        if ( otherLeaderID == leaderID )
            continue;       // skip myself

        if ( g_mapLeaderNames[otherLeaderID] == strNewName )
        {
            var strMessage = "There is already a " + STR_LEADER + " who displays as<br/>%1<br>Please modify the display name of one or both " + STR_LEADERS + " so you can tell them apart.";
            openLightBox( { text: strMessage.replace( /%1/, strNewName ), canClose: true, size: "big" } );
            return false;
        }
    }

    var leaderRecord = {
        id: leaderID,
        sectionid: getSection(),
        displayname: strDisplayName,
        firstname: strFirstName,
        lastname: strLastName,
        active: bActive?1:0,
        role: newRole,
        date: timestamp
    };

    if ( addLogin )
    {
        // if the user wants to create a login, then info had better be correct
        if ( ! validateLoginInfo( leaderID, strEmail, strPassword1, strPassword2 ) )
            return false;

        strRole = $('form[name=leader-edit] input[name=login-badgemaster]').attr( "checked" ) ? "v" : "n";
        var strAction = "action=insert&email=" + strEmail + "&sectionid=" + getURLSection() + "&role=" + strRole + "&displayname=" + escape(strNewName);

        var strWelcomeMessage = buildWelcomeText( "#edit-leader", true, false, strEmail, strPassword1 ).replace( /<br\/>/g, "\n" ).replace( /&nbsp;/g, " " );
        if ( ! getSwitch( "#edit-leader .welcome-enable" ) )
        {
            if ( ! confirm( "Do you want to create this new " + STR_LEADER + " without sending a 'welcome' message?" ) ) 
            {
                toggleWelcome( "#edit-leader", true );
                return false;
            }
        }

        else if ( strWelcomeMessage != "" )
            strAction += "&welcome=" + escape( strWelcomeMessage );

        doUpdateLogin( strAction, strPassword1, leaderRecord );
    }
    else
        ajaxUpdateLeader( leaderRecord );

    return true;
};

function toggleInputSwitch( name, value )
{
    toggleSwitch( "input[name=" + name + "] + ", value == 0 );

    if ( value == 0 )
        $( 'input[name=' + name + ']' ).val( $('input[name=' + name + '] + small span:first-child' ).attr('value') );
    else
        $( 'input[name=' + name + ']' ).val( $('input[name=' + name + '] + small span:first-child + span' ).attr('value') );
};

function doSaveYouth()
{
    clearActive();

    if ( $('#youth-edit-save').attr( "disabled" ) )
    {
        console_log( "quitting early" );
        return false;
    }

    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();
    var strDisplayName = trim( $('form[name=youth-edit] input[name=youth-displayname]').val() );
    var strFirstName = trim( $('form[name=youth-edit] input[name=youth-firstname]').val() );
    var strLastName = trim( $('form[name=youth-edit] input[name=youth-lastname]').val() );
    var addLogin = $('form[name=youth-edit] input[name=login-enabled]').attr( "checked" )
        var strEmail = addLogin ? trim( $('form[name=youth-edit] input[name=login-email]').val() ) : "";
    var strPassword1 = addLogin ? trim( $('form[name=youth-edit] input[name=login-password1]').val() ) : "";
    var strPassword2 = addLogin ? trim( $('form[name=youth-edit] input[name=login-password2]').val() ) : "";
    var patrolID = $('form[name=youth-edit] input[name=youth-patrolid]').val();
    if ( patrolID == null || patrolID == "" )
        patrolID = 0;

    var newRole = $('#list-youth-roles li.checked').attr( 'role' );
    var bActive = $('form[name=youth-edit] input[name=youth-active]').val() == 1;
    var uniformID = $('form[name=youth-edit] input[name=youth-uniformid]').val();

    var timestamp = g_jdPickers['joined-changer-date-input'].stringToDate( $('#joined-changer-date-input').val() ).getTime();

    if ( ! bActive )
    {
        if ( isLastActiveYouth( youthID ) )
        {
            openLightBox( { text: "You cannot deactivate the only active " + STR_YOUTH, canClose: true } );
            return false;
        }
    }

    if ( strDisplayName == "" && strFirstName == "" && strLastName == "" )
    {
        openLightBox( { text: "You must provide a name", canClose: true } );
        return false;
    }

    // check to see if there will be confusion over this name (i.e., two kids identified as "Pat")
    var strNewName = strDisplayName;
    if ( ! strNewName || strNewName === undefined || strNewName == "" )
        strNewName = trim( strFirstName + " " + strLastName );

    for ( var otherYouthID in g_mapYouthNames )
    {
        if ( otherYouthID == youthID )
            continue;       // skip myself

        if ( g_mapYouthNames[otherYouthID] == strNewName )
        {
            var strMessage = "There is already a " + STR_YOUTH + " who displays as<br/>%1<br>Please modify the display name of one or both " + STR_YOUTHS + " so you can tell them apart.";
            openLightBox( { text: strMessage.replace( /%1/, strNewName ), canClose: true, size: "big" } );
            return false;
        }
    }

    var youthRecord = {
        id: youthID,
        sectionid: getSection(),
        displayname: strDisplayName,
        firstname: strFirstName,
        lastname: strLastName,
        active: bActive?1:0,
        uniformid: uniformID,
        role: newRole,
        patrolid: patrolID,
        date: timestamp
    };

    if ( addLogin )
    {
        // if the user wants to create a login, then info had better be correct
        if ( ! validateLoginInfo( youthID, strEmail, strPassword1, strPassword2 ) )
            return false;

        var strAction = "action=insert&email=" + strEmail + "&sectionid=" + getURLSection() + "&role=p&displayname=" + escape(strNewName);

        var strWelcomeMessage = buildWelcomeText( "#edit-youth", false, false, strEmail, strPassword1 ).replace( /<br\/>/g, "\n" ).replace( /&nbsp;/g, " " );
        if ( ! getSwitch( "#edit-youth .welcome-enable" ) )
        {
            if ( ! confirm( "Do you want to create this new " + STR_YOUTH + " without sending a 'welcome' message?" ) ) 
            {
                toggleWelcome( "#edit-youth", true );
                return false;
            }
        }

        else if ( strWelcomeMessage != "" )
            strAction += "&welcome=" + escape( strWelcomeMessage );

        console_log( "action = '" + strAction + "'" );
        doUpdateLogin( strAction, strPassword1, youthRecord );
    }
    else
        ajaxUpdateYouth( youthRecord );

    return true;
};

function ajaxUpdateLeader( leaderRecord )
{
    var strAction = (leaderRecord.id==0 ? "insert" : "select=id.eq." + leaderRecord.id + "&update") + "=" + escape(JSON.stringify( leaderRecord )) + "&sectionid=" + getURLSection();

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&worksheet=Leaders&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "leader record updated" );
            openLightBox( { text: "Record updated", timeout: 1000 } );
            var strName = getDisplayName( leaderRecord, leaderRecord.id );
            g_mapLeaderNames[leaderRecord.id] = strName; // this'll eventualy get overwritten by ajaxGetYouth
            console_log( "new name says '" + strName + "'" );
            $('#login-leaderlink small').html( htmlEncode( g_mapLeaderNames[leaderRecord.id] ) );

            g_cachedData = null;
            ajaxGetYouth( function() { console_log( "ajaxGetYouth: doSaveLeader" ); } );
        }
    });

    restorePage();
};

function ajaxUpdateYouth( youthRecord )
{
    var strAction = (youthRecord.id==0 ? "insert" : "select=id.eq." + youthRecord.id + "&update") + "=" + escape(JSON.stringify( youthRecord )) + "&sectionid=" + getURLSection();

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&worksheet=Youth&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "youth record updated" );
            openLightBox( { text: "Record updated", timeout: 1000 } );

            g_cachedData = null;
            ajaxGetYouth( function() { console_log( "ajaxGetYouth: doSaveYouth" ); } );
        }
    });

    restorePage();
};

function ajaxUpdateYouthPatrols( oldPatrolID, newPatrolID )
{
    openLightBox( { page: "manage-youth", text: "Records updated", timeout: 1000 } );

    var youthRecord = {
        patrolid: newPatrolID
    }

    var strAction = "select=patrol_id.eq." + oldPatrolID + "&update=" + JSON.stringify( youthRecord );

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&worksheet=Youth&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "youth record(s) updated" );
            g_cachedData = null;
            ajaxGetYouth( function() { console_log( "ajaxGetYouth: doSaveYouth" ); } );
        }
    });
};

function ajaxDeletePatrol( patrolID, reloadYouth )
{
    openLightBox( { text: STR_PATROL + " deleted", timeout: 1000 } );

    var strAction = "select=id.eq." + patrolID + "&delete";

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&sectionid=" + getURLSection() + "&worksheet=Patrols&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            $("#list-patrols li[patrolid=" + patrolID + "]").remove();
            $("#list-patrols-manage tr[patrolid=" + patrolID + "]").remove();
            delete g_dbTables['Patrols'][patrolID];

            if ( reloadYouth )
                ajaxUpdateYouthPatrols( patrolID, 0, reloadYouth );
        }
    });
};

function doDeletePatrol( patrolID )
{
    var affectsCurrentYouth = false;
    var currentYouthID = $('form[name=youth-edit] input[name=youth-id]').val();
    var nCount = 0;
    for ( var youthID in g_dbTables['Youth'] )
    {
        if ( g_dbTables['Youth'][youthID].patrolid == patrolID )
        {
            nCount++;
            if ( youthID == currentYouthID )
                affectsCurrentYouth = true;
        }
    }

    if ( nCount > 0 )
    {
        var strMessage = "There are still %1 " + STR_YOUTHS + " in this " + STR_PATROL + ".  Deleting this " + STR_PATROL + " will make them 'Unassigned'.  Continue?";
        if ( ! confirm( strMessage.replace( /%1/, nCount ) ) )
            return;
    }

    if ( size( g_dbTables['Patrols'] ) <= 2 )  // 2 = the 'unassigned' patrol + the one we are just deleting
        restorePage();      // nothing left to show, so go back to list of patrols

    ajaxDeletePatrol( patrolID, nCount > 0 );

    if ( affectsCurrentYouth )
        doSetPatrol( patrolID )
};

function ajaxCreatePatrol()
{
    openLightBox( { text: STR_PATROL + " created", timeout: 1000 } );

    var patrolRecord = {
        id: 0,
        sectionid: getSection(),
        name: ""
    };

    var strAction = "insert=" + escape(JSON.stringify( patrolRecord ));

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&worksheet=Patrols&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "patrols record returned '" + data.id + "'" );
            var patrol = {
                id: data.id,
                name: ""
            };

            g_dbTables['Patrols'][data.id] = patrol;
            addPatrolRows( patrol, true );
        }
    });
};

function doCreatePatrol()
{
    doRenamePatrols();

    var canCreate = true;
    for ( var patrolID in g_dbTables['Patrols'] )
    {
        var patrol = g_dbTables['Patrols'][patrolID];
        var strName = patrol.name;
        if ( strName === undefined || strName == null || strName == "" )
            canCreate = false;
    }

    if ( canCreate )
        ajaxCreatePatrol( patrolID );
    else
        openLightBox( { text: "There is an unnamed " + STR_PATROL + ". Name it before creating a new " + STR_PATROL + ".", canClose: true } );

    addPage( "#patrols-manage" );
};

function ajaxUpdatePatrol( patrolID, strName )
{
    openLightBox( { page: 'patrols', text: STR_PATROL + " updated", timeout: 1000 } );

    var patrolRecord = {
        id: patrolID,
        sectionid: getSection(),
        name: strName 
    };

    var strAction = "select=id.eq." + patrolID + "&update=" + escape(JSON.stringify( patrolRecord ));
    console_log( "doing '" + strAction + "'" );

    jQuery.ajax( {
        url: BADGES_TABLES,
        data: "uid=" + getLoginID() + "&worksheet=Patrols&" + strAction,
        error: function( request, textStatus, errorThrown ) {
            openLightBox( { text: "Error updating database", canClose: true } );
        },
        success: function( data ) 
        {
            console_log( "patrols record updated" );
        }
    });
};

function doRenamePatrols()
{
    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();
    var youth = g_dbTables['Youth'][youthID];

    for ( var patrolID in g_dbTables['Patrols'] )
    {
        if ( patrolID == 0 )
            continue;   // no way to update the 'Unassigned' patrol

        var input = $("#list-patrols-manage input[patrolid=" + patrolID + "]");
        var strName = trim( input.val() );
        var strNameLC = strName.toLowerCase();

        if ( g_dbTables['Patrols'][patrolID].name != strName )
        {
            if ( strName == "" &&  g_dbTables['Patrols'][patrolID].name === undefined )
                continue;       // skip this case

            console_log( "input '" + strName + "' differs from name '" + g_dbTables['Patrols'][patrolID].name + "'" );
            var validName = true;
            var strUnassignedNameLC = g_dbTables['Patrols'][0].name.toLowerCase();
            if ( strNameLC == strUnassignedNameLC )
            {
                openLightBox( { text: "Attempt to assign reserved name '" + g_dbTables['Patrols'][0].name + "' was refused", canClose: true } );
                input.val( input.attr( "origval" ) );
                validName = false;
            }

            if ( validName )
            {
                for ( var pid in g_dbTables['Patrols'] )
                {
                    if ( pid == 0 || pid == patrolID )
                        continue; // don't check for duplicate names with myself

                    var strName2 = trim( $("#list-patrols-manage input[patrolid=" + pid + "]").val() );
                    if ( strNameLC == strName2.toLowerCase() )
                    {
                        openLightBox( { text: "Attempt to assign duplicate name '" + strName2 + "' was refused", canClose: true } );
                        input.val( input.attr( "origval" ) );
                        validName = false;
                    }
                }
            }

            if ( validName ) 
            {
                ajaxUpdatePatrol( patrolID, strName );

                // rather than do an 'ajaxGetYouth' to refresh the lists, just manually
                // update everything that needs it
                g_dbTables['Patrols'][patrolID].name = strName;      // make sure the name will be rendered correctly
                input.val( strName );                                   // make sure that it is trimmed

                strName = getPatrolName( patrolID, true ); 
                var a = $("#list-patrols li[patrolid=" + patrolID + "] a");
                if ( a === undefined || a == null )
                    console_warn( "no a element" );
                else
                {
                    console_log( "updating to '" + strName + "'" );
                    a.html( strName ); 
                }
                $("li.divider[patrolid=" + patrolID + "]").html( strName ); 

                // update the "original" (i.e., safe) value
                var element = $('#list-patrols-manage tr[patrolid=' + patrolID + '] input[placeholder]' );
                element.attr( "origval", element.val() );
            }
        }
    }

    // make sure that the GUI reflects the possibly new name
    if ( youth !== undefined )
        doSetPatrol( youth.patrolid ); 
};

function doSetPatrol( patrolID )
{
    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();
    for ( var pid in g_dbTables['Patrols'] )
        $( "#list-patrols li[patrolid=" + pid + "]" ).removeClass( "checked" );

    $( "#list-patrols li[patrolid=" + patrolID + "]" ).addClass( "checked" );

    $('form[name=youth-edit] input[name=youth-patrolid]').val( patrolID );
    $('#current-patrol').html( getPatrolName( patrolID, true ) );
};

function getPatrolName( patrolID, useItalics )
{
    if ( patrolID == 0 )
        ;
    else if ( patrolID === undefined || patrolID == null || patrolID == "" )
    {
        console_trace( "patrolID = '" + patrolID + "'" );
        patrolID = 0;
    }

    var strName = null;
    var italicize = patrolID == 0 && SECTION_TYPE != "company";

    var patrol = g_dbTables['Patrols'][patrolID];
    if ( patrol === undefined )
    {
        strName = "Unknown";
        italicize = true;
    }
    else
    {
        strName = htmlEncode( patrol.name );

        if ( strName === undefined || strName == null || strName == "" )
        {
            strName = "Unnamed";
            italicize = true;
        }
    }

    if ( italicize && useItalics )
        strName = "<span style='font-style:italic;'>" + strName + "</span>";

    return strName;
};

function setMemberStatus( active, bFromGUI )
{
    clearActive();

    if ( hasUpPoint( "manage-leaders" ) )
    {
        if ( bFromGUI && ! active )       // being deactivated?
            if ( getLeaderLoginID() > 0 && ! confirm( "Deactivating a " + STR_LEADER.toLowerCase() + " will also delete the " + STR_LEADER.toLowerCase() + "'s Login." ) )
            {
                setMemberStatus( 1, true )
                return false;
            }
        
        $('form[name=leader-edit] input[name=leader-active]').val( active )
    }
    else
        $('form[name=youth-edit] input[name=youth-active]').val( active )

    return true;
};

function setJoinedDate( strDate )
{
    $('#joined-changer-date-input').val( strDate );
    $('#joined-changer-date').show();
    g_jdPickers['joined-changer-date-input'].selectDate();

    $('.current-date').html( strDate );
};

function setYouthRole( role )
{
    clearActive();

    $('#list-youth-roles li[role]').removeClass( "checked" );
    $('#list-youth-roles li[role=' + role + ']').addClass( "checked" );

    $('#current-youth-role').html( ROLES[role].long );
};

function setLeaderRole( role )
{
    clearActive();

    $('#list-leader-roles li[role]').removeClass( "checked" );
    $('#list-leader-roles li[role=' + role + ']').addClass( "checked" );

    $('#current-leader-role').html( LEADER_ROLES[role].long );
};

function getRoleTag( listRoles, role )
{
    if ( listRoles == null )
        return "";

    var strRole = "";
    if ( role !== undefined && role >= 0 && role < listRoles.length )
        strRole = listRoles[role].tag;

    if ( strRole != "" )
        return "<span class='plapl'>(" + strRole + ")</span>";

    return "";
};

function getLeaderLoginID()
{
    var leaderID = $('input[name=leader-id]').val();

    var leader = g_dbTables['Leaders'][leaderID];
    if ( leader && leader.loginid > 0 )
        return leader.loginid;

    return 0;
}

function doEditLeader( leaderID, bNavigate )
{
    clearActive();

    if ( ! isOnline() )
    {
        openLightBox( { text: "You must be on-line to make changes to account settings.", canClose: true } );
        return;
    }

    $('#joined-date-changer div[data-role=header] a').text( STR_LEADER );

    // Setting a value to null leaves it untouched (doesn't clear it), so we pre-clear the fields
    $('form[name=leader-edit] input[name=leader-displayname]').val( "" );
    $('form[name=leader-edit] input[name=leader-firstname]').val( "" );
    $('form[name=leader-edit] input[name=leader-lastname]').val( "" );
    $('form[name=leader-edit] input[name=login-enabled]').attr( "checked", true );
    $('form[name=leader-edit] input[name=login-email]').val( "" );
    $('form[name=leader-edit] input[name=login-password1]').val( "" );
    $('form[name=leader-edit] input[name=login-password2]').val( "" );
    $('form[name=leader-edit] input[name=login-badgemaster]').attr( "checked", true );

    var leader = g_dbTables['Leaders'][leaderID];
    var bNew = leader === undefined;

    $("form[name=leader-edit] input[name=login-enabled]").parent().toggle( bNew );
    $("form[name=leader-edit] input[name=login-email]").parent().toggle( bNew );
    $("form[name=leader-edit] input[name=login-password1]").parent().parent().toggle( bNew );
    $("form[name=leader-edit] input[name=login-password2]").parent().toggle( bNew );
    $("form[name=leader-edit] input[name=login-badgemaster]").parent().toggle( bNew );

    if ( bNew )
    {
        setLeaderRole( 0 );
        $( "#list-patrols li[patrolid=" + 0 + "]" ).addClass( "checked" );

        $('form[name=leader-edit] input[name=leader-id]').val( 0 );
        setMemberStatus( 1, false );
        toggleInputSwitch( 'leader-active', 0 );
        $('input[name=leader-active]').parent().show();
        setJoinedDate( formatDate( getServerTimestamp() ) );
        $('#leader-login').hide();
        enableLoginFields( 'leader-edit' );
    }
    else
    {
        setLeaderRole( leader.role );

        $('form[name=leader-edit] input[name=leader-id]').val( leader.id );
        $('form[name=leader-edit] input[name=leader-displayname]').val( leader.displayname );
        $('form[name=leader-edit] input[name=leader-firstname]').val( leader.firstname );
        $('form[name=leader-edit] input[name=leader-lastname]').val( leader.lastname );

        var login = g_dbTables['Logins'][leader.loginid];
        if ( login )
        {
            $('form[name=leader-edit] input[name=login-email]').val( login.email );
            $('#leader-login small').html( login.email );
        }
        else
        {
            $('#leader-login small').html( "None" );
        }
        $('#leader-login').toggle( leader.active == 1 && ! hasUpPoint( "edit-login" ) );

        setMemberStatus( leader.active, false );
        toggleInputSwitch( 'leader-active', leader.active ? 0 : 1 );
        setJoinedDate( formatDate( leader.date ) );

        var otherLeader = getLeader( getLoginID() );
        $('input[name=leader-active]').parent().toggle( otherLeader == null || otherLeader.id != leaderID );

        $('#leader-edit-save a').removeAttr( "disabled" );
        $( "#edit-leader .welcome" ).hide();
    }

    $('#leader-edit-delete').toggle( !bNew );
    toggleWelcome( "#edit-leader", true );
    $("#edit-leader div[data-role=header] h1").text( bNew ? ("New " + STR_LEADER) : ("Edit " + STR_LEADER) );
    $("#leader-edit-save").html( bNew ? "&nbsp;Create&nbsp;" : "&nbsp;&nbsp;Save&nbsp;&nbsp;" );

    if ( bNavigate )
        addPage( "#edit-leader" );

    toggleWelcomeText( "#edit-leader", getLocalStorage( "welcome-custom-leader" ) );
};

function doSaveYouthRecords()
{
    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();

    var listReqs = new Array();
    var mapActions = {};
    $('#initialize-table-body td[z=1]').each( function() {
        var badgeID = $(this).closest("tr").attr( 'badgeid' );
        var section = $(this).attr( "q" );
        if ( section === undefined )
            section = "";
        var bMarkAsComplete = $(this).attr('pre')==0;
        var req = $(this).attr( "r" ).replace( /(\d+-)(.+)/, "$2" );
        var reqID = getRequirementID( badgeID, section + req );

        listReqs.push( reqID );

        mapActions[reqID] = bMarkAsComplete;
        //console_log( "youth " + youthID + ": req " + req + ", reqID = " + reqID + " -> " + bMarkAsComplete ); 
    } );

    if ( listReqs.length > 0 )
    {
        var strTimestamp = getServerTimestamp();
        var strNotes = "";

        for ( var i = 0; i < listReqs.length; i++ )
        {
            var requirementID = listReqs[i];

            var jsonCompletion = g_mapRequirementCompletion['illegal_bogus_value']; // force it to be undefined, for now
            if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][requirementID] !== undefined )
                jsonCompletion = g_mapRequirementCompletion[youthID][requirementID];

            var bMarkAsComplete = mapActions[requirementID];
            if ( bMarkAsComplete === undefined )
                console_trace( "wowot! youthID=" + youthID + ", reqID=" + reqID );
            else if ( bMarkAsComplete )
            {
                if ( jsonCompletion === undefined || jsonCompletion.complete < jsonCompletion.total )
                    addRequirementComplete( youthID, requirementID, strNotes, true, false, strTimestamp, getLoginID(), true );
            }
            else
            {
                if ( g_setMarkedRequirements[youthID] !== undefined && g_setMarkedRequirements[youthID][requirementID] !== undefined )
                    clearRequirementComplete( youthID, requirementID, "", true, false, true );
            }
        }

        unscorecard( youthID );     // clear cache
        scorecard( youthID );       // restore cache for current user
        scheduleSync( "sync'd by initial records", 500 );
    }

    restorePage();
};

function doInitializeYouthRecords( bNavigate )
{
    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();

    setCurrentYouth( youthID );

    scorecardAllYouth( function() {
        buildInitialReport();

        if ( bNavigate )
            addPage( '#initialize-youth-records' );
    } );
};

function buildInitialReport()
{
    var youthID = g_currentUser;
    var tableReport = $('#initialize-table-body > table');
    tableReport.empty();
    var nCols = 0;

    // for each category, iterate over all the badges and generate the HTML.  We'll have 
    // to do a second pass once we know the max number of cells in the table
    var mapHtml = {};
    var htmlRows = "";
    for ( var iType = 0; iType < BADGE_SELECTOR_TYPES.length; iType++ )
    {
        var type = BADGE_SELECTOR_TYPES[iType];

        // todo: is there a better way to test for this (to prevent the Combo divider from being added)?
        if ( type == "Pseudo" )
            continue;

        var listBadges = getSelectorBadges( type );

        for ( var iBadge = 0; iBadge < listBadges.length; iBadge++ )
        {
            var badgeID = listBadges[iBadge];

            var listOfOneBadge = [ badgeID ];
            var json = getSortedBadgeRequirements( listOfOneBadge );
            var jsonSections = segmentRequirements2( json.ids, json.classes, youthID, listOfOneBadge );
            var jsonHTML = renderSegments_HTML( jsonSections, youthID, listOfOneBadge, true, true );

            // store the result for later consumption
            mapHtml[badgeID] = jsonHTML;

            if ( jsonHTML.numCells > nCols )
                nCols = jsonHTML.numCells;
        }
    }

    for ( var iType = 0; iType < BADGE_SELECTOR_TYPES.length; iType++ )
    {
        var type = BADGE_SELECTOR_TYPES[iType];

        // todo: is there a better way to test for this (to prevent the Combo divider from being added)?
        if ( type == "Pseudo" )
            continue;

        htmlRows += "<tr><td colspan=" + ( nCols + 2 ) + " class='divider'>" + BADGE_SELECTOR_TYPE_LABELS[iType] + "</td></tr>";

        var listBadges = getSelectorBadges( type );
        for ( var iBadge = 0; iBadge < listBadges.length; iBadge++ )
        {
            var badgeID = listBadges[iBadge];
            var jsonHTML = mapHtml[badgeID];
            var strCells = jsonHTML.reqs;
            if ( strCells.match( /colspan=['"]?\d['"]?/ ) )
                strCells = strCells.replace( /colspan=['"]?\d['"]?/, "colspan=" + nCols );
            else
            {
                var nCells = strCells.match( /<td/g ).length;
                if ( nCells < nCols )
                {
                    var htmlExtra = "<td caption style='border-left:solid 1px #666;' colspan=" + (nCols-nCells) + "/></tr>";
                    strCells += htmlExtra;
                }
            }

            var flag = getBadgeStatus( youthID, badgeID );
            if ( flag == "awarded" )
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/check.gif'/>";
            else if ( flag == "complete" )
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/unawarded.gif'/>";
            else
                strGlyph = "<img class='glyph' style='margin-right:24px;' src='" + BIMG + "/images/blank.gif'/>";

            htmlRows += "<tr badgeid='" + badgeID + "'><td class='" + flag + "'>" + strGlyph + getBadgeName( badgeID ) + "</td><td><div class='button inline' style='padding: 4px 10px; margin: 2px 10px -1px 5px;' onclick='selectAllInBadgeRow(\"" + badgeID + "\");'>All</div></td>" + strCells + "</tr>";
            
        }
    }

    var htmlCaptions = "<td caption colspan=" + nCols + ">Requirements</td>";
    tableReport.append( "<tr><td/><td/>" + htmlCaptions + "</tr>" );
    tableReport.append( htmlRows );
};

function doEditYouth( youthID, bNavigate )
{
    clearActive();

    if ( ! isOnline() )
    {
        openLightBox( { text: "You must be on-line to make changes to account settings.", canClose: true } );
        return;
    }

    $('#joined-date-changer div[data-role=header] a').text( STR_YOUTH );

    // Setting a value to null leaves it untouched (doesn't clear it), so we pre-clear the fields
    $('form[name=youth-edit] input[name=youth-displayname]').val( "" );
    $('form[name=youth-edit] input[name=youth-firstname]').val( "" );
    $('form[name=youth-edit] input[name=youth-lastname]').val( "" );
    $('form[name=youth-edit] input[name=youth-patrolid]').val( "" );
    $('form[name=youth-edit] input[name=login-enabled]').attr( "checked", false );
    $('form[name=youth-edit] input[name=login-email]').val( "" );
    $('form[name=youth-edit] input[name=login-password1]').val( "" );
    $('form[name=youth-edit] input[name=login-password2]').val( "" );
    $('#current-patrol').html( getPatrolName( 0, true ) );
    for ( var patrolID in g_dbTables['Patrols'] )
        $( "#list-patrols li[patrolid=" + patrolID + "]" ).removeClass( "checked" );

    $('input[name=youth-uniformid]').parent().toggle( UNIFORMS.length > 1 );

    var youth = g_dbTables['Youth'][youthID];
    var bNew = youth === undefined;

    if ( bNew )
    {
        setYouthRole( 0 );
        $( "#list-patrols li[patrolid=" + 0 + "]" ).addClass( "checked" );

        $('form[name=youth-edit] input[name=youth-id]').val( 0 );
        setMemberStatus( 1, false );
        toggleInputSwitch( 'youth-active', 0 );
        toggleInputSwitch( 'youth-uniformid', g_listUniformIDs[0] );
        setJoinedDate( formatDate( getServerTimestamp() ) );

        var nYouth = size( g_mapYouthNames );

        if ( nYouth >= g_nMaxYouth )
        {
            $('#youth-edit-save').attr( "disabled", true );
            openLightBox( { text: "You already have the maximum number of " + STR_YOUTHS + " for this account.<br/><br/>Please delete some inactive " + STR_YOUTHS + " first.", canClose: true, size: "big" } );
        }
        else
            $('#youth-edit-save').removeAttr( "disabled" );
    }
    else
    {
        setYouthRole( youth.role );

        $('form[name=youth-edit] input[name=youth-id]').val( youth.id );
        $('form[name=youth-edit] input[name=youth-displayname]').val( youth.displayname );
        $('form[name=youth-edit] input[name=youth-firstname]').val( youth.firstname );
        $('form[name=youth-edit] input[name=youth-lastname]').val( youth.lastname );
        $('form[name=youth-edit] input[name=youth-patrolid]').val( youth.patrolid );
        $('#current-patrol').html( getPatrolName( youth.patrolid, true ) );
        setMemberStatus( youth.active, false );
        toggleInputSwitch( 'youth-active', youth.active ? 0 : 1 );

        var uniformID = getYouthUniform( youth.id );

        toggleInputSwitch( 'youth-uniformid', uniformID );
        setJoinedDate( formatDate( youth.date ) );
        $( "#list-patrols li[patrolid=" + youth.patrolid + "]" ).addClass( "checked" );

        var nYouth = 0;
        if ( g_mapAccessByYouth[youth.id] !== undefined )
            nYouth = g_mapAccessByYouth[youth.id].length;
        $('#youth-edit-logins-count').html( nYouth + "<img width=1 height=18 style='padding-left:2px;vertical-align:-2px;padding-right:7px;' src='" + BIMG + "/images/blank.gif'/>" );

        $('#youth-edit-save a').removeAttr( "disabled" );
    }

    $('#youth-edit-delete').toggle( !bNew );
    $('#youth-edit-logins').toggle( !bNew && getRole() == "v" );
    $('form[name=youth-edit] input[name=login-enabled]').parent().toggle( bNew && getRole() == "v" );
    enableLoginFields( "youth-edit" );
    toggleWelcome( "#edit-youth", true );
    $("#edit-youth div[data-role=header] h1").text( bNew ? ("New " + STR_YOUTH) : ("Edit " + STR_YOUTH) );
    $("#youth-edit-save").html( bNew ? "&nbsp;Create&nbsp;" : "&nbsp;&nbsp;Save&nbsp;&nbsp;" );

    if ( bNavigate )
        addPage( "#edit-youth" );

    toggleWelcomeText( "#edit-youth", getLocalStorage( "welcome-custom-youth" ) );
    $("#bulk-initialization").toggle( youthID != 0 );
};

function doTaxReportDetails( bNavigate, youthID )
{
    clearActive();

    if ( youthID === undefined )
        youthID = $("#tax-details").attr( "youthid" );

    $("#tax-details h1.tax-year").html( "Program-Related Costs for " + g_nTaxationYear + "<span style='font-weight:normal;color:#000;font-style:italic;font-size:90%;'> &ndash; " + htmlEncode( g_mapYouthNames[youthID] ) + "</span>" );
    $("#tax-details").attr( "youthid", youthID );

    var mapOutings = {};

    var d = new Date();
    var nYear = $( "#tax-year-selector" ).val();

    for ( var key in g_mapOutings )
    {
        var outing = g_mapOutings[key];
        if ( outing === undefined || outing == null )
            continue;       // could be gaps in what looks like an array

        outing = filterOutingByYouth( outing, youthID );
        if ( outing == null )
            continue;

        d.setTime( outing.date );
        if ( d.getFullYear() != nYear )
            continue;

        mapOutings["" + outing.id] = outing;
    }

    var listReport = $('#tax-details ul');
    listReport.empty();

    var nLastYear = -1;
    var listOutingIDs = sortOutingsByDate( mapOutings );
    for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
    {
        var outingID = listOutingIDs[iOuting];
        var outing = mapOutings[outingID];

        var nYear = getOutingYear( outing );
        if ( nYear != nLastYear )
        {
            listReport.append( "<li class='divider'>" + nYear + "-" + (nYear+1) + "</li>" );
            nLastYear = nYear;
        }

        listReport.append( "<li class='forward'><a href='javascript:void(0)' onclick='doViewEvent(" + outing.id + ");'><span class='stunted shrink'>" + getOutingName( outing ) + "</span> <small class='counter textonly' " + (outing.cost.program==0?"style='display:none;'":"") + ">$" + outing.cost.program + "</small></a></li>" );
    }

    if ( bNavigate )
        addPage( "#tax-details" );
};

function doSelectTaxYear()
{
    doTaxReport( false );
};

function doTaxReport( bNavigate )
{
    clearActive();

    var d = new Date();
    if ( g_nTaxationYear < 0 )
        g_nTaxationYear = d.getFullYear()-1;       // last year is the taxation year of interest

    var mapYouthCostsByYear = {};
    for ( var key in g_mapOutings )
    {
        var outing = g_mapOutings[key];
        if ( outing === undefined || outing == null )
            continue;       // could be gaps in what looks like an array

        if ( outing.cost === undefined )
            outing.cost = { total: 0, program: 0 };

        d.setTime( outing.date );
        var nYear = d.getFullYear();        // the calendar date of this object  (e.g., 2009)

        if ( mapYouthCostsByYear[nYear] === undefined )
        {
            mapYouthCostsByYear[nYear] = {};
            for ( var youthID in g_mapYouthNames )
                mapYouthCostsByYear[nYear][youthID] = 0;
        }

        var mapYouthCosts = mapYouthCostsByYear[nYear];
        for ( var iYouth = 0; iYouth < outing.youth.length; iYouth++ )
        {
            var youthID = outing.youth[iYouth].id;
            mapYouthCosts[youthID] += parseInt( outing.cost.program );
        }
    }

    if ( bNavigate )
    {
        var listYears = new Array();
        for ( var nYear in mapYouthCostsByYear )
            listYears.push( nYear );
        listYears.sort().reverse();

        $( "#tax-year-selector" ).empty();
        $.each( listYears, function( i, nYear ) {
            $( "#tax-year-selector" ).append( "<option " + (nYear==g_nTaxationYear?"selected":"") + " value='" + nYear + "'>" + nYear + "</option>" );
        });
    }
        
    g_nTaxationYear = $( "#tax-year-selector" ).val();
    $("#tax-report h1.tax-year").html( "Program-Related Costs for " + g_nTaxationYear );

    var mapYouthCosts = mapYouthCostsByYear[g_nTaxationYear];
    for ( var youthID in g_mapYouthNames )
    {
        var youth = g_dbTables['Youth'][youthID];

        var d = new Date();
        d.setTime( youth.date );
        var nEntryYear = d.getFullYear();

        $('#list-youth-tax li[youthid=' + youthID + '] small').text( "$" + mapYouthCosts[youthID] );
        $('#list-youth-tax li[youthid=' + youthID + ']').toggle( g_nTaxationYear >= nEntryYear );
    }

    if ( bNavigate )
        addPage( "#tax-report" );
};

function getLoginName( id )
{
    var strName = g_mapLoginNames[id];
    if ( strName === undefined || ! strName )
    {
        if ( g_mapLoginLeaders[id] !== undefined )
        {
            for ( var leaderID in g_dbTables['Leaders'] )
            {
                if ( g_dbTables['Leaders'][leaderID].loginid == id )
                {
                    strName = g_mapLeaderNames[leaderID];
                    g_mapLoginNames[id] = strName;      // update the list
                    break;
                }
            }
        }
    }

    if ( strName === undefined || ! strName )
        strName = g_mapLoginEmails[id];

    return htmlEncode( strName );
};

function htmlEncode( strText )
{
    if ( strText === undefined || strText == null || strText == "" )
        return strText;

    return strText.replace( /&/g, "&amp;" ).replace( /</g, "&lt;" ).replace( />/g, "&gt;" ).replace( /'/g, "&#39;" );
};

function updateLoginLists( data, callback )
{
    ajaxGetYouthLogins( 3, function( data ) {
        g_mapLoginEmails = {};
        g_mapLoginNames = {};

        //console_log( "updateLoginLists returned youth: " + JSON.stringify( data['Logins'] ) );
        //console_log( "updateLoginLists returned access: " + JSON.stringify( data['Access'] ) );
        //console_log( "updateLoginLists returned leaders: " + JSON.stringify( data['Leaders'] ) );
        for ( var key in data['Logins'] )
        {
            var login = data['Logins'][key];
            if ( login.active != "1" )
                continue;

            g_mapLoginEmails[login.id] = login.email;
            g_mapLoginNames[login.id] = login.displayname;
        }

        g_mapLoginLeaders = {};
        g_mapAccessByLogin = {};
        g_mapAccessByYouth = {};
        g_setBadgemasters = {};

        // sort the logins into leaders and scout/parents
        for ( var keyAccess in data['Access'] )
        {
            var access = data['Access'][keyAccess];
            var id = access.loginid;

            if ( data['Logins'][id] === undefined )
                continue;

            if ( data['Logins'][id].active != 1 )
                continue;

            var youthID = access.youthid;

            if ( youthID <= 0 )
            {
                g_mapLoginLeaders[id] = g_mapLoginEmails[id]
                if ( youthID == "0" )
                    g_setBadgemasters[id] = 1;
            }

            else if ( ! g_mapLoginLeaders[id] )
            {
                if ( data['Youth'][youthID] === undefined )
                {
                    if ( getRole() != "p" )
                        console_warn( "skipping invalid youth " + youthID );
                    continue;
                }

                // do we need to create a map entry for this login?
                if ( ! g_mapAccessByLogin[id] )
                    g_mapAccessByLogin[id] = [];

                // append this youth to the list for this login
                g_mapAccessByLogin[id].push( youthID );

                // do we need to create a map entry for this youthid?
                if ( ! g_mapAccessByYouth[youthID] )
                    g_mapAccessByYouth[youthID] = [];

                // append this login to the list for this youth
                g_mapAccessByYouth[youthID].push( id );
            }
            else
                console_log( "ignoring login " + id + " that is both leader and parent/scout" );
        }

        var listSortedNames = new Array();
        var mapLoginsByName = {};
        for ( var key in data['Logins'] )
        {
            var login = data['Logins'][key];

            if ( login.active != 1 )
                continue;

            var strName = login.displayname;
            if ( ! strName || strName === undefined || strName == "" )
                strName = login.email;
            strName = strName.toLowerCase() + ":" + login.id;

            listSortedNames.push( strName );      // append this name to the end of a simple list
            mapLoginsByName[strName] = login;
        }

        listSortedNames.sort();                   // sort, alphabetically

        var imgPresence = "<img class='glyph' src='./images/blank.gif' style='vertical-align: -2px; margin-right: 8px;'>";
        var listLogins = $('#list-logins-v');
        listLogins.empty();
        for ( var iName = 0; iName < listSortedNames.length; iName++ )
        {
            var login = mapLoginsByName[listSortedNames[iName]];
            var id = login.id;

            if ( ! g_mapLoginLeaders[id] )
                continue;  // we only show leaders in this list

            if ( g_mapLoginEmails[id] == "support@dakemi.com" )
                continue;  // we never show this email, as it's not created by the account owners, and it's only temporary

            var li = document.createElement( "li" );
            li.className = "arrow";
            li.innerHTML = "<a href=\"javascript:void(0);\" onclick=\"doEditLogin('" + id + "',true);\">" + imgPresence + getLoginName(id) + "</a>";
            li.setAttribute( "loginid", id );

            listLogins.append( li );
        }

        listLogins = $('#list-logins-p');
        listLogins.empty();
        var nAdded = 0;
        for ( var iName = 0; iName < listSortedNames.length; iName++ )
        {
            var login = mapLoginsByName[listSortedNames[iName]];
            var id = login.id;

            if ( g_mapLoginLeaders[id] )
                continue;  // we only show scouts/parents in this list

            var strNoAccessClass = "";
            var strNoAccessText = "";
            var strSubtext = "";
            if ( g_mapAccessByLogin[id] === undefined )
            {
                strNoAccessClass = " noaccess";
                strNoAccessText = " <span class='noaccess'>(No accessed " + STR_YOUTHS + ")</span>";
            }
            else
                strSubtext = getAccessedYouthSubtext( data['Access'], id, true );

            var li = document.createElement( "li" );
            li.className = "arrow" + strNoAccessClass;
            li.innerHTML = "<a href=\"javascript:void(0);\" onclick=\"doEditLogin('" + id + "',false);\">" + imgPresence + getLoginName(id) + strNoAccessText + strSubtext + "</a>";
            li.setAttribute( "loginid", id );

            listLogins.append( li );
            nAdded++;
        }

        listLogins.toggle( nAdded > 0 );

        if ( hasUpPoint( "edit-login" ) )
        {
            var id = getEditLoginID();
            var strSubtext = getAccessedYouthSubtext( data['Access'], id, false );
            $('#login-edit-youth-count').css( 'margin-top', strSubtext == "" ? '-26px' : '-32px' );
            $('#login-edit-youth a').html( "Accessed " + STR_YOUTHS + strSubtext );
        }

        listLogins = $('#list-logins-picker');
        listLogins.empty();
        for ( var iName = 0; iName < listSortedNames.length; iName++ )
        {
            var login = mapLoginsByName[listSortedNames[iName]];
            var id = login.id;

            if ( g_mapLoginLeaders[id] )
                continue;  // only show scout/parent logins... leaders can always see everyone

            var li = document.createElement('li');
            li.className = "checker";
            li.setAttribute( "loginid", id );

            li.innerHTML = "<a href='javascript:void(0)' onclick='$(\"#list-logins-picker li[loginid=" + id + "]\").toggleClass(\"selected\");clearActive();'><img glyph src='" + BIMG + "/images/blank.gif'/>" + getLoginName(id) + "</a>";
            listLogins.append( li );
        }

        var leaderID = $('input[name=leader-id]').val();
        var leader = data['Leaders'][leaderID];
        if ( leader && leader.loginid > 0 )
        {
            var login = data['Logins'][leader.loginid];
            if ( login )
                $('#leader-login small').html( login.email );
            else
            {
                $('#leader-login small').html( "None" );
                console_warn( "no login for leader.loginid = " + leader.loginid );
            }
        }
        else
            $('#leader-login small').html( "None" );

        callback();
    });
}

function ajaxGetLogins( callback )
{
    if ( ! isOnline() )
    {
        readYouth( callback );
        updateLoginLists( g_dbTables, function() { /* nop */ } );
        return;
    }

    ajaxGetYouthLogins( 3, function( data ) {
        updateLoginLists( data, callback );
    });
};

function doPickYouth()
{
    var id = getEditLoginID();

    var mapAssociative = {};
    for ( var key in g_mapAccessByLogin[id] )
    {
        var youthID = g_mapAccessByLogin[id][key];
        mapAssociative[youthID] = 1;
    }

    for ( var youthID in g_mapYouthNames )
    {
        if ( mapAssociative[youthID] )
            $( "#list-logins-youth-picker li[youthid=" + youthID + "]" ).addClass( "selected" );
        else
            $( "#list-logins-youth-picker li[youthid=" + youthID + "]" ).removeClass( "selected" );
    }

    addPage( '#login-youth-picker' );
};

function doPickLogins()
{
    var nParentLogins = 0;
    for ( var id in g_mapLoginEmails )
        if ( g_mapLoginLeaders[id] === undefined )
            nParentLogins++;

    if ( nParentLogins == 0 )
    {
        clearActive();
        openLightBox( { text: "No " + STR_YOUTH + "/Parent logins", canClose: true } );
        return;
    }

    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();

    var mapAssociative = {};
    for ( var key in g_mapAccessByYouth[youthID] )
    {
        var id = g_mapAccessByYouth[youthID][key];
        mapAssociative[id] = 1;
    }

    for ( var id in g_mapLoginNames )
    {
        if ( g_mapLoginLeaders[id] )
            continue;  // we only show scout/parent accounts in this list

        if ( mapAssociative[id] )
            $( "#list-logins-picker li[loginid=" + id + "]" ).addClass( "selected" );
        else
            $( "#list-logins-picker li[loginid=" + id + "]" ).removeClass( "selected" );
    }

    addPage( '#youth-logins-picker' );
};

function doSaveLoginYouth()
{
    clearActive();

    var id = getEditLoginID();
    var listActions = new Array();
    var nYouth = 0;

    $('#list-logins-youth-picker li[youthid]').each( function() {
        var youthID = $(this).attr( "youthid" );
        var isFound = false;
        for ( var key in g_mapAccessByLogin[id] )
        {
            if ( g_mapAccessByLogin[id][key] == youthID )
            {
                isFound = true;
                break;
            }
        }
        var isSelected = $(this).hasClass( "selected" );
        if ( isSelected )
            nYouth++;

        if ( isSelected && ! isFound )
        {
            var accessRecord = {
                sectionid: getSection(),
                loginid: id,
                youthid: youthID,
                notify: 1
            }
            listActions.push( "insert=" + JSON.stringify( accessRecord ) );
        }
        if ( ! isSelected && isFound )
        {
            listActions.push( "delete&select=login_id.eq." + id + " AND youth_id.eq." + youthID );
        }
    } );

    openLightBox( { page: "edit-login", text: "Record updated", timeout: listActions.length * 1000 } );

    for ( var key in listActions )
    {
        var strAction = listActions[key];
        jQuery.ajax( {
            url: BADGES_TABLES,
            data: "uid=" + getLoginID() + "&worksheet=Access&" + strAction,
            error: function( request, textStatus, errorThrown ) {
                openLightBox( { text: "Error updating database", canClose: true } );
            },
            success: function( data ) 
            {
                console_log( "access record updated" );
                var qdata = data;

                g_cachedData = null;
                ajaxGetLogins( function() {
                    console_log( "doSaveYouthLogin record updated" );
                });
                $('#login-edit-youth-count').html( nYouth + "<img width=1 height=18 style='padding-left:2px;vertical-align:-2px;padding-right:7px;' src='" + BIMG + "/images/blank.gif'/>" );
            }
        });
    }

    restorePage();
};

function doSaveAccesses()
{
    clearActive();

    var youthID = $('form[name=youth-edit] input[name=youth-id]').val();
    var listActions = new Array();
    var nLogins = 0;

    $('#list-logins-picker li[loginid]').each( function() {
        var id = $(this).attr( "loginid" );
        var isFound = false;
        for ( var key in g_mapAccessByLogin[id] )
        {
            if ( g_mapAccessByLogin[id][key] == youthID )
            {
                isFound = true;
                break;
            }
        }
        var isSelected = $(this).hasClass( "selected" );
        if ( isSelected )
            nLogins++;

        if ( isSelected && ! isFound )
        {
            var accessRecord = {
                sectionid: getSection(),
                loginid: id,
                youthid: youthID,
                notify: "1"
            }
            listActions.push( "insert=" + JSON.stringify( accessRecord ) );
        }
        if ( ! isSelected && isFound )
        {
            listActions.push( "delete&select=login_id.eq." + id + " AND youth_id.eq." + youthID );
        }
    } );

    openLightBox( { page: "edit-youth", text: "Record updated", timeout: listActions.length * 1000 } );

    for ( var key in listActions )
    {
        var strAction = listActions[key];
        jQuery.ajax( {
            url: BADGES_TABLES,
            data: "uid=" + getLoginID() + "&worksheet=Access&" + strAction,
            error: function( request, textStatus, errorThrown ) {
                openLightBox( { text: "Error updating database", canClose: true } );
            },
            success: function( data ) 
            {
                console_log( "youth record updated" );
                g_cachedData = null;
                ajaxGetLogins( function() { console_log( "youth record updated" ); } );
                $('#youth-edit-logins-count').html( nLogins + "<img width=1 height=18 style='padding-left:2px;vertical-align:-2px;padding-right:7px;' src='" + BIMG + "/images/blank.gif'/>" );
            }
        });
    }

    restorePage();
};

/*
 * This is where we get the bulk of the information about youth and logins.  Note that the two are pretty tightly
 * coupled, so that any attempt to get the youth also requires that we pull down the logins and the access tables.
 * Similarly, parent logins are only relevant in terms of the youth they reference.
 *
 * Hence, calls to ajaxGetYouth or ajaxGetLogins actually both end up here.
 *
 * THIS IS SAFE to call multiple times... it uses g_cachedData != null to avoid re-issuing the AJAX call.
 *
 * If this fails, we need to continue to read information from the database, but warn the user that they are 
 * still off-line
 */
function ajaxGetYouthLogins( nRetries, callback )
{
    if ( g_cachedData != null )
        callback( g_cachedData );

    else if ( ! isOnline() )
        console_log( "can't get youth/logins... not online" );

    else
    {
        jQuery.ajax( {
            url: BADGES_TABLES,
            data: "uid=" + getLoginID() + "&sectionid=" + getURLSection() + "&worksheet=Logins,Leaders,Youth,Access,Patrols,Properties",
            timeout: 5000,
            error: function( request, textStatus, errorThrown ) {
                // proceed anyway, but off-line
                if ( --nRetries > 0 )
                {
                    console_trace( "failed to connect to Spreadsheet servlet... retries = " + nRetries );
                    ajaxGetYouthLogins( nRetries, callback )
                }
                else
                {
                    if ( g_isServerReachable ) 
                    {
                        setOnline( false );
                        openLightBox( { text: "Intermittent network connectivity.  Working off-line.", canClose: true } );
                    }
                    readFromDbTables();
                    buildBadgeMap();
                }
            },
            success: function( data ) 
            {
                //console_log( "ajaxGetYouthLogins: ajax returned '" + JSON.stringify( data ) + "'" );
                g_cachedData = data;
                callback( data );
            }
        });
    }
};

function processYouthLogins( data, callback )
{
    // see if the existing current youth exists in the return values
    //console_log( "ajaxGetYouth returned youth: " + JSON.stringify( data['Youth'] ) );
    //console_log( "ajaxGetYouth returned access: " + JSON.stringify( data['Access'] ) );
   
    // get a list of all the youth this login can see
    var allowedYouth = {};
    for ( var key in data['Access'] )
    {
        var access = data['Access'][key];
        if ( access.loginid == getLoginID() )
            allowedYouth[getRole() == "p" ? access.youthid : 0] = 1;
    }

    if ( size( allowedYouth ) == 0 )
    {
        if ( g_isEmbeddedSection == -1 && g_mode == "group" && getLoginID() != null )
        {
            console_warn( "could not find any accessed youth for login " + getLoginID() );
            openLightBox( { text: "There are no " + STR_YOUTHS + " associated with this login.  Contact your " + STR_LEADERS + " for assistance.", canClose: true } );
            doLogout( false );
        }
    }

    // See if the current-youth exists in the access list... if it is, we'll keep it as the current youth,
    // otherwise, we'll have to pick another
    var strNewYouthID = null;
    for ( var youthID in data['Youth'] )
    {
        var youth = data['Youth'][youthID];
        if (  allowedYouth[0] === undefined && allowedYouth[youthID] === undefined )
            continue;

        if ( youth.active == 1 )
        {
            if ( youthID == g_lastYouth )
            {
                strNewYouthID = youthID;            // even if we had found a candidate, this is the one we want
                break;                              // look no further
            }
            else if ( strNewYouthID == null )       // do we need a candidate?
                strNewYouthID = youthID;
        }
    }

    // did we find a suitable candidate?  persist it for next time
    if ( strNewYouthID )
        persistCurrentYouth( strNewYouthID )

    // copy the records into the cached db tables
    g_dbTables['Patrols'] = data['Patrols']

    // copy the records into the cached db tables
    g_dbTables['Leaders'] = data['Leaders']

    // copy the allowed subset of the records into the cached db tables
    var mapYouth = {};
    for ( var youthID in data['Youth'] )
    {
        var youth = data['Youth'][youthID];
        if ( allowedYouth[0] === undefined && allowedYouth[youthID] === undefined )
            continue;

        mapYouth[youthID] = youth;
    }
    g_dbTables['Youth'] = mapYouth;

    // persist the Youth/Access data in the local database
    setLocalStorage( "db-youth", JSON.stringify( g_dbTables['Youth'] ) );
    setLocalStorage( "db-patrols", JSON.stringify( g_dbTables['Patrols'] ) );
    setLocalStorage( "db-leaders", JSON.stringify( g_dbTables['Leaders'] ) );

    readYouth( callback );
};

function ajaxGetYouth( callback )
{
    if ( ! isOnline() )
    {
        readYouth( callback );
        return;
    }

    ajaxGetYouthLogins( 3, function( data ) {
        processYouthLogins( data, callback );
    });
};

// Get a a list of all the badge requirements that can be marked as complete
// (for use with multiMarkComplete and markEntireBadge)
// Any requirement that has a non-zero weight is a candidate for marking complete
function getBadgeRequirements( badgeID )
{
    var listReqs = {};
    for ( var reqID in g_dbTables['Requirements'] )
    {
        var requirement = g_dbTables['Requirements'][reqID];

        if ( requirement.weight > 0 && requirement.badgeid == badgeID )
            listReqs[reqID] = 1;
    }

    return listReqs;
};

function finalizeAward( youthID, badgeID )
{
    window.setTimeout( function() {
        var strNotes = "";
        addAwarded( youthID, badgeID, strNotes, getServerTimestamp(), getLoginID(), true );
        queueTransaction( youthID, badgeID, STATE_SET, FLAG_AWARDED, strNotes, true, getServerTimestamp() );
        populateAlphabetic( g_filter, false, false );
        updateCategory( getBadgeCategory(badgeID) );
    }, 4000 );
};

function markEntireBadge( bFromGUI, bAward )
{
    var requirementID = $('#list-youth-complete').attr( "reqid" );
    var badgeID = g_currentBadgeID;
    clearCurrentRequirementHighlighting();
    var youthID = g_currentUser;            // GUI operates on current user

    if ( ! ensureUpdatable() )
        return;

    if ( ! ensureLoggedIn() )
        return;

    if ( isBadgeAwarded( youthID, badgeID ) ) 
    {
        openLightBox( { text: "This badge has already been awarded.", canClose: true } );
        return;
    }

    workerPause();

    var isCurrentlyComplete = isBadgeComplete( youthID, badgeID );
    if ( bFromGUI )
    {
        if ( ! isCurrentlyComplete )
        {
            var isPartiallyComplete = false;
            for ( var reqID in g_setMarkedRequirements[youthID] )
            {
                if ( deriveBadgeID( reqID ) == badgeID )
                {
                    isPartiallyComplete = true;
                    break;
                }
            }

            if ( isPartiallyComplete )
                if ( ! confirm( "Are you sure you want to mark all requirements as complete?" ) )
                    return;
        }
        else if ( ! confirm( "Are you sure you want to mark all requirements as incomplete?" ) )
            return;
    }

    var strNotes = "";
    var listReqs = getBadgeRequirements( badgeID );
    for ( var reqID in listReqs )
    {
        var jsonCompletion = g_mapRequirementCompletion['illegal_bogus_value']; // force it to be undefined, for now
        if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][reqID] !== undefined )
            jsonCompletion = g_mapRequirementCompletion[youthID][reqID];

        if ( ! isCurrentlyComplete )
        {
            if ( jsonCompletion === undefined || jsonCompletion.complete < jsonCompletion.total )
            {
                //console_log( "marking requirement '" + reqID + "' as complete" );
                addRequirementComplete( youthID, reqID, strNotes, true, false, getServerTimestamp(), getLoginID(), true );
            }
        }
        else
        {
            if ( g_setMarkedRequirements[youthID][reqID] !== undefined )
            {
                //console_log( "marking requirement '" + reqID + "' as incomplete" );
                clearRequirementComplete( youthID, reqID, strNotes, true, false, true );
            }
        }

        updateSelection( reqID );
    }

    if ( bAward )
        finalizeAward( youthID, badgeID );

    scheduleSync( "sync'd by entire", POST_UPDATE_INTERVAL );

    processDirtyFlags( youthID, false, true, true );

    updateFloaty( requirementID );

    populateAlphabetic( g_filter, false, false );
    updateCategory( getBadgeCategory(badgeID) );

    restorePage();
};

function multiMarkComplete( bMarkAsComplete, bMarkEntire )
{
    var reqID = $('#list-youth-complete').attr( "reqid" );
    clearCurrentRequirementHighlighting();

    if ( ! ensureUpdatable() )
        return;

    var badgeID = g_currentBadgeID;

    var listReqs = {};
    if ( bMarkEntire )
        listReqs = getBadgeRequirements( badgeID );
    else
        listReqs[reqID] = 1;

    setMultiYouth = {};
    $('#list-youth-complete li.selected[youthid]').each( function() {
        var youthID = $(this).attr( "youthid" );
        setMultiYouth[youthID] = 1;
    });

    var nYouth = size( setMultiYouth );
    // conditionally display a confirmation dialog
    var strConfirmMessage = null;
    if ( bMarkEntire )
    {
        if ( bMarkAsComplete )
            strConfirmMessage = "Are you sure you want to mark all requirements as complete for the %1 selected " + STR_YOUTHS + "?";
        else
            strConfirmMessage = "Are you sure you want to mark all requirements as incomplete for the %1 selected " + STR_YOUTHS + "?";

        strConfirmMessage = strConfirmMessage.replace( /%1/, nYouth );
    }

    if ( strConfirmMessage != null && ! confirm( strConfirmMessage ) )
        return;

    restorePage();   // go back

    var strNotes = "";
    for ( var youthID in setMultiYouth )
    {
        for ( var requirementID in listReqs )
        {
            var jsonCompletion = g_mapRequirementCompletion['illegal_bogus_value']; // force it to be undefined, for now
            if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][requirementID] !== undefined )
                jsonCompletion = g_mapRequirementCompletion[youthID][requirementID];

            if ( bMarkAsComplete )
            {
                if ( jsonCompletion === undefined || jsonCompletion.complete < jsonCompletion.total )
                    addRequirementComplete( youthID, requirementID, strNotes, true, false, getServerTimestamp(), getLoginID(), true );
            }
            else
            {
                if ( g_setMarkedRequirements[youthID] !== undefined && g_setMarkedRequirements[youthID][requirementID] !== undefined )
                    clearRequirementComplete( youthID, requirementID, strNotes, true, false, true );
            }

            if ( youthID == g_currentUser )
                updateSelection( requirementID );
        }

        unscorecard( youthID );
    }

    if ( setMultiYouth[g_currentUser] != undefined )
        scorecard( g_currentUser );       // restore cache for current user

    scheduleSync( "sync'd by multi", POST_UPDATE_INTERVAL );

    updateFloaty( reqID );

    populateAlphabetic( g_filter, false, false );
    updateCategory( getBadgeCategory(badgeID) );
};

function setMode( bResync )
{
    clearActive();

    var modeID = getLoginID() != null || g_isEmbeddedSection != -1 ? "group" : "handbook";

    if ( modeID == "group" && isAppleMobileDevice() && g_noJQT )
    {
        openLightBox( { text: "Sorry, this device cannot support an Account", canClose: true } );
        doLogout( false );
        return;     // presumably, there were no changes
    }

    var bChanged = modeID != g_mode;

    g_mode = modeID;
    persistProperty( 'mode', g_mode );

    var strNewYouthID = "0";

    if ( g_mode == "handbook" )
    {
        workerPause();

        if ( g_currentUser != strNewYouthID )
            setCurrentYouth( strNewYouthID );
    }
    else
    {
        if ( bChanged && bResync )
        {
            if ( isOnline() )
            {
                workerPause();

                window.setTimeout( function() {
                    ajaxSyncTransactions( true, function() {
                        strNewYouthID = g_lastYouth;

                        if ( ! getLoginID() )
                            setCurrentYouth( "0" );
                        else if ( g_currentUser != strNewYouthID ) 
                            setCurrentYouth( strNewYouthID );
                        else
                            closeLightBox( 1000 );
                        console_log( "setMode sync" );
                    } );
                }, 1000 );
            }
            else
            {
                if ( ! getLoginID() )
                    setCurrentYouth( "0" );
                else if ( g_currentUser != strNewYouthID ) 
                    setCurrentYouth( g_lastYouth );
                else
                    closeLightBox( 1000 );
                console_log( "setMode sync (off-line)" );
            }
        }
    }

    updateLoginDisplay();

    if ( bChanged )
        readEvents( updateEventSelector );
};

function setCurrentYouth( youthID )
{
    if ( isNaN( youthID ) )
        g_currentUser = parseInt( youthID );
    else 
        g_currentUser = youthID;

    $('#scout-picker li.selection').removeClass( "checked" );
    $('#scout-picker li.selection[youthid=' + g_currentUser + ']').addClass( "checked" );
    if ( g_mode == 'group' )
        persistCurrentYouth( g_currentUser );

    $("#current-youth").html( htmlEncode( g_mapYouthNames[g_currentUser] ) + "" );
    $("#badge-status-youth-0").text( g_mapYouthNames[g_currentUser] );
    $("#badge-status-youth-1").text( g_mapYouthNames[g_currentUser] );
    $("#floaty-current-youth").text( g_mapYouthNames[g_currentUser] );
    $("#floaty-current-youth + ul.rounded").css( "margin-top", g_mode == "handbook" ? "20px" : "8px" );

    $("#snapshot-name select").val( youthID );

    var strQuickLinks = "Quick Links";
    if ( size( g_mapYouthNames ) > 1 )
        strQuickLinks += "<span style='font-weight:normal;color:#000;font-style:italic;font-size:90%;'> &ndash; " + htmlEncode( g_mapYouthNames[youthID] ) + "</span>";
    $('#quick-links').html( strQuickLinks );

    scorecard( youthID, true );
};

function switchYouth( youthID )
{
    if ( youthID != g_currentUser )
    {
        openLightBox( { text: "Switching to<div style='padding-top:5px;font-weight:bold;'>" + htmlEncode( g_mapYouthNames[youthID] ) + "</div>" } );

        clearCurrentRequirementHighlighting();      // unhighlight and get rid of floaty
        setCurrentYouth( youthID );
    }


    // don't save the up point if we're on the details page... otherwise it screws up the restorePage logic
    if ( ! getCurrentPage().match( /^badge-details-/ ) )
        saveUpPoint();

    return true;
};

function processBadgeReportResults( mapSteps, strLabel )
{
    var listReport = $('#report-body');
    listReport.empty();

    var foundSomething = false;
    var showIcons = false;

    // "who's working on what" and "Ready to Award" reports has many badges... and we are interested in seeing by badge or by youth
    // "By Badge" has only only one badge... and we are interested in seeing youth grouped by patrol
    var groupByPatrol = size( mapSteps ) == 1;

    // possible recast the map by patrol
    var masterBadgeID = null;
    for ( var badgeID in mapSteps )
        masterBadgeID = badgeID;

    var select = $('#report-subselector select');

    var listDividers = null;
    var mapDividers = {};
    if ( groupByPatrol )
    {
        if ( select.attr( "badgeid" ) != masterBadgeID )
        {
            select.empty();
            var htmlSelect = "<option value=\"\">All</option>";

            // sort the requirements
            var listRequirementIDs = new Array();
            for ( var requirement in g_mapBadgeRequirementIDs[masterBadgeID] )
            {
                var reqID = g_mapBadgeRequirementIDs[masterBadgeID][requirement];
                if ( getRequirementWeight( reqID ) <= 0 )
                    continue;

                var strRequirementID = requirement;     // make a copy
                if ( strRequirementID.match( /^([A-Z]?)(\d[a-z]?)$/ ) )
                    strRequirementID = RegExp.$1 + "0" + RegExp.$2;

                listRequirementIDs.push( strRequirementID );
            }
            listRequirementIDs.sort();

            var strRows = "";
            for ( var i in listRequirementIDs )
            {
                var requirement = listRequirementIDs[i].replace(/^([A-Z]?)0/,"$1");
                var reqID = g_mapBadgeRequirementIDs[masterBadgeID][requirement];
                if ( getRequirementWeight( reqID ) > 0 )
                {
                    var strDescription = simplifyRequirementDescription( g_dbTables['Requirements'][reqID].description );
                    htmlSelect += "<option value=\"" + requirement + "\">" + requirement + ". " + strDescription.substring( 0,32 ) + "</option>";
                }
            }
            select.html( htmlSelect );
            select.val('');        // set the selection to the first value ('all')
            select.attr( "badgeid", masterBadgeID );
        }

        var listPatrols = [ 0 ];
        for ( var patrolID in g_dbTables['Patrols'] )
            listPatrols.push( patrolID );

        for ( var iPatrol = 0; iPatrol < listPatrols.length; iPatrol++ )
        {
            var patrolID = listPatrols[iPatrol];
            mapDividers[patrolID] = {};
            for ( var youthID in g_mapYouthNames )
            {
                if ( g_setActiveYouth[youthID] === undefined )
                    continue;

                if ( g_dbTables['Youth'][youthID].patrolid == patrolID )
                    mapDividers[patrolID][youthID] = 1;
            }
        }

        $('#report-header').html( g_dbTables['MetaData'][masterBadgeID].name );
        listDividers = sortPatrols();
    }
    else
    {
        listDividers = getSteps();
        mapDividers = mapSteps;

        if ( select.attr( "badgeid" ) != masterBadgeID )
        {
            select.empty();
            var htmlSelect = "<option value=\"\">All</option>";
            select.html( htmlSelect );
            select.val('');        // set the selection to the first value ('all')
            select.attr( "badgeid", masterBadgeID );
        }
    }

    for ( var iDivider = 0; iDivider < listDividers.length; iDivider++ )
    {
        var dividerID = listDividers[iDivider];

        if ( mapDividers[dividerID] === undefined )
            continue;
        if ( size( mapDividers[dividerID] ) == 0 )
            continue;       // skip this empty group

        var badgeID = groupByPatrol ? masterBadgeID : dividerID;

        if ( groupByPatrol )
            listReport.append( "<li class='divider'>" + getPatrolName( dividerID, true ) + "</li>" );
        else
            listReport.append( "<li class='divider'>" + g_dbTables['MetaData'][badgeID].name + "</li>" );


        var listYouth = sortMembersByName( "Youth", mapDividers[dividerID] );

        for ( var iYouth = 0; iYouth < listYouth.length; iYouth++ )
        {
            var youthID = listYouth[iYouth];
            var youth = g_dbTables['Youth'][youthID];
            if ( youth === undefined )
                continue;

            if ( youth.active == 0 )
            {
                console_log( "not active!" );
                continue;       // we only care about active scouts
            }

            var flag = null;
            var nPercent = 0;

            if ( select.val() == "" ) 
            {
                flag = getBadgeStatus( youthID, badgeID );

                if ( g_mapBadgeCompletion[youthID] != undefined && g_mapBadgeCompletion[youthID][badgeID] )
                {
                    var jsonCompletion = g_mapBadgeCompletion[youthID][badgeID];
                    nPercent = asPercentage( jsonCompletion );
                }
            }
            else
            {
                var reqID = getRequirementID( badgeID, select.val() );
                flag = getRequirementStatus( youthID, reqID );
                if ( flag == "complete" || flag == "implicit" )        // just show these as awarded (simple check box)
                    flag = "awarded";

                if ( g_mapRequirementCompletion[youthID] != undefined && g_mapRequirementCompletion[youthID][reqID] )
                {
                    var jsonCompletion = g_mapRequirementCompletion[youthID][reqID];
                    nPercent = asPercentage( jsonCompletion );
                }
            }

            if ( flag == "awarded" )
            {
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/check.gif'/>";
                flag = "complete";
                showIcons = true;
            }
            else if ( flag == "complete" )
            {
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/unawarded.gif'/>";
                showIcons = true;
            }
            else if ( flag == "ready" )
            {
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/readybadge.gif'/>";
                showIcons = true;
            }
            else if ( flag == "favourite" )
            {
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:4px;' src='" + BIMG + "/images/star.gif'/>";
                showIcons = true;
            }
            else
                strGlyph = "<img class='glyph' style='margin-right:24px;' src='" + BIMG + "/images/blank.gif'/>";

            var strTarget = "";
            // SECTION specific
            if ( g_report == "nextsteps" )
            {
                if ( true )
                    strTarget = "populateMyPath(true, false, \"Report\");";

                else if ( badgeID == "venturer" )
                    strTarget = "addPage2(\"#venturer-category-company\",\"Report\");";
                else if ( badgeID == "queensventurer" )
                    strTarget = "addPage2(\"#queensventurer-category-company\",\"Report\");";

                else if ( badgeID == "voyageur" )
                    strTarget = "addPage2(\"#voyageur-category-troop\",\"Report\");";
                else if ( badgeID == "pathfinder" )
                    strTarget = "addPage2(\"#pathfinder-category-troop\",\"Report\");";
                else if ( badgeID == "chiefscoutsaward" )
                    strTarget = "addPage2(\"#chiefscouts-category-troop\",\"Report\");";
            }

            if ( strTarget == "" )
                strTarget = "setCurrentBadge(\"" + badgeID + "\",true,\"dissolve\");";

            var strYouthName = htmlEncode( getDisplayName( youth, youthID ) );

            var li = document.createElement( 'li' );
            li.className = "forward";
            li.innerHTML = "<a youthid='" + youthID + "' class='" + flag + "' href='javascript:void(0);' onclick='switchYouth(" + youthID +"); window.setTimeout(function(){" + strTarget + "},1000);'><span class='stunted'>" + strGlyph + strYouthName + "</span> <small class='counter'></small></a>";

            listReport.append( li );

            annotateYouthPercentage( youthID, badgeID, nPercent );

            foundSomething = true;
        }
    }

    listReport.toggle( foundSomething );
    $('#report-noresults').toggle( ! foundSomething );
    $('#report-noresults').text( "No next steps could be determined." );
    $('#report div.page-content .glyph').toggle( showIcons );

    $("#report .name").text( strLabel );

    $('#report-instructions').hide();
    if ( groupByPatrol )
        $('#report-header').toggle( foundSomething );

    $('#report-body').css( 'margin-top', groupByPatrol?'0px':'15px' );
    $('#report-groupby').hide();
    $('#report-buttons').hide();
    $('#report-body-events').hide();
    $('#report-wait').hide();
};

function simplifyRequirementDescription( strDescription )
{
    // strip out all the links
    var listLinks = new Array();
    strDescription = strDescription.replace( /\n/g, ' ' );
    while ( strDescription.match( /(\[\[.+?\]\])/ ) )
    {
        var strLink = RegExp.$1;

        var strPlaceholder = "~%" + listLinks.length + "%~";
        strDescription = strDescription.replace( /(\[\[.+?\]\])/, strPlaceholder );

        // external links #1 (href + text)
        strLink = strLink.replace( /\[\[(http[^|]+?)\|(.+?)\]\]/g, "$2" );
        // external links #2 (href only)
        strLink = strLink.replace( /\[\[(http[^|]+?)\]\]/g, "$1" );
        // external links (relative) #1 (href + text)
        strLink = strLink.replace( /\[\[(\/[^|]+?)\|(.+?)\]\]/g, "$2" );
        // internal links #1 (always href + text)
        strLink = strLink.replace( /\[\[badge:([^|]+?)\|(.+?)\]\]/g, "$2" );
        // internal links #2 (always href + text)
        strLink = strLink.replace( /\[\[category:([^|]+?)\|(.+?)\]\]/g, "$2" );
        // internal counts #3
        strLink = strLink.replace( /\[\[category-count:(.+?)\]\]/, "" );
        strLink = strLink.replace( /\[\[complete:(.+?)\]\]/g, "" );

        listLinks.push( { link: strLink, re: new RegExp( strPlaceholder ) } );
    }

    // restore all the links
    while ( listLinks.length > 0 )
    {
        var placeholder = listLinks.shift();
        strDescription = strDescription.replace( placeholder.re, placeholder.link );
    }

    // replace dashes with bullet symbol
    strDescription = strDescription.replace( /-\)/g, " &bull; " );

    // replace <li> with bullet symbol
    strDescription = strDescription.replace( /<li.*?>/g, " &bull; " );

    // strip out all HTML
    return stripHTML( strDescription );
};

function stripHTML( html )
{
    var tmp = document.createElement("DIV");
    tmp.innerHTML = html;
    return tmp.textContent || tmp.innerText;
};

/** 
 * Split a list of requirements in to segments.
 *
 * [
 *   {
 *       partid: "A",
 *       displayname: "Skills",
 *       requirements: [    // in display order
 *           {
 *              id: "mybadge.A1a",
 *              label: "1a",
 *              status: "complete|implicit|partial|favourite|ready|na",
 *              dependent: false
 *           },
 *           {
 *              id: "mybadge.A1b",
 *              label: "1b",
 *              status: "complete|implicit|partial|favourite|ready|na",
 *           },
 *           {
 *              id: "mybadge.A2",
 *              label: "2",
 *              status: "complete|implicit|partial|favourite|ready|na"
 *              dependent: false
 *           }
 *                 :    // etc
 *       ]
 *   },
 *   {
 *       id: "mybadge.B",
 *       displayname: "Knowledge",
 *       requirements: [
 *           {
 *              id: "mybadge.B1",
 *              label: "1",
 *              status: "complete|implicit|partial|favourite|ready|na"
 *              dependent: true
 *           },
 *                 :    // etc
 *       ]
 *   },
 *       : // etc
 * ],
 */
function segmentRequirements2( listRequirementIDs, listRequirementClasses, youthID, listBadges )
{
    var listSections = new Array();
    var jsonSection = { requirements: new Array() };
    var lastBadgeID = "";

    // SCOUT-specific
    // special handling for lanyards
    var isExpandable = getCurrentPage() != "edit-youth" && getCurrentPage() != "initialize-youth-records" && ! getInPlaceEdit();

    if ( isExpandable && (listRequirementIDs[0] == 'brownlanyard.1' || listRequirementIDs[0] == 'greenlanyard.1' || listRequirementIDs[0] == 'whitelanyard.1') )
    {
        listSections.push( {
            id: listRequirementIDs[0] + "a",
            displayname: "Badges",
            requirements: new Array()
        } );
        listSections.push( {
            id: listRequirementIDs[0] + "b",
            displayname: "Categories",
            requirements: new Array()
        } );

        var nRequiredBadges = 6;
        var nRequiredCategories = 2;
        if ( g_dbTables['Requirements'][listRequirementIDs[0]].autocompletion.match( /badgecount:(\d+)-(\d+)/ ) )
        {
            nRequiredBadges = RegExp.$1;
            nRequiredCategories = RegExp.$2;
        }

        var nBadges = 0;
        var nCategories = 0;

        if ( g_mapCategoryCounts[youthID] !== undefined )
        {
            for ( var key in g_mapCategoryCounts[youthID] )
            {
                nBadges += g_mapCategoryCounts[youthID][key];
                if ( g_mapCategoryCounts[youthID][key] > 0 )
                    nCategories++;
            }
        }

        for ( var i = 1; i <= nRequiredBadges; i++ )
        {
            var status = "na";
            listSections[0].requirements.push( {
                id: listRequirementIDs[0] + "a" + i,
                label: i,
                status: i <= nBadges ? "complete" : "incomplete",
                dependent: false,
                tooltip: "Badge"
            } );
        }
        for ( var i = 1; i <= nRequiredCategories; i++ )
        {
            var status = "na";
            listSections[1].requirements.push( {
                id: listRequirementIDs[0] + "b" + i,
                label: i,
                status: i <= nCategories ? "complete" : "incomplete",
                dependent: false,
                tooltip: "Category"
            } );
        }

        return listSections;
    }

    for ( var iReq = 0; iReq < listRequirementIDs.length; iReq++ )
    {
        var req = g_dbTables['Requirements'][listRequirementIDs[iReq]];
        if ( req === undefined ) console_trace( "wowot! no req for " + iReq + ": '" + listRequirementIDs[iReq] );
        var badgeID = req.badgeid;
        var parentReqID = deriveParentID( req.id );              // strip of the subrequirement letter

        var strRequirement = req.requirement.replace( /^[A-Z](.*)/, "$1" );     // strip off initial "A", "B", etc.

        if ( strRequirement.match( /^\d+$/ ) && isSubReqd( parentReqID ) )      // skip subreq'd parents
            continue;

        else if ( strRequirement != "" && ! strRequirement.match( /^\d+$/ ) && ! isSubReqd( parentReqID ) ) // skip requirements like "A4b" or "1c" (if we're not subreq'd)
            continue;

        else if ( strRequirement == "" )             // did we find a new section?
        {
            if ( listBadges.length == 1  )
            {
                if ( jsonSection.displayname !== undefined )
                    listSections.push( jsonSection );

                var strDisplayName = req.description;
                if ( strDisplayName === undefined )
                    strDisplayName = "";

                strDisplayName = strDisplayName.replace( /^[A-Z][.]\s+/, "" );         // strip off "A. (Knowledge)"
                strDisplayName = strDisplayName.replace( /^Part\s+\w+\s*-\s*/, "" );   // strip off "Part A - (Knowledge)"
                strDisplayName = strDisplayName.replace( /\s*-.+/, "" );               // strip off "(Skills) - in moving water..."
                strDisplayName = strDisplayName.replace( /\s*<p>.+/, "" );               // strip off "<p>In moving water..."
                strDisplayName = strDisplayName.replace( /([^:]+?):.+/, "$1" );        // strip off "(Initiative): do one: of the following"

                jsonSection = { // start a new section
                    id: req.id,
                    displayname: strDisplayName,
                    requirements: new Array()
                };
            }

            continue;                                                           // skip the rest of the processing
        }

        else if ( badgeID != lastBadgeID && listBadges.length > 1  )            // did we find a new badge?
        {
            if ( jsonSection.displayname !== undefined )
                listSections.push( jsonSection );

            var strDisplayName = getBadgeName( badgeID ).replace( /^.*-\s*(.+)/, "$1" ); // strip off "Voyageur - (Citizenship)"

            jsonSection = { // start a new section
                id: badgeID,
                displayname: strDisplayName,
                requirements: new Array()
            };

            lastBadgeID = badgeID;

            if ( strRequirement == "" )                                         // the first req could be a section
                continue;
        }

        // below this point, we have a requirement that we care about
        var jsonRequirement = { 
            id: req.id,
            label: strRequirement
        }

        if ( youthID > 0 )
        {
            var flag = getRequirementStatus( youthID, req.id );

            var parentReqID = deriveParentID( req.id );

            // is this is a subreq, and the parent was marked as complete?
            if ( parentReqID != req.id && isSubReqd( parentReqID ) && getRequirementStatus( youthID, parentReqID ) == "complete" )
            {
                jsonRequirement.status = "na";
                jsonRequirement.label = DONTCARE;
            }
            else if ( flag == "complete" || flag == "implicit" )
                jsonRequirement.status = flag;
            else if ( ! isRequirementNeeded( youthID, req.id ) )
                jsonRequirement.status = "na";
            else
                jsonRequirement.status = flag;
        }
        else
            jsonRequirement.status = "incomplete";      // actually, not needed for the headers

        jsonRequirement.tooltip = htmlEncode( simplifyRequirementDescription( req.description.replace( /<\/?[bi]>/g, "" ) ) );
        if ( hasSubRequirements( req.id ) )
        {
            var subreq = simplifyRequirementDescription( g_dbTables['Requirements'][req.id+'a'].description.replace( /<\/?[bi]>/g, "" ) );
            if ( subreq.length > 40 )
                subreq = subreq.substring( 0, 40 ) + "...";
            jsonRequirement.tooltip += "&#10;&nbsp;&nbsp;a) " + htmlEncode( subreq );
            jsonRequirement.tooltip += "&#10;&nbsp;&nbsp;b) ...";
        }

        jsonRequirement.dependent = listRequirementClasses[req.id] == "or";
        jsonSection.requirements.push( jsonRequirement );
    }

    listSections.push( jsonSection );                                           // append the last section we were working on

    // now, collapse any sections that don't restart numbering (e.g., "A1,A2" + "B3,B4" + "C5")
    var isCollapsable = listBadges.length == 1 ? true : false;
    for ( var iSection = 1; iSection < listSections.length; iSection++ )
    {
        // if a section starts with a "1" or "1a", we have to keep the sections
        if ( listSections[iSection].requirements[0].label.match( /^1a?$/ ) )
        {
            isCollapsable = false;
            break;
        }
    }

    if ( isCollapsable )
    {
        for ( var iSection = 1; iSection < listSections.length; iSection++ )
            listSections[0].requirements = listSections[0].requirements.concat( listSections[iSection].requirements );

        jsonSection = {
            requirements: listSections[0].requirements
        };
        listSections = new Array();
        listSections.push( jsonSection );
    }

    return listSections;
};

function isRequirementAddressedByOuting( outing, reqID ) 
{
    for ( var iLink = 0; iLink < outing.links.length; iLink++ )
        if ( outing.links[iLink] == reqID )
            return true;

    return false;
};

function renderSegmentsUnresolved_HTML( listSections, outing )
{
    var isPastOuting = isPast( outing );

    var nClickables = 0;
    var htmlReqs = "";
    for ( var iSection = 0; iSection < listSections.length; iSection++ )
    {
        var section = listSections[iSection];

        if ( section.requirements.length == 0 ) // handle under-construction badges
            continue;

        if ( htmlReqs != "" )
            htmlReqs += "<td class='unresolved'/>";

        var htmlSectionReqs = "";
        for ( var iReq = 0; iReq < section.requirements.length; iReq++ )
        {
            var req = section.requirements[iReq];

            var strText = "";
            var strAttr = "";
            if ( isRequirementAddressedByOuting( outing, req.id ) )
            {
                var strTooltip = "";
                var strAnnotation = "";
                var strImage = "";
                if ( ! isPastOuting ) 
                {
                    strImage = "<img src='" + BIMG + "/images/planned.gif'/>";
                    if ( isTallyReq( req.id ) )
                    {
                        strTooltip = "The selected event may count towards this requirement";
                        strAnnotation = "+";
                    }
                    else
                    {
                        strTooltip = "The selected event may complete this requirement";
                        strAnnotation = YES;
                    }
                }
                else if ( isOutingUnresolved( outing, false ) )
                {
                    strImage = "<img style='height:18px;' src='" + BIMG + "/images/questionmark.gif'/>";
                    if ( isTallyReq( req.id ) )
                    {
                        strTooltip = "The selected event may have counted towards this requirement";
                        strAnnotation = "+";
                    }
                    else
                    {
                        strTooltip = "The selected event may have completed this requirement";
                        strAnnotation = YES;
                    }
                    strAttr = " r='" + iSection + "-" + req.label + "' onclick='selectColumn(\"" + iSection + "-" + req.label + "\",true)'";
                }
                else
                {
                    if ( isTallyReq( req.id ) )
                    {
                        strTooltip = "The selected event counted towards this requirement";
                        strAnnotation = "+";
                    }
                    else
                    {
                        strTooltip = "The selected event completed this requirement";
                        strAnnotation = YES;
                    }
                    strAttr = " r='" + iSection + "-" + req.label + "' onclick='selectColumn(\"" + iSection + "-" + req.label + "\",true)'";
                }

                if ( strImage != "" || strAttr != "" )
                    strText = "<span style='white-space:nowrap;' title='" + strTooltip + "'>" + strImage + "<span y>" + strAnnotation + "</span></span>";

               nClickables++;
            }
                     
            htmlSectionReqs += "<td class='unresolved'" + strAttr + ">" + strText + "</td>";
        }
        htmlReqs += htmlSectionReqs;
    }

    if ( nClickables == 0 )
        return "";

    return htmlReqs;
};

function renderSegments_HTML( listSections, youthID, listBadges, isInterative, allowInPlaceEdit )
{
    var htmlCaptions = "";
    var nCells = listSections.length-1;
    for ( var iSection = 0; iSection < listSections.length; iSection++ )
        nCells += listSections[iSection].requirements.length;

    if ( nCells <= 0 )      // handle under-construction badges
        nCells = 0;

    for ( var iSection = 0; iSection < listSections.length; iSection++ )
    {
        var section = listSections[iSection];

        if ( section.requirements.length == 0 ) // handle under-construction badges
            continue;

        if ( htmlCaptions != "" )
            htmlCaptions += "<td caption spacer></td>";

        var strDisplayName = section.displayname;
        if ( strDisplayName === undefined )
            strDisplayName = isInterative ? "Requirements" : "";
        else if ( strDisplayName == "Compulsory Targets" )      // SCOUT_AU specific
            strDisplayName = "Compulsory";
        else if ( strDisplayName == "Elective Targets" )        // SCOUT_AU specific
            strDisplayName = "Elective";

        htmlCaptions += "<td caption colspan=" + section.requirements.length + ">" + strDisplayName + "</td>";
    }

    if ( htmlCaptions == "" )       // handle under-construction badges
        htmlCaptions = listSections.length == 1 ? "" : "<td caption>Requirements</td>";

    var htmlReqs = "";
    if ( youthID > 0 && listBadges.length == 1 )
    {
        var isAwarded = isBadgeAwarded( youthID, listBadges[0] );
        var isComplete = isBadgeComplete( youthID, listBadges[0] );

        if ( isAwarded || isComplete )
        {
            // span all N sections (which includes the N-1 spacers)
            htmlReqs += "<td colspan=" + nCells + " y awarded>" +  (isAwarded?"Awarded":"Complete") + "</td>";
        }
    }

    if ( htmlReqs == "" )
    {
        var outing = getOuting( getCurrentRelatedEvent() );
        var iBadge = 0;
        for ( var iSection = 0; iSection < listSections.length; iSection++ )
        {
            var section = listSections[iSection];

            if ( section.requirements.length == 0 ) // handle under-construction badges
                continue;

            if ( htmlReqs != "" )
                if ( youthID > 0 )
                    htmlReqs += "<td spacer></td>";
                else
                    htmlReqs += "<td spacer style='border:none;'></td>";

            var badgeID = deriveBadgeID( section.requirements[0].id );

            var isAwarded = isBadgeAwarded( youthID, badgeID );
            var isComplete = isBadgeComplete( youthID, badgeID );

            if ( youthID > 0 && ( isAwarded || isComplete ) )
                htmlReqs += "<td colspan=" + section.requirements.length + " y awarded>" +  (isAwarded?"Awarded":"Complete") + "</td>";

            else
            {
                var htmlSectionReqs = "";
                for ( var iReq = 0; iReq < section.requirements.length; iReq++ )
                {
                    var p = "";
                    if ( outingHasYouth( outing, youthID ) )
                        p = " ptcp=\"1\" ";

                    var req = section.requirements[iReq];
                    var flag = req.status;

                    var strText = req.label;        // by default we want the "1" or "2c" 
                    var s = "";

                    var strClass = "";
                    var canEdit = false;
                    var isPreset = false;
                    if ( youthID == 0 )
                    {
                        canEdit = false;
                        strText = strText;
                    }
                    else if ( flag == "complete" || flag == "implicit" )
                    {
                        if ( ! allowInPlaceEdit )
                        {
                            strText = YES;       // replace the "1" or "2c" with a checkmark
                            strClass = "y";
                        }
                        else
                        {
                            if ( flag == "complete" )
                            {
                                var strMark = "<span y>" + YES + "</span>";
                                var w = g_mapAutocompletion[req.id] === undefined ? "": (isTallyReq( req.id ) ? "" : " auto");
                                strText = strMark + "<span" + w + ">" + strText + "</span>";
                                canEdit = true;
                                isPreset = true;

                                var requirement = g_dbTables['Requirements'][req.id];
                                if ( requirement.requirement.match( /^([A-Z])/ ) )
                                    s = " q='" + RegExp.$1 + "' ";
                            }
                            else
                            {
                                strText = DONTCARE;    // replace the "1" or "2c" with a "don't care" dash
                                strClass = "na";
                            }
                        }
                    }
                    else if ( ! allowInPlaceEdit && flag == "na" )
                    {
                        strText = DONTCARE;
                        strClass = "na";
                    }
                    else if ( ! allowInPlaceEdit )
                    {
                        if ( flag == "ready" && isInterative )
                            strText = "<img style='height:18px;' src='" + BIMG + "/images/readybadge.gif'/>";   // replace with image
                        else if ( g_mapUpcomingReqs[req.id] !== undefined && g_mapUpcomingReqs[req.id].length > 0 && isInterative )
                            strText = "<img style='height:13px;' src='" + BIMG + "/images/planned.gif'/>";         // replace with image
                        else if ( flag == "favourite" && isInterative )
                            strText = "<img style='height:16px;' src='" + BIMG + "/images/star.gif'/>";         // replace with image
                        else if ( flag == "partial" && isInterative )
                        {
                            strText = "&#9632;";                                                                // replace with block
                            strClass = "h";
                        }
                    }
                    else
                    {
                        // just stick with the number
                        canEdit = true;
                        var w = g_mapAutocompletion[req.id] === undefined ? "": (isTallyReq( req.id ) ? "" : " auto");
                        var strMark = "<span y>" + YES + "</span>";
                        if ( outing !== undefined )
                        {
                            if ( isRequirementAddressedByOuting( outing, req.id ) && isTallyReq( req.id ) )
                            {
                                var nCount = getOutingTallyCount( outing, youthID, req.id );
                                var sRollup = getReqRollup( req.id )
                                if ( sRollup != "" )
                                    sRollup = nCount + sRollup;
                                strMark = "<span class='tally' yy>+" + sRollup + "</span>";
                                if ( nCount == 0 )
                                {
                                    isPreset = true;
                                    p = "";
                                }
                            }
                        }
                        
                        strText = "<span" + w + ">" + strText + "</span>" + strMark;

                        var requirement = g_dbTables['Requirements'][req.id];
                        if ( requirement.requirement.match( /^([A-Z])/ ) )
                            s = " q='" + RegExp.$1 + "' ";

                        if ( flag == "na" )
                            strClass = "na";
                    }

                    strClass += " " + ( req.dependent ? "or" : "" );

                    if ( isInterative )
                    {
                        if ( youthID > 0 )
                        {
                            htmlSectionReqs += "<td " + p + (canEdit?("pre='" + (isPreset?1:0)):"") + "' "  + strClass + " title='" + req.tooltip + "' b=" + iBadge + (canEdit?(" z=0" + s):"") + " onclick='" + (allowInPlaceEdit?(canEdit?"bbb":"c"):"a") + "(this);' " + (allowInPlaceEdit&&canEdit?("r='" + iSection + "-" + req.label + "'"):"") + ">" + strText + "</td>";
                        }
                        else if ( allowInPlaceEdit )
                            htmlSectionReqs += "<td caption r title='" + req.tooltip + "' b=" + iBadge + " style='cursor:pointer;padding-top:0;' onclick='selectColumn(\"" + iSection + "-" + req.label + "\",false);'>" + strText + "</td>";
                        else
                            htmlSectionReqs += "<td caption title='" + req.tooltip + "' b=" + iBadge + " style='padding-top:0;'>" + strText + "</td>";
                    }
                    else    // no tooltips for record sheets
                        htmlSectionReqs += "<td " + strClass + ">" + strText + "</td>";
                }
                htmlReqs += htmlSectionReqs;
            }

            if ( listBadges.length > 1 )
                iBadge++;
        }
    }

    if ( htmlReqs == "" )       // handle under-construction badges
        htmlReqs += "<td caption>undefined</td>";

    return { reqs: htmlReqs, captions: htmlCaptions, numCells: nCells };
};

function isRequirementNeeded( youthID, reqID )
{
    if ( isAutoRequirement( reqID ) )
        return true;

    var badgeID = deriveBadgeID( reqID );
    var reqs = getBadgeCompletionRequirements( badgeID );
    var req = g_dbTables['Requirements'][reqID];

    var parentReqID = deriveParentID( req.id );        // strip of the subrequirement letter
    var isParentSubReqd = isSubReqd( parentReqID );

    // if the badge logic doesn't include an OR, then every requirement has an impact
    if ( ! isParentSubReqd )
    {
        if ( reqs.indexOf( "|" ) < 0 ) 
            return true;
    }
    else
    {
        // we can probably do a similar shortcut for subreqs by investigating the parent
        var flag = getRequirementStatus( youthID, parentReqID );
        return flag != "complete" && flag != "implicit";
    }

    var jsonCompletionOld = evaluateExpression( youthID, reqs, badgeID );
    if ( g_mapRequirementCompletion[youthID] === undefined )
        g_mapRequirementCompletion[youthID] = {};

    var jsonCompletionReqOld = g_mapRequirementCompletion[youthID][reqID];

    if ( jsonCompletionReqOld !== undefined && jsonCompletionReqOld.complete > 0 )
    {
        console_trace( "wowot! requirement '" + reqID + "' of youth " + youthID + " has completion of " + jsonCompletionReqOld.complete );
        return false;
    }

    // fake out the requirement being marked as complete
    var nWeight = getRequirementWeight( reqID );

    // Okay so we have a situation in which the following expression (1&2&true)|(1&3&4) returns FALSE for
    // either 4 or 5.   Because if 1 and 2 are not complete then the expression boils down to
    // (!10:30)|(!0:30) = !10:30.  If we then make one of 3 complete, we end up with (!10:30)|(!10:30) = !10:30
    // which means that 3 had no effect on the completion score, and is therefore not needed.
    // 
    // So what we want to do is to evaluate any and block e.g., (1&2&true) or (1&3&4) and see if ANY is
    // impacted.
    
    var isNeeded = false;
    if ( reqs.match( /(.*?)\((([\w!:-]+&)*([\w!:-]+))\)(.*)/ ) )
    {
        // look for nested AND's, e.g. "1|2|(3&4&5)|6"
        while ( reqs.match( /(.*?)\((([\w!:-]+&)*([\w!:-]+))\)(.*)/ ) )
        {
            var pre = RegExp.$1;
            var regex = RegExp.$2;
            var post = RegExp.$5;

            var regexExists = new RegExp( "(&|^)" + req.requirement + "(&|$)" );
            if ( ! regexExists.test( regex ) )
                reqs = pre + "!na" + post;
            else
            {
                g_mapRequirementCompletion[youthID][reqID] = jsonCompletionReqOld;
                var jsonCompletionOld = evaluate_AND( youthID, regex, badgeID );

                g_mapRequirementCompletion[youthID][reqID] = { complete: nWeight, total: nWeight };
                var jsonCompletionNew = evaluate_AND( youthID, regex, badgeID );

                isNeeded = jsonCompletionOld.complete == 0 || jsonCompletionOld.complete != jsonCompletionNew.complete;
                if ( isNeeded )
                {
                    if ( req.requirement == "E1" )
                    console_trace( "here" );
                    break;
                }
            }
        }
    }
    else
    {
        g_mapRequirementCompletion[youthID][reqID] = { complete: nWeight, total: nWeight };
        var jsonCompletionNew = evaluateExpression( youthID, reqs, badgeID );
        isNeeded = jsonCompletionOld.complete == 0 || jsonCompletionOld.complete != jsonCompletionNew.complete;
    }


    // restore the old completion record, if any
    if ( jsonCompletionReqOld === undefined )
        delete g_mapRequirementCompletion[youthID][reqID];
    else
        g_mapRequirementCompletion[youthID][reqID] = jsonCompletionReqOld;

    return isNeeded;
};

function getSortedBadgeRequirements( listBadges )
{
    var listRequirementIDs = new Array();
    var listRequirementClasses = new Array();

    for ( var iBadge = 0; iBadge < listBadges.length; iBadge++ )
    {
        var listBadgeRequirementLabels = new Array();
        var listBadgeRequirementIDs = new Array();

        badgeID = listBadges[iBadge];

        var metadata = g_dbTables['MetaData'][badgeID];
        if ( metadata === undefined )
            console_warn( "wowot! no metadata for '" + badgeID + "'" );

        // sort the requirements

        var resultsRequirements = new Array();
        for ( var reqID in g_dbTables['Requirements'] )
        {
            var req = g_dbTables['Requirements'][reqID];
            if ( req.badgeid != badgeID )
                continue;
            // skip zero-weight digit reqs (e.g., "A1" or "5") as these are not completable
            if ( getRequirementWeight( req.id ) == 0 &&  req.requirement.match( /[A-Z]?([0-9]+)$/ ) )
                continue;
            resultsRequirements.push( req );
        }

        for ( var iRow = 0; iRow < resultsRequirements.length; iRow++ )
        {
            var req = resultsRequirements[iRow];
            var strRequirementLabel = req.requirement; 
            var reqID = g_mapBadgeRequirementIDs[badgeID][strRequirementLabel];

            // first check for conjunctions
            var regex1 = new RegExp( "\\|\\(?" + strRequirementLabel + "[&\\|\\)]" );
            var addConjunction = false;
            if ( regex1.test( metadata.displaylogic ) )
                addConjunction = true;
            else
            {
                var regex2 = new RegExp( "\\|" + strRequirementLabel + "$" );
                if ( regex2.test( metadata.displaylogic ) )
                    addConjunction = true;
            }

            // make sure the requirement is preceeded by a "0" if it is less than 10, to ensure that we don't
            // get "1", "11", "12", "2", "3" etc
            if ( strRequirementLabel.match( /^([A-Z]?)(\d[a-z]?)$/ ) )    // one digit?
                strRequirementLabel = RegExp.$1 + "0" + RegExp.$2;

            listBadgeRequirementLabels.push( strRequirementLabel );
            listBadgeRequirementIDs[strRequirementLabel] = req.id;

            listRequirementClasses[req.id] = addConjunction ? "or" : "";
        }

        listBadgeRequirementLabels.sort();

        // now extract the ID's for sorted labels
        for ( var iReq = 0; iReq < listBadgeRequirementLabels.length; iReq++ )
        {
            var reqID = listBadgeRequirementIDs[listBadgeRequirementLabels[iReq]];
            listRequirementIDs.push( reqID );
        }
    }

    return {
        ids: listRequirementIDs,
        classes: listRequirementClasses
    }
};

function formatRollup( nCount )
{
    if ( nCount === undefined || nCount == 0 )
        return "&ndash;";

    return nCount;
};

function processAttendanceTableReportResults( labelID, nYear, strPageTitle )
{
    var strTitle = "";
    if ( labelID == -1 )
        strTitle = "All Events";
    else if ( labelID == -2 )
        strTitle = "Outings";
    else
        strTitle = htmlEncode( g_dbTables['Labels'][labelID].name );

    var allowInPlaceEdit = getInPlaceEdit();

    $('#table-report-attendance-year a').html( strTitle );

    strTitle += ": " + nYear + "-" + (nYear+1) 
    $('#table-report-header').html( strTitle );
    $('#table-report-attendance-year-header').html( strTitle );

    var mapOutings = {};
    for ( var key in g_mapOutings )
    {
        var outing = g_mapOutings[key];
        if ( outing === undefined || outing == null )
            continue;       // could be gaps in what looks like an array

        if ( ! isMeetingVisible( outing ) ) 
            continue;

        if ( isReminder( outing ) )
            continue;

        if ( filterOutingByLabel( outing, labelID ) == null )
            continue;

        if ( filterOutingByYear( outing, nYear ) == null )
            continue;

        foundSomething = true;
        mapOutings["" + outing.id] = outing;
    }

    var tableReport = $('#table-report-body > table');
    tableReport.empty();

    if ( foundSomething )
    {
        var listDividers = null;
        var mapDividers = {};

        var listPatrols = [ 0 ];
        for ( var patrolID in g_dbTables['Patrols'] )
            listPatrols.push( patrolID );

        for ( var iPatrol = 0; iPatrol < listPatrols.length; iPatrol++ )
        {
            var patrolID = listPatrols[iPatrol];
            mapDividers[patrolID] = {};
            for ( var youthID in g_mapYouthNames )
            {
                if ( g_setActiveYouth[youthID] === undefined )
                    continue;

                if ( g_dbTables['Youth'][youthID].patrolid == patrolID )
                    mapDividers[patrolID][youthID] = 1;
            }
        }

        listDividers = sortPatrols();

        // add the leaders as a pseudo patrol
        var patrolID = -1;
        mapDividers[patrolID] = {};
        for ( var leaderID in g_mapLeaderNames )
        {
            if ( g_setActiveLeaders[leaderID] === undefined )
                continue;

            mapDividers[patrolID][leaderID] = 1;
        }
        listDividers.push( -1 );

        var foundSomething = false;
        var listOutingIDs = sortOutingsByDate( mapOutings );
        listOutingIDs.reverse();
        var numCells = listOutingIDs.length;

        var htmlCaptions = "";
        var mapYouthAttendance = {};
        var mapLeaderAttendance = {};
        for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
        {
            var outingID = listOutingIDs[iOuting];
            var outing = getOuting( outingID );
            
            var jsOnClick = "doViewEvent(" + outingID + ");";
            if ( allowInPlaceEdit )
                jsOnClick = "selectAttendanceColumn(" + outingID + ");";

            htmlCaptions += "<td caption onclick='" + jsOnClick + "' style='cursor:pointer;padding:5px;vertical-align:bottom;' title='" + getOutingName( outing ).replace( /'/g, "&#39;" ) + "'>" + buildAttendanceReportHeader( getOutingYouthLabels( outing, -1 ) ) + "<br/>" + formatDate( outing.date, "mmm d" ) + "</td>";

            mapYouthAttendance[outingID] = {};
            for ( var iYouth = 0; iYouth < outing.youth.length; iYouth++ )
                mapYouthAttendance[outingID][outing.youth[iYouth].id] = 1;
            mapLeaderAttendance[outingID] = {};
            if ( outing.leaders !== undefined )
                for ( var iLeader = 0; iLeader < outing.leaders.length; iLeader++ )
                    mapLeaderAttendance[outingID][outing.leaders[iLeader].id] = 1;
        }
        tableReport.append( "<tr><td/><td/>" + htmlCaptions + "</tr>" );

        for ( var iDivider = 0; iDivider < listDividers.length; iDivider++ )
        {
            var dividerID = listDividers[iDivider];

            if ( mapDividers[dividerID] === undefined )
                continue;
            if ( size( mapDividers[dividerID] ) == 0 )
                continue;       // skip this empty group

            var isLeader = dividerID == -1;
            tableReport.append( "<tr><td colspan=" + ( numCells + 2 ) + " class='divider'>" + (isLeader?STR_LEADERS:getPatrolName( dividerID, true )) + "</td></tr>" );
            var listYouth = sortMembersByName( (isLeader?"Leaders":"Youth"), mapDividers[dividerID] );

            for ( var iYouth = 0; iYouth < listYouth.length; iYouth++ )
            {
                var youthID = listYouth[iYouth];
                var youth = g_dbTables[isLeader?'Leaders':'Youth'][youthID];
                if ( youth.active == 0 )
                    continue;       // we only care about active scouts

                var jsOnClick = "";
                if ( allowInPlaceEdit )
                    jsOnClick = "onclick='bbb(this);' ";

                var htmlRollups = "";
                for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
                {
                    var outingID = listOutingIDs[iOuting];
                    var outing = getOuting( outingID );
                    var bAttended = false;
                    var strYes = YES;
                    var strTitle = "";

                    if ( isLeader )
                        bAttended = mapLeaderAttendance[outingID][youthID] !== undefined;
                    else
                    {
                        bAttended = mapYouthAttendance[outingID][youthID] !== undefined;
                        var labels = getOutingYouthLabels( outing, youthID );
                        if ( isOverridden( labels, outing.labels ) )
                        {
                            strYes += "<sup style='color:800;'>*</sup>";
                            strTitle = "title=\"This " + STR_YOUTH_POSSESSIVE + " participation in the event differs from the other " + STR_YOUTHS + "\"";
                        }
                    }

                    if ( allowInPlaceEdit )
                    {
                        var strMark = isUpcoming(getOuting(outingID))?"<img src='./images/planned.gif'>":strYes;

                        if ( bAttended )
                            strMark = "<span y>" + strMark + "</span><span>&ndash;</span>";
                        else
                            strMark = "<span>&ndash;</span><span y>" + strMark + "</span>";

                        htmlRollups += "<td " + strTitle + " z=0 " + (bAttended?"pre=1 ":"pre=0 ") + jsOnClick + " outingid=" + outingID + ">" + strMark + "</td>";
                    }
                    else
                        htmlRollups += "<td " + (isLeader?"l":"") + strTitle + ">" + (bAttended?(isUpcoming(getOuting(outingID))?"<img src='./images/planned.gif'>":strYes):"&ndash;") + "</td>";
                }

                var strYouthName = isLeader? g_mapLeaderNames[youthID] : getDisplayName( youth, youthID );

                var htmlAllButton = allowInPlaceEdit ? "<div class='button inline' style='padding: 4px 10px; margin: 2px 10px -1px 5px;' onclick='selectAllInRow(" + isLeader + "," + youthID + ");'>All</div>" : "";
                tableReport.append( "<tr " + (allowInPlaceEdit?"z ":"") + (isLeader?"leaderid":"youthid") + "=" + youth.id + "><td>" + htmlEncode( strYouthName ) + getRoleTag( isLeader?null:ROLES, youth.role ) + "</td><td>" + htmlAllButton + "</td>" + htmlRollups + "</tr>" );
                foundSomething = true;
            }
        }
    }
    else
        tableReport.append( "<tr><td class='noresults' style='font-weight: normal;padding:10px 50px; font-size:90%;'>No results</td></tr>" );

    $( '#table-report div.hint > div.no-print' ).html( "Hint: hover your mouse over any column header to see the event name." );

    $( '#table-report div[data-role=header] a' ).remove();
    if ( ! allowInPlaceEdit )
    {
        $( '#table-report div[data-role=header]' ).append( "<a data-icon='arrow-l' href='javascript:void(0)' onclick='restorePage();'>Reports</a>" );
        if ( getLoginID() && getRole() == "v" )
            $( '#table-report div[data-role=header]' ).append( "<a data-icon='gear' href='javascript:void(0)' onclick='editTableAttendanceReport();'>&nbsp;&nbsp;In Bulk&nbsp;&nbsp;</a>" );
        $( '#table-report div.hint > div.no-print' ).html( "Hint: hover your mouse over any box to see the requirement text." );
    }
    else
    {
        var back = "setInPlaceEdit(false,false);doTableReportRefresh();";
        $( '#table-report div[data-role=header]' ).append( "<a data-icon='delete' href='javascript:void(0)' onclick='" + back + "'>&nbsp;Cancel&nbsp;</a>" );
        $( '#table-report div[data-role=header]' ).append( "<a data-icon='check' href='javascript:void(0)' onclick='doSaveBulkAttendanceEdit();" + back + "'>&nbsp;&nbsp;Save&nbsp;&nbsp;</a>" );
        $( '#table-report div.hint > div.no-print' ).html( isMobileDevice() ? "Tap any cell to update it, or any column label to update multiple cells." : "Click on any cell to update it, or any column label to update multiple cells." );
    }

    $("#table-report-attendance-year").toggle( ! allowInPlaceEdit );
    $("#table-report-attendance-year").parent().next().css( "margin-top", allowInPlaceEdit ? "15px" : "0" );
    $("#table-report .name").text( strPageTitle );
    $('#table-report-header').toggle( foundSomething && allowInPlaceEdit );
    $('#table-report-wait').hide();
    $('#table-report-body').show();
    $('#table-report .hint').toggle( foundSomething && allowInPlaceEdit );
    $('#table-report-attendance-year-header').show();
};

function processEventTableReportResults( labelID, strLabel )
{
    // we might need to explode some pseudo-badges
    var listLabels = new Array();
    var strTitle = "";
    var strTarget = "";

    if ( labelID.match( /^combo:/ ) )
    {
        if ( labelID == "combo:all" ) {
            for ( var iLabel = 0; iLabel < g_listSortedLabelIDs.length; iLabel++ )
                if ( g_listSortedLabelIDs[iLabel] != g_reminderID )
                    listLabels.push( g_listSortedLabelIDs[iLabel] );
            strTitle = "All Events";
        }
        else 
        {
            for ( var iLabel = 0; iLabel < g_listSortedLabelIDs.length; iLabel++ )
                if ( g_listSortedLabelIDs[iLabel] != g_meetingID && g_listSortedLabelIDs[iLabel] != g_reminderID )
                    listLabels.push( g_listSortedLabelIDs[iLabel] );
            strTitle = "Outings";
        }
    }
    else
    {
        listLabels.push( labelID );
        strTitle = g_dbTables['Labels'][labelID].name;
    }

    $('#table-report-header').html( strTitle );
    var tableReport = $('#table-report-body > table');

    tableReport.empty();

    var listDividers = null;
    var mapDividers = {};

    var listAttributes = new Array();
    var numCells = 0;
    for ( var iLabel = 0; iLabel < listLabels.length; iLabel++ )
    {
        var json = {
            id: listLabels[iLabel],
            key: g_dbTables['Labels'][listLabels[iLabel]].key,
            attributes: new Array()
        };

        for ( var attributeID in g_dbTables['Attributes'] )
        {
            var attribute = g_dbTables['Attributes'][attributeID];
            if ( attribute.labelid == listLabels[iLabel] )
                json.attributes.push( attribute.id );
        }
        listAttributes.push( json );
        numCells += json.attributes.length + 1;
    }

    numCells += listAttributes.length-1;

    var hasAttributes = false;
    for ( var iLabel = 0; iLabel < listAttributes.length; iLabel++ )
    {
        if ( listAttributes[iLabel].attributes.length > 0 )
        {
            hasAttributes = true;
            break;
        }
    }

    var htmlCaptions = "";
    $.each( listAttributes, function( i, json ) {
        if ( htmlCaptions != "" )
            htmlCaptions += "<td caption spacer2></td>";

        for ( var iAttribute = 0; iAttribute < json.attributes.length; iAttribute++ )
        {
            var attribute = g_dbTables['Attributes'][json.attributes[iAttribute]];
            htmlCaptions += "<td caption><img title='" + attribute.name + "' src='" + attribute.image + "'/></td>";
        }

        var label = g_dbTables['Labels'][json.id];
        htmlCaptions += "<td caption" + (hasAttributes?" s":"") + "><img title='" + label.name + "' src='" + label.image + "'/></td>";
    } );

    tableReport.append( "<tr><td/><td/>" + htmlCaptions + "</tr>" );

    var listPatrols = [ 0 ];
    for ( var patrolID in g_dbTables['Patrols'] )
        listPatrols.push( patrolID );

    for ( var iPatrol = 0; iPatrol < listPatrols.length; iPatrol++ )
    {
        var patrolID = listPatrols[iPatrol];
        mapDividers[patrolID] = {};
        for ( var youthID in g_mapYouthNames )
        {
            if ( g_setActiveYouth[youthID] === undefined )
                continue;

            if ( g_dbTables['Youth'][youthID].patrolid == patrolID )
                mapDividers[patrolID][youthID] = 1;
        }
    }

    listDividers = sortPatrols();

    // add the leaders as a pseudo patrol
    var patrolID = -1;
    mapDividers[patrolID] = {};
    for ( var leaderID in g_mapLeaderNames )
    {
        if ( g_setActiveLeaders[leaderID] === undefined )
            continue;

        mapDividers[patrolID][leaderID] = 1;
    }
    listDividers.push( -1 );

    var showIcons = false;

    for ( var iDivider = 0; iDivider < listDividers.length; iDivider++ )
    {
        var dividerID = listDividers[iDivider];

        if ( mapDividers[dividerID] === undefined )
            continue;
        if ( size( mapDividers[dividerID] ) == 0 )
            continue;       // skip this empty group

        if ( listDividers.length > 1 )
            tableReport.append( "<tr><td colspan=" + ( numCells + 2 ) + " class='divider'>" + (dividerID==-1?STR_LEADERS:getPatrolName( dividerID, true )) + "</td></tr>" );

        var listYouth = sortMembersByName( (dividerID==-1?"Leaders":"Youth"), mapDividers[dividerID] );

        for ( var iYouth = 0; iYouth < listYouth.length; iYouth++ )
        {
            var youthID = listYouth[iYouth];
            var youth = g_dbTables[dividerID==-1?'Leaders':'Youth'][youthID];
            if ( youth.active == 0 )
                continue;       // we only care about active scouts

            // do the rollup
            var mapRollups = getEventRollups( youthID, true, dividerID == -1 );

            var htmlRollups = "";
            $.each( listAttributes, function( i, json ) {
                if ( htmlRollups != "" )
                    htmlRollups += "<td spacer2></td>";

                for ( var iAttribute = 0; iAttribute < json.attributes.length; iAttribute++ )
                    htmlRollups += "<td>" + formatRollup( mapRollups[json.key + "." + g_dbTables['Attributes'][json.attributes[iAttribute]].key] ) + "</td>";

                htmlRollups += "<td" + (hasAttributes?" s":"") + ">" + formatRollup( mapRollups[json.key] ) + "</td>";
            });

            var strYouthName = getDisplayName( youth, youthID );

            tableReport.append( "<tr youthid='" + youthID + "' onclick='switchYouth(" + youthID +");buildEventReport(-1," + youthID + ");'><td>" + htmlEncode( strYouthName ) + getRoleTag( ROLES, youth.role ) + "</td><td style='width:10px;'/>" + htmlRollups + "</tr>" );
        }
    }

    $('#table-report div.page-content .glyph').toggle( showIcons );
    $("#table-report .name").text( strLabel );
    $('#table-report-header').show();
    $('#table-report-wait').hide();
    $('#table-report-body').show();
};

function processBadgeTableReportResults( badgeID, strLabel )
{
    // we might need to explode some pseudo-badges
    var listBadges = new Array();
    var strTitle = "";
    var strTarget = "";
    g_warnNoParticipants = true;

    if ( badgeID.match( /^activity:/ ) )
    {
        if ( badgeID == "activity:voyageur" )
        {
            listBadges.push( "voyageurcitizenship" );
            listBadges.push( "voyageurleadership" );
            listBadges.push( "voyageurpersonaldevelopment" );
            listBadges.push( "voyageuroutdoorskills" );
            strTitle = "Voyageur Activity Badges";
            strTarget = "#voyageur-category-troop";
            badgeID = "voyageur";
        }
        else if ( badgeID == "activity:pathfinder" )
        {
            listBadges.push( "pathfindercitizenship" );
            listBadges.push( "pathfinderleadership" );
            listBadges.push( "pathfinderpersonaldevelopment" );
            listBadges.push( "pathfinderoutdoorskills" );
            strTitle = "Pathfinder Activity Badges";
            strTarget = "#pathfinder-category-troop";
            badgeID = "pathfinder";
        }
        else if ( badgeID == "activity:pioneer" )
        {
            listBadges.push( "pioneercitizenship" );
            listBadges.push( "pioneercampcraft" );
            listBadges.push( "pioneerairactivities" );
            listBadges.push( "pioneerconstruction" );
            listBadges.push( "pioneerenvironment" );
            listBadges.push( "pioneerwatersafety" );
            listBadges.push( "pioneeremergencies" );
            listBadges.push( "pioneerwateractivities" );
            strTitle = "Pioneer Target Badges";
            strTarget = "#pioneer-category-troop_au";
            badgeID = "pioneer";
        }
        else if ( badgeID == "activity:explorer" )
        {
            listBadges.push( "explorercitizenship" );
            listBadges.push( "explorercampcraft" );
            listBadges.push( "explorerairactivities" );
            listBadges.push( "explorerconstruction" );
            listBadges.push( "explorerenvironment" );
            listBadges.push( "explorerwatersafety" );
            listBadges.push( "exploreremergencies" );
            listBadges.push( "explorerwateractivities" );
            strTitle = "Explorer Target Badges";
            strTarget = "#explorer-category-troop_au";
            badgeID = "explorer";
        }
        else if ( badgeID == "activity:adventurer" )
        {
            listBadges.push( "adventurercitizenship" );
            listBadges.push( "adventurercampcraft" );
            listBadges.push( "adventurerairactivities" );
            listBadges.push( "adventurerconstruction" );
            listBadges.push( "adventurerenvironment" );
            listBadges.push( "adventurerwatersafety" );
            listBadges.push( "adventureremergencies" );
            listBadges.push( "adventurerwateractivities" );
            strTitle = "Adventurer Target Badges";
            strTarget = "#adventurer-category-troop_au";
            badgeID = "adventurer";
        }
        else if ( badgeID == "activity:youinguiding" )
        {
            listBadges.push( "yig-challenge-1" );
            listBadges.push( "yig-challenge-2" );
            listBadges.push( "yig-challenge-3" );
            listBadges.push( "yig-challenge-4" );
            strTitle = "You in Guiding Challenges";
            strTarget = "#youinguiding-category-unit";
            badgeID = "yig";
        }
        else if ( badgeID == "activity:youandothers" )
        {
            listBadges.push( "yao-challenge-1" );
            listBadges.push( "yao-challenge-2" );
            listBadges.push( "yao-challenge-3" );
            listBadges.push( "yao-challenge-4" );
            strTitle = "You and Other Challenges";
            strTarget = "#youandothers-category-unit";
            badgeID = "yao";
        }
        else if ( badgeID == "activity:discoveringyou" )
        {
            listBadges.push( "dy-challenge-1" );
            listBadges.push( "dy-challenge-2" );
            listBadges.push( "dy-challenge-3" );
            listBadges.push( "dy-challenge-4" );
            strTitle = "Discovering You Challenges";
            strTarget = "#discoveringyou-category-unit";
            badgeID = "dy";
        }
        else if ( badgeID == "activity:beyondyou" )
        {
            listBadges.push( "by-challenge-1" );
            listBadges.push( "by-challenge-2" );
            listBadges.push( "by-challenge-3" );
            listBadges.push( "by-challenge-4" );
            strTitle = "Beyond You Challenges";
            strTarget = "#beyondyou-category-unit";
            badgeID = "by";
        }
        else if ( badgeID == "activity:venturer" )
        {
            listBadges.push( "exploration" );
            listBadges.push( "personalfitness" );
            listBadges.push( "personalinterest" );
            listBadges.push( "service" );
            listBadges.push( "socculspir" );
            listBadges.push( "vocational" );
            strTitle = "Venturer Level Awards";
            strTarget = "#venturer-category-company";
            badgeID = "venturer";
        }
        else if ( badgeID == "activity:queensventurer" )
        {
            if ( $.inArray( 0, g_listUniformIDs ) )
                listBadges.push( "worldconservation" );
            listBadges.push( "worldscoutenvironment" );
            listBadges.push( "religioninlife" );
            listBadges.push( "spirituality" );
            strTitle = "Queen's Venturer Level Awards";
            strTarget = "#queensventurer-category-company";
            badgeID = "queensventurer";
        }
    }
    else
    {
        listBadges.push( badgeID );
        strTitle = g_dbTables['MetaData'][badgeID].name;
    }

    var tableReport = $('#table-report-body > table');
    tableReport.empty();

    var listDividers = null;
    var mapDividers = {};

    var json = getSortedBadgeRequirements( listBadges );

    var allowInPlaceEdit = getInPlaceEdit();
    var jsonSections = segmentRequirements2( json.ids, json.classes, 0, listBadges );
    var jsonHTML = renderSegments_HTML( jsonSections, 0, listBadges, true, allowInPlaceEdit );

    if ( jsonHTML.captions != "" )
    {
        if ( jsonHTML.numCells > 1 )
        {
            tableReport.append( "<tr class=thin><td colspan=2></td>" + jsonHTML.captions + "</tr>" );
            tableReport.append( "<tr><td colspan=2></td>" + jsonHTML.reqs + "</tr>" );
        }
        else
            tableReport.append( "<tr><td colspan=2></td>" + jsonHTML.captions + "</tr>" );
    }
    else if ( jsonHTML.numCells == 1 )
        tableReport.append( "<tr><td colspan=2></td><td caption style='padding-right:5px;'>Requirement</td></tr>" );
    else
    {
        tableReport.append( "<tr class=thin><td colspan=2></td><td colspan=" + jsonHTML.numCells + " caption>Requirements</td></tr>" );
        tableReport.append( "<tr><td colspan=2></td>" + jsonHTML.reqs + "</tr>" );
    }

    var listPatrols = [ 0 ];
    for ( var patrolID in g_dbTables['Patrols'] )
        listPatrols.push( patrolID );

    var setParticipants = {};
    var nParticipants = 0;

    var bulkOutingID = getCurrentRelatedEvent();

    if ( allowInPlaceEdit )
    {
        var now = getServerTimestamp();

        $('#table-report-groupby').show();
        $('#table-report-groupby li + li').toggle( bulkOutingID != -1 );

        if ( bulkOutingID != -1 )
        {
            var outing = getOuting( bulkOutingID );
            now = outing.date;

            toggleSwitch( "#table-report-groupby li", g_tableReportGrouping );
            if ( g_tableReportGrouping )
            {
                $.each( outing.youth, function( i, outingYouth ) {
                    nParticipants++;
                    setParticipants[outingYouth.id] = 1;
                });
            }

            htmlIcons = renderSegmentsUnresolved_HTML( jsonSections, outing );
            if ( htmlIcons )
            {
                var strAllButton = "<div class='button inline' style='margin: 2px 10px -1px 5px;padding: 4px 10px;' onclick='selectAllUnresolved();'>All</div>";
                tableReport.append( "<tr><td/><td>" + strAllButton + "</td>" + htmlIcons + "</tr>" );
            }
        }

        var strDate = formatDate( now );
        $('#table-report-date-input').val( strDate );
        g_jdPickers['table-report-date-input'].selectDate();
        $('#table-report-date-input').parent().parent().children( "div.jdpicker-val" ).html( strDate );

        $('#table-report-date').parent().show();
        $('#table-report-date-header').show();
    }
    else 
    {
        $( '#table-report-date' ).parent().hide();
        $( '#table-report-date-header' ).hide();
    }

    for ( var iPatrol = 0; iPatrol < listPatrols.length; iPatrol++ )
    {
        var patrolID = listPatrols[iPatrol];
        mapDividers[patrolID] = {};
        for ( var youthID in g_mapYouthNames )
        {
            if ( g_setActiveYouth[youthID] === undefined )
                continue;

            if ( nParticipants > 0 && setParticipants[youthID] === undefined )
                continue;

            if ( g_dbTables['Youth'][youthID].patrolid == patrolID )
                mapDividers[patrolID][youthID] = 1;
        }
    }

    $('#table-report-header').html( strTitle );
    listDividers = sortPatrols();
    var showIcons = false;

    for ( var iDivider = 0; iDivider < listDividers.length; iDivider++ )
    {
        var dividerID = listDividers[iDivider];

        if ( mapDividers[dividerID] === undefined )
            continue;
        if ( size( mapDividers[dividerID] ) == 0 )
            continue;       // skip this empty group

        if ( listDividers.length > 1 )
            tableReport.append( "<tr><td colspan=" + ( jsonHTML.numCells + 2 ) + " class='divider'>" + getPatrolName( dividerID, true ) + "</td></tr>" );

        var listYouth = sortMembersByName( "Youth", mapDividers[dividerID] );

        for ( var iYouth = 0; iYouth < listYouth.length; iYouth++ )
        {
            var youthID = listYouth[iYouth];
            var youth = g_dbTables['Youth'][youthID];
            if ( youth.active == 0 )
            {
                console_log( "not active!" );
                continue;       // we only care about active scouts
            }

            var strGlyph = "";
            var flag = getBadgeStatus( youthID, badgeID );
            if ( flag == "awarded" )
            {
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/check.gif'/>";
                showIcons = true;
            }
            else if ( flag == "complete" )
            {
                strGlyph = "<img class='glyph' style='vertical-align:-2px;margin-right:8px;' src='" + BIMG + "/images/unawarded.gif'/>";
                showIcons = true;
            }
            else
                strGlyph = "<img class='glyph' style='margin-right:24px;' src='" + BIMG + "/images/blank.gif'/>";

            var strYouthName = getDisplayName( youth, youthID );
            var nPercent = getBadgePercentComplete( youthID, badgeID );

            var strPercentageComplete = "";
            if ( nPercent > 0 && nPercent < 100 )
            {
                strPercentageComplete = (nPercent <= 9 ? "&nbsp;&nbsp;" : "" ) + nPercent + "%";
                strPercentageComplete = "<small class='counter' style='margin: 0 5px 0 0;padding:4px 5px 4px 8px;'>" + strPercentageComplete + "</small>";
            }
            // hijack the percentage complete when we're doing inplace editing
            if ( allowInPlaceEdit ) 
            {
                if ( flag != "complete" && flag != "awarded" )
                    strPercentageComplete = "<div class='button inline' style='padding: 4px 10px; margin: 2px 10px -1px 5px;' onclick='selectAllInRow(false," + youthID + ");'>All</div>";
            }

            jsonHTML = renderSegments_HTML( segmentRequirements2( json.ids, json.classes, youthID, listBadges ), youthID, listBadges, true, allowInPlaceEdit );

            var strExtra = allowInPlaceEdit ? "" : ( listBadges.length == 1 ? " onclick='switchYouth(" + youthID +");setCurrentBadge(\"" + badgeID + "\",true,\"dissolve\");' " : " onclick='switchYouth(" + youthID +");addPage2(\"" + strTarget + "\",\"Report\");' " );

            tableReport.append( "<tr " + (allowInPlaceEdit?"z ":"") + "youthid='" + youthID + "' badges='" + listBadges.join(',') + "'><td class='" + flag + "'" + strExtra + ">" + strGlyph + htmlEncode( strYouthName ) + getRoleTag( ROLES, youth.role ) + "</td><td>" + strPercentageComplete + "</td>" + jsonHTML.reqs + "</tr>" );
        }
    }

    $( '#table-report div[data-role=header] a' ).remove();
    if ( ! allowInPlaceEdit )
    {
        $( '#table-report div[data-role=header]' ).append( "<a data-icon='arrow-l' href='javascript:void(0)' onclick='restorePage();'>Reports</a>" );
        if ( getLoginID() && getRole() == "v" )
            $( '#table-report div[data-role=header]' ).append( "<a data-icon='gear' href='javascript:void(0)' onclick='editTableReport();'>&nbsp;&nbsp;In Bulk&nbsp;&nbsp;</a>" );
        $( '#table-report div.hint > div.no-print' ).html( "Hint: hover your mouse over any box to see the requirement text." );
    }
    else
    {
        var back = lastUpPoint().match( /badge-details/ ) ? "setInPlaceEdit(false,false);restoreUpPoint();setCurrentBadge( g_currentBadgeID, false, null );" : "setInPlaceEdit(false,false);doTableReportRefresh();";
        $( '#table-report div[data-role=header]' ).append( "<a data-icon='delete' href='javascript:void(0)' onclick='" + back + "'>&nbsp;Cancel&nbsp;</a>" );
        $( '#table-report div[data-role=header]' ).append( "<a data-icon='check' href='javascript:void(0)' onclick='doSaveBulkEdit();" + back + "'>&nbsp;&nbsp;Save&nbsp;&nbsp;</a>" );
        $( '#table-report div.hint > div.no-print' ).html( isMobileDevice() ? "Tap any numbered requirement to update it, or any column label to update multiple." : "Click on any numbered requirement to update it, or any column label to update multiple." );
    }

    $('#table-report div.page-content .glyph').toggle( showIcons );
    $("#table-report .name").html( tagAbbreviations( strLabel ) );
    $('#table-report-header').show();
    $('#table-report-wait').hide();
    $('#table-report-body').show();
    $('#table-report .hint').show();
};

function selectAttendanceCell( el )
{
    var cell = $(el);
    var outingID = cell.attr( "outingid" );
    var strOutingName = getOutingName( getOuting( outingID ) );
    var tr = cell.closest( "tr" );
    var leaderID = tr.attr( "leaderid" );
    if ( leaderID == null )
    {
        var youthID = tr.attr( "youthID" );
        alert( "youth = '" + g_mapYouthNames[youthID] + "', outing = '" + strOutingName + "'" );
    }
    else
        alert( "leader = '" + g_mapLeaderNames[leaderID] + "', outing = '" + strOutingName + "'" );
};

function doSaveBulkAttendanceEdit()
{
    var mapOutings = {};
    var mapYouth = {};
    
    // get a list of all the values that changed
    $( '#table-report td[z=1]' ).each( function() {
        var tr = $(this).closest( "tr" );
        var leaderID = tr.attr( "leaderid" );
        var youthID = null;
        if ( leaderID == null )
        {
            youthID = tr.attr( "youthid" );
            mapYouth[youthID] = 1;
        }

        var outingID = $(this).attr( "outingid" );
        var bMarkAsComplete = $(this).attr('pre')==0;

        if( mapOutings[outingID] === undefined )
        {
            mapOutings[outingID] = {
                youth_add: new Array(),
                youth_remove: new Array(),
                leader_add: new Array(),
                leader_remove: new Array()
            };
        }

        if ( leaderID == null )
        {
            if ( bMarkAsComplete )
                mapOutings[outingID]['youth_add'].push( youthID );
            else
                mapOutings[outingID]['youth_remove'].push( youthID );
        }
        else
        {
            if ( bMarkAsComplete )
                mapOutings[outingID]['leader_add'].push( leaderID );
            else
                mapOutings[outingID]['leader_remove'].push( leaderID );
        }
    });

    var wasReopened = false;

    for ( var outingID in mapOutings )
    {
        var outing = getOuting( outingID );

        // step 1, remove any unwanted leaders and/or youth
        $.each( mapOutings[outingID]['leader_remove'], function( iLeader, leaderID ) {
            for ( var i = outing.leaders.length - 1; i >= 0; i-- ) 
                if ( outing.leaders[i].id == leaderID )
                    outing.leaders.splice( i, 1 );
        });
        $.each( mapOutings[outingID]['youth_remove'], function( iYouth, youthID ) {
            for ( var i = outing.youth.length - 1; i >= 0; i-- ) 
                if ( outing.youth[i].id == leaderID )
                    outing.youth.splice( i, 1 );
        });
        // step 2, add any new leaders and/or youth
        $.each( mapOutings[outingID]['leader_add'], function( iLeader, leaderID ) {
            outing.leaders.push( { id: leaderID } );
        });
        $.each( mapOutings[outingID]['youth_add'], function( iYouth, youthID ) {
            outing.youth.push( { id: youthID } );
        });
    
        // if we changed this outing's youth, and there were some related reqs, and the outing had been previously closed, then re-open it
        if ( ( mapOutings[outingID]['youth_add'].length > 0 || mapOutings[outingID]['youth_remove'] ) && outing.links.length > 0 && ! isUpcoming( outing ) )
        {
            if ( outing.closed )
               wasReopened = true;
            outing.closed = false;
        }

        // update the local db
        var mozOutingCombos = getJsonTableMap( 'db-outing-combos' );
        console_log( "old outing = '" + JSON.stringify( mozOutingCombos["o" + outing.id] ) + "'" );
        console_log( "new outing = '" + JSON.stringify( outing ) + "'" );
        mozOutingCombos["o" + outing.id] = outing;       // replace or insert this value
        setLocalStorage( 'db-outing-combos', JSON.stringify( mozOutingCombos ) );

        finishSaveEvent( outing, false );
    }

    for ( var youthID in mapYouth )
        unscorecard( youthID );     // clear cache

    setInPlaceEdit( false, true );          // set this flag

    if ( mapYouth[g_currentUser] != undefined )
        scorecard( g_currentUser );       // restore cache for current user

    doTableReportRefresh();

    if ( wasReopened )
        openLightBox( { text: "Some events will need to be re-finalized, because the participants were changed.", canClose: true} );
};

function doSaveBulkEdit()
{
    var bMarkAsComplete = true;

    var mapReqs = {};
    var mapActions = {};
    var setTallies = {};
    // get a list of all the values that changed
    $( '#table-report td[z=1]' ).each( function() {
        var youthID = $(this).closest("tr").attr( 'youthid' );
        var iBadge = $(this).attr( "b" );
        var section = $(this).attr( "q" );
        if ( section === undefined )
            section = "";
        var bMarkAsComplete = $(this).attr('pre')==0;
        var listBadges = $(this).closest("tr").attr( 'badges' ).split(',');
        var req = $(this).attr( "r" ).replace( /(\d+-)(.+)/, "$2" );
        var reqID = getRequirementID( listBadges[iBadge], section + req );

        if( mapReqs[youthID] === undefined )
            mapReqs[youthID] = new Array();

        mapReqs[youthID].push( reqID );

        if( mapActions[youthID] === undefined )
            mapActions[youthID] = {};

        mapActions[youthID][reqID] = bMarkAsComplete;
        //console_log( "youth " + youthID + ": req " + req + ", reqID = " + reqID + " -> " + bMarkAsComplete ); 
    });

    var strTimestamp = getServerTimestamp();
    try {
        strTimestamp = g_jdPickers['table-report-date-input'].stringToDate( $('#table-report-date-input').val() ).getTime();
    } catch(e) {
        console_warn( "error '" + e + "' trying to get date" );
    }

    var strNotes = "";
    var outingID = getCurrentRelatedEvent();
    var outing = null;
    if ( outingID != null && outingID > 0 )
        outing = getOuting( outingID );
    if ( outing !== undefined && outing )
        strNotes = "Participated in '" + getOutingName( outing ) + "'";

    for ( var youthID in mapReqs )
    {
        var listReqs = mapReqs[youthID];
        for ( var i = 0; i < listReqs.length; i++ )
        {
            var requirementID = listReqs[i];

            if ( outing && isRequirementAddressedByOuting( outing, requirementID ) && isTallyReq( requirementID ) )
            {
                var nIncrement = getOutingTallyCount( outing, youthID, requirementID );
                if ( nIncrement > 0 )
                {
                    var nCount = parseInt(getTally( youthID, requirementID )) + parseInt(nIncrement);

                    var strNewNotes = getTallyNotes( youthID, requirementID );
                    if ( strNewNotes != "" )
                        strNewNotes = ", " + strNewNotes;
                    strNewNotes = "Set to " + nCount + " by participation in '" + getOutingName( outing ) + "'" + strNewNotes;
                    
                    var bMarkAsComplete = mapActions[youthID][requirementID];
                    if ( bMarkAsComplete )
                        addTally( youthID, requirementID, nCount, strNewNotes, true, false, getServerTimestamp(), getLoginID(), false )
                    else if ( getRequirementStatus( youthID, requirementID ) == "complete" )
                        clearRequirementComplete( youthID, requirementID, "", true, false, true );
                }
            }
            else
            {
                var jsonCompletion = g_mapRequirementCompletion['illegal_bogus_value']; // force it to be undefined, for now
                if ( g_mapRequirementCompletion[youthID] !== undefined && g_mapRequirementCompletion[youthID][requirementID] !== undefined )
                    jsonCompletion = g_mapRequirementCompletion[youthID][requirementID];

                var bMarkAsComplete = mapActions[youthID][requirementID];
                if ( bMarkAsComplete === undefined )
                    console_trace( "wowot! youthID=" + youthID + ", reqID=" + reqID );
                else if ( bMarkAsComplete )
                {
                    if ( jsonCompletion === undefined || jsonCompletion.complete < jsonCompletion.total )
                        addRequirementComplete( youthID, requirementID, strNotes, true, false, strTimestamp, getLoginID(), true );
                }
                else
                {
                    if ( g_setMarkedRequirements[youthID] !== undefined && g_setMarkedRequirements[youthID][requirementID] !== undefined )
                        clearRequirementComplete( youthID, requirementID, "", true, false, true );
                }
            }
        }

        unscorecard( youthID );     // clear cache
    }

    setInPlaceEdit( false, true );          // set this flag

    if ( mapReqs[g_currentUser] != undefined )
        scorecard( g_currentUser );       // restore cache for current user

    doTableReportRefresh();
};

// This short-named function is used in every single cell in the table
function a( el )
{
    var youthID = $(el).closest("tr").attr( 'youthid' );
    var iBadge = $(el).attr( "b" );
    var listBadges = $(el).closest("tr").attr( 'badges' ).split(',');
    switchYouth( youthID );
    window.setTimeout( function() {
        setCurrentBadge( listBadges[iBadge], true, "dissolve" );
    }, 1000 );
};

function selectAttendanceColumn( outingID )
{
    var anyClear = $("#table-report-body td[pre=0][z=0][outingid=" + outingID + "]").length > 0;
    var anyClearPre = $("#table-report-body td[pre=1][z=1][outingid=" + outingID + "]").length > 0;
    anyClear = anyClear || anyClearPre;
    // The bogus toggleClass is necessary to work around an IE8 bug, in which the redraw wasn't happening
    // until you did a mouse off of the table!
    $("#table-report-body td[pre=0][z][outingid=" + outingID + "]").attr( 'z', anyClear ? 1 : 0 ).toggleClass( 'j' );
    $("#table-report-body td[pre=1][z][outingid=" + outingID + "]").attr( 'z', anyClear ? 0 : 1 ).toggleClass( 'j' );
};

// This short-named function is used in every single header cell in the table report
function selectColumn( strLabel, onlyParticipants )
{
    if ( onlyParticipants )
    {
        var nParticipants = $("#table-report-body tr td[ptcp=1]" ).length;
        if ( nParticipants == 0 )
        {
            onlyParticipants = false;
            if ( g_warnNoParticipants )
            {
                openLightBox( { text: "Event has no participants, so using all " + STR_YOUTHS, canClose: true } );
                g_warnNoParticipants = false;
            }
        }
    }

    if ( onlyParticipants )
    {
        var anyClear = $("#table-report-body tr td[ptcp=1][pre=0][z=0][r=" + strLabel + "]").length > 0;
        var anyClearPre = $("#table-report-body tr td[ptcp=1][pre=1][z=1][r=" + strLabel + "]").length > 0;
        anyClear = anyClear || anyClearPre;
        // The bogus toggleClass is necessary to work around an IE8 bug, in which the redraw wasn't happening
        // until you did a mouse off of the table!
        $("#table-report-body tr td[ptcp=1][pre=0][z][r=" + strLabel + "]").attr( 'z', anyClear ? 1 : 0 ).toggleClass( 'j' );
        $("#table-report-body tr td[ptcp=1][pre=1][z][r=" + strLabel + "]").attr( 'z', anyClear ? 0 : 1 ).toggleClass( 'j' );
    }
    else
    {
        var anyClear = $("#table-report-body td[pre=0][z=0][r=" + strLabel + "]").length > 0;
        var anyClearPre = $("#table-report-body td[pre=1][z=1][r=" + strLabel + "]").length > 0;
        anyClear = anyClear || anyClearPre;
        // The bogus toggleClass is necessary to work around an IE8 bug, in which the redraw wasn't happening
        // until you did a mouse off of the table!
        $("#table-report-body td[pre=0][z][r=" + strLabel + "]").attr( 'z', anyClear ? 1 : 0 ).toggleClass( 'j' );
        $("#table-report-body td[pre=1][z][r=" + strLabel + "]").attr( 'z', anyClear ? 0 : 1 ).toggleClass( 'j' );
    }
};

// This short-named function is used in every single cell in the table report
function bbb( el )
{
    var z = $(el).attr( 'z' );
    // The bogus toggleClass is necessary to work around an IE8 bug, in which the redraw wasn't happening
    // until you did a mouse off of the table!
    $(el).attr( 'z', z == 1 ? 0 : 1 ).toggleClass( 'j' );
};

// This short-named function is used in every single cell in the table report
function c( el )
{
    // nop
};

function selectAllInBadgeRow( badgeID )
{
    clearActive();

    var anyOff = false;
    $("#initialize-table-body tr[badgeid=" + badgeID + "] td[z]").each( function() {
        if ( $(this).attr( "z" ) == "0" )
            anyOff = true;
    });
    $("#initialize-table-body tr[badgeid=" + badgeID + "] td[z]").attr( "z", anyOff ? 1 : 0 );
};

function selectAllInRow( isLeader, youthID )
{
    clearActive();

    var strAttr = isLeader ? "leaderid" : "youthid";

    var anyOff = false;
    $("#table-report tr[" + strAttr + "=" + youthID + "] td[z]").each( function() {
        if ( $(this).attr( "z" ) == "0" )
            anyOff = true;
    });
    $("#table-report tr[" + strAttr + "=" + youthID + "] td[z]").attr( "z", anyOff ? 1 : 0 );
};

function doReportGrouping( reportGrouping )
{
    if ( reportGrouping != g_reportGrouping )
    {
        persistProperty( "reportgrouping", reportGrouping );
        g_reportGrouping = reportGrouping;
        doReportRefresh();
    }
}

function doEventReportGrouping( eventReportGrouping )
{
    if ( eventReportGrouping != g_eventReportGrouping )
    {
        persistProperty( "eventreportgrouping", eventReportGrouping );
        g_eventReportGrouping = eventReportGrouping;
        doEventReportRefresh();
    }

    restartPollInterval();
}

function doTableReportGrouping( reportGrouping )
{
    if ( reportGrouping != g_tableReportGrouping )
    {
        g_tableReportGrouping = reportGrouping;
        doTableReportRefresh();
    }

    restartPollInterval();
}

function testHypotheticalCompletion( youthID, setBadgeIDs, setNewReqs )
{
    if ( g_mapBadgeCompletion[youthID] !== undefined )
    {
        g_mapBadgeCompletion["temp"] = {};
        for ( var id in g_mapBadgeCompletion[youthID] )
            g_mapBadgeCompletion["temp"][id] = g_mapBadgeCompletion[youthID][id];
    }

    if ( g_mapCategoryCounts[youthID] !== undefined )
    {
        g_mapCategoryCounts["temp"] = {};
        for ( var id in g_mapCategoryCounts[youthID] )
            g_mapCategoryCounts["temp"][id] = g_mapCategoryCounts[youthID][id];
    }

    if ( g_mapTypeCounts[youthID] !== undefined )
    {
        g_mapTypeCounts["temp"] = {};
        for ( var id in g_mapTypeCounts[youthID] )
            g_mapTypeCounts["temp"][id] = g_mapTypeCounts[youthID][id];
    }

    if ( g_mapRequirementCompletion[youthID] !== undefined )
    {
        g_mapRequirementCompletion["temp"] = {};
        for ( var id in g_mapRequirementCompletion[youthID] )
            g_mapRequirementCompletion["temp"][id] = g_mapRequirementCompletion[youthID][id];
    }

    if ( g_setMarkedRequirements[youthID] !== undefined )
    {
        g_setMarkedRequirements["temp"] = {};
        for ( var id in g_setMarkedRequirements[youthID] )
            g_setMarkedRequirements["temp"][id] = g_setMarkedRequirements[youthID][id];
    }

    g_setYouthScorecarded["temp"] = 1;
    g_setDirtyBadges["temp"] = {};
    g_setDirtyRequirements["temp"] = {};

    // add the new reqs
    for ( var id in setNewReqs )
        addRequirementComplete( "temp", id, "", false, false, getServerTimestamp(), -1, false );

    processDirtyFlags( "temp", false, false, false );

    for ( var badgeID in setBadgeIDs )
        if ( isBadgeComplete( "temp", badgeID ) )
            $("span.final[youthid=" + youthID + "][badgeid=" + badgeID + "]").html( "(final requirements)" );

    //console_log( g_mapYouthNames[youthID] + ": (post) complete = " + isBadgeComplete( "temp", badgeID ) )
    //console_log( g_mapYouthNames[youthID] + ": (post) jsonComplete = '" + JSON.stringify( g_mapBadgeCompletion["temp"][badgeID] ) + "'" );

    delete g_setDirtyBadges["temp"];
    delete g_setDirtyRequirements["temp"];
    delete g_setYouthScorecarded["temp"];
    delete g_mapBadgeCompletion["temp"];
    delete g_mapCategoryCounts["temp"];
    delete g_mapTypeCounts["temp"];
    delete g_setMarkedRequirements["temp"];
    delete g_mapRequirementCompletion["temp"];
};

function processReportResults( mapReady, bByRequirement, strLabel )
{
    var listReport = $('#report-body');
    listReport.empty();

    var foundSomething = false;

    toggleSwitch( "#report-groupby li", g_reportGrouping == 'youth' );

    if ( g_reportGrouping != 'youth' ) 
    {
        $( '#report-instructions' ).css( "padding-right", "12px" );
        if ( g_report == 'ready' )
        {
            $( '#report-instructions' ).css( "padding-right", "75px" );
            $( '#report-instructions' ).html( "<div style='display:inline;'>" + (isMobileDevice() ? "Tap a name to see the badge requirement(s) that are ready to test." : "Click a name to see the badge requirement(s) that are ready to test.") + "</div><div class='no-print' style='margin: 0 -60px 15px 0;float: right; font-style:normal;'><a href='javascript:void(0)' onclick='window.print();'>Print</a></div>" );
            ""
        }
        else if ( g_report == 'awarded' )
            $( '#report-instructions' ).html( isMobileDevice() ? "Tap a name to mark the badge as awarded, or tap the 'Award All' button." : "Click a name to mark the badge as awarded, or click the 'Award All' button." );

        // we were passed a map of youth, keying a set of badges
        // we need to end up with a map of badges, key as set of youth
        var mapReadyInverse = {};
        for ( var youthID in mapReady )
        {
            var youth = g_dbTables['Youth'][youthID];
            if ( youth === undefined || youth.active == 0 )
                continue;       // we only care about active scouts

            for ( var reqID in mapReady[youthID] )
            {
                var badgeID = reqID;
                if ( bByRequirement )
                    badgeID = deriveBadgeID( reqID );

                if ( mapReadyInverse[badgeID] === undefined )
                    mapReadyInverse[badgeID] = {};

                mapReadyInverse[badgeID][youthID] = 1;
            }
        }

        // now, spit out the results
        for ( var badgeID in mapReadyInverse )
        {
            var listYouth = sortMembersByName( "Youth", mapReadyInverse[badgeID] );

            if ( listYouth.length > 0 )
            {
                var li = document.createElement( 'li' );
                li.className = "divider";
                li.innerHTML = getBadgeName( badgeID );
                listReport.append( li );
            }


            for ( var iYouth in listYouth )
            {
                var youthID = listYouth[iYouth];
                var youth = g_dbTables['Youth'][youthID];
                var strYouthName = htmlEncode( getDisplayName( youth, youthID ) );

                var strUniform = "";
                if ( g_report == "awarded" && g_listUniformIDs.length > 1 )
                    strUniform = "<span class='plapl'>(" + (youth.uniformid == 0 ? "Old <img class='shirt' src='./images/shirt.gif'/>" : "New <img class='shirt' src='./section-images/shirt.gif'/>") + ")</span>";

                var htmlRequirements = "";
                var li = document.createElement( 'li' );
                if ( g_report == 'awarded' )
                    li.innerHTML = "<a href='javascript:void(0)' onclick='if(confirm(\"Award this badge to " + strYouthName + "?\")){markAwarded2(" + youthID + ",\"" + badgeID + "\", true);$(this).parent().hide();}else{clearActive();}'>" + strYouthName + strUniform + "</a>";
                else
                {
                    var strReqs = "";
                    if ( g_report == 'ready' )
                    {
                        // get a list of all the ready requirements associated with this youth
                        // append these reqs in the correct order
                        var jsonSortedReqs = getSortedBadgeReqs( badgeID );
                        strReqs = getSortedLinksAnnotation( jsonSortedReqs, g_setReadyRequirements[youthID] );

                        if ( strReqs.length > 0 )
                            strReqs = "<span class='plapl no-print' style='padding-left:5px'>[" + strReqs.substring(2) + "]</span>";

                        strReqs += " <span class='final' youthid=" + youthID + " badgeid='" + badgeID + "'></span>";

                        for ( var i in jsonSortedReqs.IDs )
                        {
                            var req = jsonSortedReqs.reqs[jsonSortedReqs.IDs[i].replace(/^([A-Z]?)0/,"$1")];
                            if ( g_setReadyRequirements[youthID][req.id] === undefined )
                                continue;

                            var strDescription = htmlEncode( simplifyRequirementDescription( req.description.replace( /<\/?[bi]>/g, "" ) ) );
                            if ( hasSubRequirements( req.id ) )
                            {
                                for ( var iSubReq = 0; iSubReq < 26; iSubReq++ )
                                {
                                    var subReqLetter = String.fromCharCode( 97 + iSubReq );
                                    var subReq = g_dbTables['Requirements'][req.id+subReqLetter];
                                    if ( subReq === undefined )
                                        break;
                                    var subReqDescription = simplifyRequirementDescription( subReq.description.replace( /<\/?[bi]>/g, "" ) );
                                    strDescription += "<span class='subtext'>" + subReqLetter + ") " + htmlEncode( subReqDescription ) + "</span>";
                                }
                            }
                            var strDescription = "<span class='reqnum'>#" + req.requirement + "</span><div style='margin-left: 40px;'>" + strDescription + "</div>";
                            htmlRequirements += "<li class='print-only'>" + strDescription + "</li>";
                        }
                    }

                    li.className = "forward";
                    li.innerHTML = "<a href='javascript:void(0)' onclick='switchYouth(" + youthID +");window.setTimeout(function(){setCurrentBadge(\"" + badgeID + "\",true,\"dissolve\");},1000);'>" + strYouthName + strReqs + "</a>";
                }
                listReport.append( li );
                if ( htmlRequirements )
                    listReport.append( htmlRequirements );

                foundSomething = true;
            }
        }
    }
    else
    {
        if ( g_report == 'ready' )
        {
            $( '#report-instructions' ).html( "<div style='display:inline;'>" + (isMobileDevice() ? "Tap a badge to see the requirement(s) that are ready to test." : "Click a badge to see the requirement(s) that are ready to test.") + "<div class='no-print' style='margin: 0 -60px 0 15px;float: right; font-style:normal;'><a href='javascript:void(0)' onclick='window.print();'>Print</a></div>" );
        }
        else if ( g_report == 'awarded' )
            $( '#report-instructions' ).html( isMobileDevice() ? "Tap a badge to mark it as awarded, or tap the 'Award All' button." : "Click a badge to mark it as awarded, or click the 'Award All' button." );

        for ( var youthID in mapReady )
        {
            var youth = g_dbTables['Youth'][youthID];
            if ( youth === undefined || youth.active == 0 )
                continue;       // we only care about active scouts

            var strYouthName = getDisplayName( youth, youthID );
            var strUniform = "";
            if ( g_report == "awarded" && g_listUniformIDs.length > 1 )
                strUniform = "<span class='plapl'>(" + (youth.uniformid == 0 ? "Old <img class='shirt' src='./images/shirt.gif'/>" : "New <img class='shirt' src='./section-images/shirt.gif'/>") + ")</span>";

            // do we need to convert this map of youth and requirements to a map of youth and badgeid's?
            var setBadges = null;
            if ( bByRequirement )
            {
                var setBadges = {};
                for ( var reqID in mapReady[youthID] )
                    setBadges[deriveBadgeID( reqID )] = 1;
            }
            else
                setBadges = mapReady[youthID];

            // make the results be sorted by name
            var listSortedNames = new Array();
            var mapBadgesByName = {};
            for ( var badgeID in setBadges )
            {
                var strName = g_dbTables['MetaData'][badgeID].name;
                listSortedNames.push( strName );      // append this name to the end of a simple list
                mapBadgesByName[strName] = badgeID;
            }
            listSortedNames.sort();

            var listBadges = new Array();
            for ( var iBadge in listSortedNames )
            {
                var strName = listSortedNames[iBadge];
                listBadges.push( mapBadgesByName[strName] );
            }

            if ( listSortedNames.length > 0 )
            {
                var li = document.createElement( 'li' );
                li.className = "divider";
                li.innerHTML = htmlEncode( strYouthName ) + strUniform;
                listReport.append( li );
            }

            for ( var iBadge in listBadges )
            {
                var badgeID = listBadges[iBadge];
                var htmlRequirements = "";
                var li = document.createElement( 'li' );
                if ( g_report == 'awarded' )
                {
                    li.innerHTML = "<a href='javascript:void(0)' onclick='if(confirm(\"Award this badge to " + strYouthName + "?\")){markAwarded2(" + youthID + ",\"" + badgeID + "\", true);$(this).parent().hide();}else{clearActive();}'>" + g_dbTables['MetaData'][badgeID].name + "</a>";
                }
                else
                {
                    var strReqs = "";
                    if ( g_report == 'ready' )
                    {
                        // get a list of all the ready requirements associated with this youth
                        // append these reqs in the correct order
                        var jsonSortedReqs = getSortedBadgeReqs( badgeID );
                        strReqs = getSortedLinksAnnotation( jsonSortedReqs, g_setReadyRequirements[youthID] );

                        if ( strReqs.length > 0 )
                            strReqs = "<span class='plapl no-print' style='padding-left:5px'>[" + strReqs.substring(2) + "]</span>";

                        strReqs += " <span class='final' youthid=" + youthID + " badgeid='" + badgeID + "'></span>";

                        for ( var i in jsonSortedReqs.IDs )
                        {
                            var req = jsonSortedReqs.reqs[jsonSortedReqs.IDs[i].replace(/^([A-Z]?)0/,"$1")];
                            if ( g_setReadyRequirements[youthID][req.id] === undefined )
                                continue;

                            var strDescription = htmlEncode( simplifyRequirementDescription( req.description.replace( /<\/?[bi]>/g, "" ) ) );
                            if ( hasSubRequirements( req.id ) )
                            {
                                for ( var iSubReq = 0; iSubReq < 26; iSubReq++ )
                                {
                                    var subReqLetter = String.fromCharCode( 97 + iSubReq );
                                    var subReq = g_dbTables['Requirements'][req.id+subReqLetter];
                                    if ( subReq === undefined )
                                        break;
                                    var subReqDescription = simplifyRequirementDescription( subReq.description.replace( /<\/?[bi]>/g, "" ) );
                                    strDescription += "<span class='subtext'>" + subReqLetter + ") " + htmlEncode( subReqDescription ) + "</span>";
                                }
                            }
                            var strDescription = "<span class='reqnum'>#" + req.requirement + "</span><div style='margin-left: 40px;'>" + strDescription + "</div>";
                            htmlRequirements += "<li class='print-only'>" + strDescription + "</li>";
                        }
                    }

                    li.className = "forward";
                    li.innerHTML = "<a href='javascript:void(0)' onclick='switchYouth(" + youthID +");window.setTimeout(function(){setCurrentBadge(\"" + badgeID + "\",true,\"dissolve\");},1000);'>" + g_dbTables['MetaData'][badgeID].name + strReqs + "</a>";
                }
                listReport.append( li );
                if ( htmlRequirements )
                    listReport.append( htmlRequirements );

                foundSomething = true;
            }
        }
    }

    window.setTimeout( function() {
        if ( g_report == 'ready' )
        {
            for ( var youthID in g_setReadyRequirements )
            {
                var setHypotheticalBadges = {};
                for ( var reqID in g_setReadyRequirements[youthID] )
                    setHypotheticalBadges[deriveBadgeID( reqID )] = 1;

                testHypotheticalCompletion( youthID, setHypotheticalBadges, g_setReadyRequirements[youthID] );
            }
        }
    }, 1000 );

    if ( g_report == 'awarded' )
    {
        $( '#report-buttons' ).parent().html( '<div id="report-buttons" class="button" onclick="doAwardAll();">Award All</div>' );

        var strDate = formatDate( g_dateLastAward != -1 ? g_dateLastAward :  new Date().getTime() );
        
        $('#report-date-input').val( strDate );
        g_jdPickers['report-date-input'].selectDate();
        $('#report-date-input').parent().parent().children( "div.jdpicker-val" ).html( strDate );

        $('#report-date').parent().toggle( foundSomething );
        $('#report-date-header').toggle( foundSomething );
    }

    listReport.toggle( foundSomething );
    $('#report-noresults').toggle( ! foundSomething );
    $('#report-noresults').text( g_report == 'ready' ? ("No " + STR_YOUTHS + " have badges ready to test.") : ("No " + STR_YOUTHS + " have badges that need to be awarded.") );

    $("#report .name").text( strLabel );

    $('#report-instructions').toggle( foundSomething );
    $('#report-groupby').toggle( foundSomething );

    $('#report-buttons').toggle( foundSomething && g_report == 'awarded' );
    $('#report-wait').hide();
};

function checkScorecardProgress( listScorecardees, callback )
{
    if ( g_scorecardingYouth == null || g_setYouthScorecarded[g_scorecardingYouth] !== undefined )
    {
        // we're done... start with the next youth
        if ( listScorecardees.length == 0 )
        {  
            g_scorecardingYouth = null;
            callback();     // nothing youth left to scorecard?  Execute the callback
            return;
        }

        g_scorecardingYouth = listScorecardees.shift();
        scorecard( g_scorecardingYouth, true );
    }

    // checkback shortly
    window.setTimeout( function() { 
        checkScorecardProgress( listScorecardees, callback );
    }, 100 );
};

function scorecardAllYouth( callback )
{
    var listScorecardees = new Array();
    for ( var youthID in g_mapYouthNames )
        if ( g_setActiveYouth[youthID] !== undefined && g_setYouthScorecarded[youthID] === undefined )
            listScorecardees.push( youthID );

    checkScorecardProgress( listScorecardees, callback );
};

function findYouthNextStep( youthID )
{
    var listSteps = getSteps();
    for ( var iStep = 0; iStep < listSteps.length; iStep++ )
    {
        var badgeID = listSteps[iStep];
        if ( isBadgeComplete( youthID, badgeID ) || isBadgeAwarded( youthID, badgeID ) )
             continue;

        return badgeID;
    }

    return "";
};

function findNextSteps()
{
    var mapSteps = {};
    var listSteps = getSteps();

    var setUnassignedYouth = {};
    for ( var youthID in g_setYouthScorecarded )
        setUnassignedYouth[youthID] = 1;

    for ( var iStep = 0; iStep < listSteps.length; iStep++ )
    {
        var badgeID = listSteps[iStep];
        for ( var youthID in setUnassignedYouth )
        {
            if ( isBadgeComplete( youthID, badgeID ) || isBadgeAwarded( youthID, badgeID ) )
                continue;   // nope, this youth already has this badge

            if ( mapSteps[badgeID] === undefined )
                mapSteps[badgeID] = {};

            mapSteps[badgeID][youthID] = 1;
            delete setUnassignedYouth[youthID];
        }
    }

    return mapSteps;
};

function findAllBadge( badgeID )
{
    var mapSteps = {};
    mapSteps[badgeID] = {};

    for ( var youthID in g_setYouthScorecarded )
        mapSteps[badgeID][youthID] = 1;

    return mapSteps;
};

function findAllReady()
{
    var mapReady = {};

    for ( var youthID in g_setReadyRequirements )
    {
        for ( var reqID in g_setReadyRequirements[youthID] )
        {
            var badgeID = deriveBadgeID( reqID );
            if ( isBadgeAwarded( youthID, badgeID ) || isBadgeComplete( youthID, badgeID ) )
                continue;

            if ( getRequirementStatus( youthID, reqID ) == "ready" )
            {
                var parentReqID = deriveParentID( reqID );
                if ( parentReqID != reqID )
                {
                    var flag = getRequirementStatus( youthID, parentReqID );
                    if ( flag == "complete" || flag == "implicit" )
                         continue;
                }

                if ( mapReady[youthID] === undefined )
                    mapReady[youthID] = {};

                mapReady[youthID][badgeID] = 1;
            }
        }
    }

    return mapReady;
};


function findAllAwardables()
{
    var mapAwardables = {};

    for ( var youthID in g_setYouthScorecarded )
    {
        for ( var badgeID in g_dbTables['MetaData'] )
        {
            var strType = getBadgeType( badgeID );

            if ( strType === undefined || strType == "Placeholder" )
                continue;

            if ( isBadgeComplete( youthID, badgeID ) && ! isBadgeAwarded( youthID, badgeID ) )
            {
                if ( mapAwardables[youthID] === undefined )
                    mapAwardables[youthID] = {};
                mapAwardables[youthID][badgeID] = 1;
            }
        }
    }

    return mapAwardables;
};

function ensureUpdatable()
{
    if ( g_mode == 'group' && ! g_hasLocalStorage )
    {
        openLightBox( { text: "Sorry, your browser does not support updating records.", canClose: true } );
        return false;
    }

    return true;
};

function doAwardAll()
{
    if ( ! ensureUpdatable() )
        return;

    if ( ! confirm( "Mark all badges as awarded?" ) )
        return;

    var strTimestamp = getServerTimestamp();
    try {
        strTimestamp = g_jdPickers['report-date-input'].stringToDate( $('#report-date-input').val() ).getTime();
    } catch(e) {
        console_warn( "error '" + e + "' trying to get date" );
    }

    var mapAwardables = findAllAwardables();
    for ( var youthID in mapAwardables )
    {
        for ( var badgeID in mapAwardables[youthID] )
        {
            var strNotes = "";
            addAwarded( youthID, badgeID, strNotes, strTimestamp, getLoginID(), true );
            queueTransaction( youthID, badgeID, STATE_SET, FLAG_AWARDED, strNotes, false, strTimestamp );
        }
    }

    scheduleSync( "sync'd by award all", POST_UPDATE_INTERVAL );

    doReportRefresh();
};

function doResetScorecarding()
{
    for ( var youthID in g_setYouthScorecarded )
    {
        delete g_mapRequirementCompletion[youthID];
        delete g_mapBadgeCompletion[youthID];
        delete g_mapCategoryCounts[youthID];
        delete g_mapTypeCounts[youthID];

        delete g_setYouthScorecarded[youthID];
    }

    scorecardAllYouth( function() {
        doReportRefresh();
    } );
};

function buildAwardedReport()
{
    // need to scorecard all unscorecarded youth so we have a true section-wide view
    scorecardAllYouth( function() {
        processReportResults( findAllAwardables(), false, "Ready to Award" );
    } );
};

function buildReadyReport()
{
    // need to scorecard all unscorecarded youth so we have a true section-wide view
    scorecardAllYouth( function() {
        processReportResults( findAllReady(), false, "Ready to Test" );
    } );
};

function buildBadgeTableReport()
{
    // need to scorecard all unscorecarded youth so we have a true section-wide view
    scorecardAllYouth( function() {
        processBadgeTableReportResults( $('#table-report-badge-selector').val(), getInPlaceEdit() ? "Update in Bulk" :  "Completion By Badge" );
    } );
};

function buildEventTableReport()
{
    // need to scorecard all unscorecarded youth so we have a true section-wide view
    scorecardAllYouth( function() {
        processEventTableReportResults( $('#table-report-event-selector').val(), "Participation" );
    } );
};

function buildAttendanceTableReport()
{
    // need to scorecard all unscorecarded youth so we have a true section-wide view
    scorecardAllYouth( function() {
        processAttendanceTableReportResults( g_iAttendanceLabel, g_nAttendanceYear, getInPlaceEdit() ? "Update in Bulk" :  "Attendance" );
    } );
};

// SCOUT specific
function buildNextStepsReport()
{
    // need to scorecard all unscorecarded youth so we have a true section-wide view
    scorecardAllYouth( function() {
        processBadgeReportResults( findNextSteps(), "Who's Working on What" );
    } );
};

function doReportRefresh()
{
    clearActive();

    if ( g_report == 'ready' )
        buildReadyReport();
    else if ( g_report == 'awarded' )
        buildAwardedReport();
    else if ( g_report == 'nextsteps' )
        buildNextStepsReport();
};

function getInPlaceEdit()
{
    var allow = $('#table-report').attr( 'inplace-edit' );
    if ( allow === undefined )
        return false;

    return allow == "true";
};

function setInPlaceEdit( bEdit, bSyncAfter )
{
    var bWasEdited = getInPlaceEdit();
    if ( ! bEdit )
        $( '#table-report-groupby' ).hide();

    // IE8 bug... if I don't do the removeAttr, then the attr can get stuck at 'false'
    $('#table-report').removeAttr( 'inplace-edit' );
    $('#table-report').attr( 'inplace-edit', bEdit );

    if ( bWasEdited != bEdit )
    {
        if ( bWasEdited )
        {
            if ( bSyncAfter )
                scheduleSync( "sync'd by post bulk", 1000 );
        }
        else
            workerPause();
    }
};

function doTableReportRefresh()
{
    clearActive();

    $( '#table-report div[data-role=header] a' ).remove();
    $( '#table-report div[data-role=header]' ).append( "<a data-icon='arrow-l' href='javascript:void(0)' onclick='restorePage();'>Reports</a>" );

    if ( g_report == "badge" )
        buildBadgeTableReport();
    else if ( g_report == "attendance" )
        buildAttendanceTableReport();
    else
        buildEventTableReport();
};

function doAttendanceReportRefresh()
{
    clearActive();

    buildAttendanceTableReport();
};

function doTableEventReportRefresh()
{
    clearActive();

    buildEventTableReport();
};

function getRollupUnit( rollup )
{
    if ( rollup == ROLLUP_NIGHTS )
        return "n";
    else if ( rollup == ROLLUP_HOURS )
        return "h";
    else if ( rollup == ROLLUP_DAYS )
        return "d";
    else
        return "";
};

function buildAttendanceReportHeader( labels )
{
    var htmlOuting = "";

    // build up the images of the labels and attributes
    $.each( g_listSortedLabelIDs, function( iLabel, labelID ) {
        $.each( labels, function( iOuting, outingLabel ) {
            if ( outingLabel.id == labelID )
            {
                var label = g_dbTables['Labels'][outingLabel.id];

                var image = "";
                if ( label.image !== undefined && label.image.length > 0 ) 
                    image = "<img src='" + label.image + "'/>";

                var htmlLabel = image;

                if ( htmlLabel.length > 0 ) 
                {
                    if ( htmlOuting.length > 0 ) htmlOuting = "<br/>" + htmlOuting;
                    htmlOuting = htmlLabel + htmlOuting;
                }
            }
        });
    });

    return htmlOuting;
};

function buildEventReportRow( labels, useDataURI, bShowRollup )
{
    var htmlOuting = "";

    // build up the images of the labels and attributes
    $.each( g_listSortedLabelIDs, function( iLabel, labelID ) {
        $.each( labels, function( iOuting, outingLabel ) {
            if ( outingLabel.id == labelID )
            {
                var label = g_dbTables['Labels'][outingLabel.id];

                var htmlAttributes = "";
                if ( outingLabel.attributes !== undefined  )
                {
                    $.each( outingLabel.attributes, function( iAttribute, attributeID ) {
                        var attribute = g_dbTables['Attributes'][attributeID];
                        if ( attribute.image !== undefined && attribute.image.length > 0 )
                            htmlAttributes += "<img title='" + attribute.name + "' src='" + (useDataURI?attribute.image:("../" + label.key + "." + attribute.key + ".gif")) + "'/>";
                    });
                }

                var image = "";
                if ( label.image !== undefined && label.image.length > 0 ) 
                    image = "<img title='" + label.namesingular + "' src='" + (useDataURI?label.image:("../" + label.key + ".gif")) + "'/>";

                var htmlLabel = image + htmlAttributes;
                if ( label.rollup != ROLLUP_NONE && bShowRollup )
                    htmlLabel += "<span style='font-weight:normal;'>&times;</span>" + outingLabel.count + getRollupUnit( label.rollup );

                if ( htmlLabel.length > 0 ) 
                    htmlOuting += "<span labelid=" + outingLabel.id + " style='margin-right:10px;'>" + htmlLabel + "</span>";
            }
        });
    });

    return htmlOuting;
};

function getOutingAnnotation( outing )
{
    var strNoAccessText = "";
    var strNoAccessClass = "";

    if ( ! isReminder( outing ) )
    {
        if ( isUpcoming( outing ) )
            strNoAccessText = " <span class='noaccess'>(Scheduled)</span>";

        else if ( outing.youth.length == 0 )
        {
            strNoAccessText = " <span class='noaccess'>(No participants)</span>";
            strNoAccessClass = " noaccess";
        }
    }

    return {
        text: strNoAccessText,
        style: strNoAccessClass
    }
};

function buildEventReportLabelSection( mapOutings, labelID )
{
    var label = g_dbTables['Labels'][labelID];
    if ( label === undefined )
    {
        console_trace( "no label for " + labelID );
        return;
    }

    var image = "";
    if ( label.image !== undefined && label.image.length > 0 ) 
        image = "<img src='" + label.image + "'/>";

    $('#list-events').append( "<h1 class='listheader'>" + image + htmlEncode( label.name ) + "</h1>" );
    $('#list-events').append( "<ul class='rounded' labelid='" + label.id + "'></ul>" );

    var listOutingIDs = sortOutingsByDate( mapOutings );
    var nLastYear = 2100;
    var setYears = {};

    for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
    {
        var outingID = listOutingIDs[iOuting];
        var outing = mapOutings[outingID];

        var labels = getOutingYouthLabels( outing, g_outingYouthID );

        for ( var iOutingLabel = 0; iOutingLabel < labels.length; iOutingLabel++ )
        {
            var outingLabel = labels[iOutingLabel];
            if ( outingLabel.id == labelID )
            {
                var nYear = getOutingYear( outing );
                if ( nYear != nLastYear )
                {
                    setYears["y" + nYear] = 1;
                    $('#list-events ul[labelid=' + labelID + ']').append( "<li class='divider'>" + nYear + "-" + (nYear+1)
                        + (isReminder( outing ) ? "" : ("<div class='attendance-button' year=" + nYear + "></div>"))
                        + "</li>" );
                    nLastYear = nYear;
                }

                var htmlAttributes = "";
                if ( outingLabel.attributes !== undefined )
                {
                    for ( var iAttribute = 0; iAttribute < outingLabel.attributes.length; iAttribute++ )
                    {
                        var attributeID = outingLabel.attributes[iAttribute];
                        var attribute = g_dbTables['Attributes'][attributeID];
                        if ( attribute.image !== undefined && attribute.image.length > 0 )
                            htmlAttributes += "<img title='" + attribute.name + "' src='" + attribute.image + "'/>";

                    }
                }
                if ( htmlAttributes.length > 0 )
                    htmlAttributes = "<span style='margin-right: 10px;'>" + htmlAttributes + "</span>";

                var annotation = getOutingAnnotation( outing );

                var htmlDate = "<span class=\"subtext\">" + formatOutingDateRange( outing ) + "</span>";

                var strShrink = "";
                var strRollup = "";
                if ( label.rollup != ROLLUP_NONE )
                {
                    strRollup = "<small class='counter textonly'>" + outingLabel.count + " " + getRollupUnit( label.rollup ) + "</small>";
                    strShrink = " shrink2";
                }

                var htmlOuting = htmlAttributes + getOutingName(outing) + annotation.text + " " + htmlDate;
                if ( outing.shared !== undefined && ! outing.shared )
                    htmlOuting = "<img src='./images/notshared.gif' style='padding-right:5px;width:15px;' title='This event is not shared with " + STR_YOUTHS + "/parents'>" + htmlOuting;
                if ( isOutingUnresolved( outing, true ) )
                    htmlOuting = "<img src='./images/questionmark.gif' style='padding-right:5px;width:18px;margin-bottom:-1px;' title='This event needs to be finalized'>" + htmlOuting;
                var strName = "<span class='stunted" + strShrink + "'>" + htmlOuting + "</span>";

                var li = document.createElement( "li" );
                li.className = "forward" + annotation.style;
                li.innerHTML = "<a href='javascript:void(0)' onclick='g_listEvents=sortOutingsByDate(getReportEvents(" + labelID + ",g_outingYouthID)).reverse();doViewEvent(" + outing.id + ");'>" + strName + strRollup + "</a>";

                $('#list-events ul[labelid=' + labelID + ']').append( li );

                break;
            }
        }
    }

    // only show the dividers if there is more than one year
    //if ( size( setYears ) == 1 )
        //$( '#list-events ul[labelid=' + labelID + '] li.divider' ).remove();
};

function getOutingNights( outing )
{
    var nCount = 0;
    var label = null;
    if ( outing.labels == null )
        console_trace( "couldn't get labels for '" + outing + "'" );
    for ( var iOutingLabel = 0; iOutingLabel < outing.labels.length; iOutingLabel++ )
    {
        var outingLabel = outing.labels[iOutingLabel];

        label = g_dbTables['Labels'][outingLabel.id];
        if ( label.rollup == ROLLUP_NIGHTS || label.rollup == ROLLUP_DAYS )
        {
            nCount = outingLabel.count;
            break;
        }
    }

    if ( label && ( label.rollup == ROLLUP_NIGHTS || label.rollup == ROLLUP_DAYS ) && nCount > 0 )
        return nCount;

    return 0;
};

function formatOutingDateRange( outing )
{
    var htmlDate = formatDate( outing.date );

    var nCount = getOutingNights( outing );
    if ( isReminder( outing ) )
        nCount--;

    if ( nCount > 0 )
        htmlDate += " &rarr; " + formatDate( outing.date + 24*3600*1000*nCount );

    return htmlDate;
};

function buildEventLabelReport( mapOutings )
{
    if ( g_outingLabelID <= -1 )
    {
        toggleSwitch( "#events-report-groupby li", g_eventReportGrouping );
        $('#events-report-groupby').show();

        if ( g_eventReportGrouping )
        {
            // get a list of the labels we need to display
            var setLabels = {};
            for ( var outingID in mapOutings )
            {
                var outing = mapOutings[outingID];
                var labels = getOutingYouthLabels( outing, g_outingYouthID );
                for ( var iLabel = 0; iLabel < labels.length; iLabel++ )
                    setLabels[labels[iLabel].id] = 1;
            }

            for ( var iLabel = 0; iLabel < g_listSortedLabelIDs.length; iLabel++ )
                if ( setLabels[g_listSortedLabelIDs[iLabel]] !== undefined )
                    buildEventReportLabelSection( mapOutings, g_listSortedLabelIDs[iLabel] );
        }

        else
        {
            $('#list-events').append( "<h1 class='listheader'>All Events</h1>" );
            $('#list-events').append( "<ul class='rounded' labelid='" + g_outingLabelID + "'></ul>" )

            var listOutingIDs = sortOutingsByDate( mapOutings );

            var nLastYear = 2100;
            var setYears = {};

            for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
            {
                var outingID = listOutingIDs[iOuting];
                var outing = mapOutings[outingID];

                var nYear = getOutingYear( outing );
                if ( nYear != nLastYear )
                {
                    setYears["y" + nYear] = 1;
                    $('#list-events ul').append( "<li class='divider'>" + nYear + "-" + (nYear+1)
                        + "<div class='attendance-button' year=" + nYear + "></div>"
                    + "</li>" );
                    nLastYear = nYear;
                }

                var htmlOuting = buildEventReportRow( getOutingYouthLabels( outing, g_outingYouthID ), true );
                if ( outing.shared !== undefined && ! outing.shared )
                    htmlOuting = "<img src='./images/notshared.gif' style='padding-right:5px;width:15px;' title='This event is not shared with " + STR_YOUTHS + "/parents'>" + htmlOuting;
                if ( isOutingUnresolved( outing, true ) )
                    htmlOuting = "<img src='./images/questionmark.gif' style='padding-right:5px;width:18px;margin-bottom:-1px;' title='This event needs to be finalized'>" + htmlOuting;

                var annotation = getOutingAnnotation( outing );

                var li = document.createElement( "li" );
                li.className = "forward" + annotation.style;
                var htmlDate = "<span class=\"subtext\">" + formatOutingDateRange( outing ) + "</span>";

                li.innerHTML = "<a outingid=" + outing.id + " href='javascript:void(0)' onclick='doViewEvent(" + outing.id + ");'><span class='stunted'>" + htmlOuting + getOutingName( outing ) + annotation.text + htmlDate + "</span></a>";

                $('#list-events ul').append( li );
            }

            // REMOVE because we want a placeholder for the attendance button
            //if ( size( setYears ) == 1 )
                //$( '#list-events ul li.divider' ).remove();
        }
    }

    else
        buildEventReportLabelSection( mapOutings, g_outingLabelID );

    if ( g_mode == "group" && ( getRole() == "v" || getRole() == "n" ) )
    {
        $( '#list-events ul li.divider div.attendance-button').each( function() { 
            var button = '<div class="button inline" onclick="buildAttendanceReport(' + $(this).attr("year") + ',' + $(this).parent().parent().attr("labelid") + ');" href="javascript:void(0)"><img title="Attendance" src="./images/attendance2.gif"/></div>';
            $(this).append( button );
        });
    }
};

function buildOutingSelect()
{
    var listOutingIDs = sortOutingsByDate( g_mapOutings );

    var nLastYear = -1;

    var htmlSelect = "";
    var htmlOptGroup = "";
    for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
    {
        var outingID = listOutingIDs[iOuting];
        var outing = g_mapOutings[outingID];

        var nYear = getOutingYear( outing );
        if ( nYear != nLastYear )
        {
            if ( nLastYear > -1 && htmlOptGroup != "" )
                htmlSelect += "<optgroup label=\"" + nLastYear + "\">" + htmlOptGroup + "</optgroup>";
            nLastYear = nYear;
            htmlOptGroup = "";    // start again
        }

        htmlOptGroup += "<option value=" + outing.id + ">" + getOutingName( outing ) + "</outing>";
    }

    htmlSelect += "<optgroup label=\"" + nLastYear + "\">" + htmlOptGroup + "</optgroup>";

    htmlSelect = "<option value=-1>No Event</option>" + htmlSelect;
    $("#related-event-selector").html( htmlSelect );
};

function doSelectRelatedEvent( outingID )
{
    if ( outingID === undefined )
        outingID = getCurrentRelatedEvent();

    $( "#table-report-groupby li + li" ).toggle( outingID != -1 );
    doTableReportRefresh();
};

function getOutingName( outing )
{
    if ( outing === undefined )
        return "Unnamed";

    if ( ! outing.displayname )
        return "<span class='noaccess' style='font-style: italic;'>Unnamed</span>";

    return htmlEncode( outing.displayname );
};

/*
 * Return the scouting year of the event, where the scouting year is deemed to change
 * on Aug 15th.  I.e., Dec 25th, 2011 is part of the 2011 season, but Jul 7, 2010 is part of the 2009
 * season.  In general, the season is then display as 2010-2011, or 2009-2010 in the examples above.
 */
function getOutingYear( outing )
{
    var d = new Date();                 // the user-specified date of the event (e.g., 2009-oct-13)
    d.setTime( outing.date );
    var nYear = d.getFullYear();        // the calendar date of this object  (e.g., 2009)
    var newYearEnd = new Date( nYear, g_nSeasonMonth-1, g_nSeasonDay );   // month is base zero
    return d < newYearEnd ? nYear-1 : nYear; // is this in last year's season (e.g., 2008)
};

function doViewEvent( outingID ) 
{
    clearActive();
    if ( outingID == undefined )
    {
        openLightBox( { text: "No more events", timeout: 2000 } );
        return;
    }

    buildViewEvent( getOuting(outingID), true );
}

function buildEventLabelsPicker()
{
    var labels = $('#event-labels');
    labels.empty();

    for ( var iLabel = 0; iLabel < g_listSortedLabelIDs.length; iLabel++ )
    {
        var labelID = g_listSortedLabelIDs[iLabel];

        var label = g_dbTables['Labels'][labelID];

        var li = document.createElement('li');
        li.className = "checker";
        li.setAttribute( "labelid", labelID );

        var image = "";
        if ( label.image !== undefined && label.image.length > 0 ) 
            image = "<img src='" + label.image + "'/>";

        var htmlRollup = "";
        if ( label.rollup == ROLLUP_HOURS )
        {
            htmlRollup = "<option value=1>1 hour</option>";
            for ( var i = 2; i < 23; i++ )
                htmlRollup += "<option value=" + i + ">" + i + " hours</option>";
            htmlRollup += "<option value=24>24+ hours</option>";
            htmlRollup = "<span class='rollup' plabelid=" + labelID + " style='display:none;'><select onchange='g_hasUnsavedEventEdits=true;doChangeLabelCount(" + labelID + ");'>" + htmlRollup + "</select>";
        }
        else if ( label.rollup == ROLLUP_NIGHTS )
        {
            htmlRollup = "<option value=1>1 night</option>";
            for ( var i = 2; i < 14; i++ )
                htmlRollup += "<option value=" + i + ">" + i + " nights</option>";
            htmlRollup += "<option value=14>14+ nights</option>";
            htmlRollup = "<span class='rollup' plabelid=" + labelID + " style='display:none;'><select onchange='g_hasUnsavedEventEdits=true;doChangeLabelCount(" + labelID + ");'>" + htmlRollup + "</select>";
        }
        else if ( label.rollup == ROLLUP_DAYS )
        {
            htmlRollup = "<option value=1>1 day</option>";
            for ( var i = 2; i < 14; i++ )
                htmlRollup += "<option value=" + i + ">" + i + " days</option>";
            htmlRollup += "<option value=14>14+ days</option>";
            htmlRollup = "<span class='rollup' plabelid=" + labelID + " style='display:none;'><select onchange='g_hasUnsavedEventEdits=true;doChangeLabelCount(" + labelID + ");'>" + htmlRollup + "</select>";
        }

        li.innerHTML = "<a href='javascript:void(0)' onclick='g_hasUnsavedEventEdits=true;toggleLabelSelection( " + labelID + ", true );'><img glyph src='" + BIMG + "/images/blank.gif' style='border:none;'/>" + image + htmlEncode( label.namesingular ) + "</a>" + htmlRollup;

        labels.append( li );

        for ( var iAttribute = 0; iAttribute < g_listSortedAttributeIDs.length; iAttribute++ )
        {
            var attribute = g_dbTables['Attributes'][g_listSortedAttributeIDs[iAttribute]];
            if ( attribute.labelid != labelID )
                continue;

            li = document.createElement('li');
            li.className = "checker";
            li.setAttribute( "attributeid", attribute.id );
            li.setAttribute( "plabelid", label.id );

            image = "";
            if ( attribute.image !== undefined && attribute.image.length > 0 ) 
                image = "<img src='" + attribute.image + "'/>";
            li.innerHTML = "<a href='javascript:void(0)' onclick='g_hasUnsavedEventEdits=true;toggleAttributeSelection( " + attribute.id + ", true );'><img glyph src='" + BIMG + "/images/blank.gif' style='border:none;'/>" + image + htmlEncode( attribute.name ) + "</a>";

            labels.append( li );
        }
    }
};

function finishDeleteEvent( outing )
{
    deleteOuting(outing.id);

    updateEventSelector();
    doEventReportRefresh();
    doAttendanceReportRefresh();

    if ( lastUpPoint() == "alphabetic" && g_filter == "search" )
        populateAlphabetic( 'search', false, false );

    restorePage();

    queueOuting( outing, true );
};

function doDeleteEvent()
{
    if ( ! ensureUpdatable() )
        return;

    if ( ! ensureNotExpired() )
        return;

    if ( ! confirm( "Deletion is permanent, and cannot be undone.  Continue?" ) )
        return;

    var outingID = getCurrentEvent();

    // scrub everything but the event ID and force the date to be -1
    var outing = {
        sectionid: getSection(),
        id: outingID,
        date: -1,
        labels: new Array(),
        youth: new Array(),
        links: new Array()
    };

    var mozOutingCombos = getJsonTableMap( 'db-outing-combos' );
    delete mozOutingCombos["o"+outingID];
    setLocalStorage( 'db-outing-combos', JSON.stringify( mozOutingCombos ) );
    finishDeleteEvent( outing );
};

function getDayOnlyDate( timestamp )
{
    return new Date( parseInt(timestamp) ).getMidnight();;
};

function doDuplicateEvent()
{
    if ( ! ensureUpdatable() )
        return;

    if ( ! ensureNotExpired() )
        return;

    var outingID = getCurrentEvent();

    var now = new Date();
    var nowDay = now.getMidnight();

    var strOuting = JSON.stringify( getOuting( outingID ) );
    var outing1 = JSON.parse( strOuting );
    var nDayOfWeek = new Date( parseInt(outing1.date) ).getDay();

    // calculate the number of days between today, and the desired day of the week (may be zero)
    var nDaysOffset = ( nDayOfWeek - nowDay.getDay() + 7 ) % 7;
    outing1.date = (new Date( nowDay.getTime() + 86400000*nDaysOffset )).getTime(); 

    var dayOnlyDate = getDayOnlyDate( outing1.date );
    var mozOutingCombos = getJsonTableMap( 'db-outing-combos' );
    while ( true )
    {
        var isGood = true;
        for ( var key in mozOutingCombos )
        {
            var outing = mozOutingCombos[key];

            if ( outing === undefined || outing == null )
                continue;       // could be gaps in what looks like an array

            if ( getOutingMode( outing ) != g_mode )
                continue;

            // is there a conflict?
            if ( getDayOnlyDate( outing.date ).getTime() == dayOnlyDate.getTime() )
            {
                isGood = false;
                break;
            }
        }

        // if there was no conflict, we're done
        if ( isGood ) 
            break;

        outing1.date += 7*86400000; // increment by a week
        dayOnlyDate = getDayOnlyDate( outing1.date );
    }
    
    $('#duplication-mode-0 span.subtext').html( formatOutingDateRange( outing1 ) );
    $('#duplication-mode-0' ).attr( "date", outing1.date );
    $('#dayofweek').text( DOW_LONG[nDayOfWeek] ); 

    var outing2 = JSON.parse( strOuting );

    // first, adjust the outing to this year
    nDaysOffset = ( (new Date(parseInt(outing2.date))).getDayOfYear() - nowDay.getDayOfYear() + 366 ) % 366;
    outing2.date = (new Date( nowDay.getTime() + 86400000*nDaysOffset )).getTime(); 

    // second, also adjust this outing to be on the same day of the week as the original
    nDaysOffset = (( nDayOfWeek - new Date( parseInt(outing2.date) ).getDay() + 7 + 4 ) % 7) - 4;
    outing2.date = outing2.date + 86400000*nDaysOffset;
    $('#duplication-mode-1 span.subtext').html( formatOutingDateRange( outing2 ) );
    $('#duplication-mode-1' ).attr( "date", outing2.date );

    setDuplication( g_duplicationMode );       // TODO get a memory
    addPage( "#event-duplicator" );
};

function getOuting( outingID )
{
    return g_mapOutings["" + outingID];
}

function deleteOuting( outingID )
{
    delete g_mapOutings["" + outingID];
}

function setOuting( outing  )
{
    return g_mapOutings["" + outing.id] = outing;
}

function finishSaveEvent( outing, bNavigate )
{
    setOuting( outing );

    if ( hasUpPoint( 'upcoming' ) )
        buildUpcomingEvents( false );

    updateEventSelector();
    if ( bNavigate )
        restorePage();
    doEventReportRefresh();
    doAttendanceReportRefresh();

    if ( hasUpPoint( "tax-report" ) )
        doTaxReport( false );
    if ( hasUpPoint( "tax-details" ) )
        doTaxReportDetails( false );

    buildViewEvent( outing, false );
    findOutings( getSearch().toLowerCase() );

    queueOuting( outing, true );
};

function setNoNavigate( callback )
{
    g_hasUnsavedEventEdits = false;

    var wasBlocked = g_navigationBlocker;
    g_navigationBlocker = ( callback != null );

    if ( wasBlocked != g_navigationBlocker )
    {
        if ( wasBlocked )
            scheduleSync( "sync'd by post event edit", 1000 );
        else
            workerPause();
    }

    return;
};

function doSaveDuplicateEvent()
{
    if ( ! ensureUpdatable() )
        return;

    var outingID = getCurrentEvent();
    var outing = JSON.parse( JSON.stringify( getOuting( outingID ) ) );
    outing.closed = false;
    outing.youth = new Array();
    outing.leaders = new Array();
    outing.date = $('#event-duplicator li.checked').attr( "date" );
    
    // newly created outing's need an ID (which will get subsequently replaced by addOuting in worker.js)
    console_log( "created temp id (dup) '" + outing.id + "'" );
    outing.timestamp = getServerTimestamp();
    outing.id = -outing.timestamp;      // give it a temporary unique ID
    setCurrentEvent( outing.id );      // update the view

    for ( var reqID in g_mapUpcomingReqs )
    {
        if ( g_mapUpcomingReqs[reqID] === undefined )
            continue;

        for ( var iOuting = g_mapUpcomingReqs[reqID].length-1; iOuting >= 0; iOuting-- )
            if ( g_mapUpcomingReqs[reqID][iOuting] == outing.id )
                g_mapUpcomingReqs[reqID].splice( iOuting, 1 );
    }
    buildUpcomingReqs( outing );

    if ( g_mode == "handbook" )
        outing.youth.push( { id: 0 } );         // always add myself

    // update the local db
    var mozOutingCombos = getJsonTableMap( 'db-outing-combos' );
    mozOutingCombos["o" + outing.id] = outing;       // replace or insert this value
    setLocalStorage( 'db-outing-combos', JSON.stringify( mozOutingCombos ) );

    finishSaveEvent( outing, true );
};

function doSaveEvent()
{
    if ( ! ensureUpdatable() )
        return;

    var setOldYouth = {};
    var outingOld = getOuting( g_eventUnderEdit.id );
    if ( outingOld !== undefined )
        for ( var iYouth = 0; iYouth < outingOld.youth.length; iYouth++ )
            setOldYouth[outingOld.youth[iYouth].id] = 1;

    var setOldReqs = {};
    if ( outingOld !== undefined )
        for ( var iReq = 0; iReq < outingOld.links.length; iReq++ )
            setOldReqs[outingOld.links[iReq]] = 1;

    var outing = {
        sectionid: getSection(),
        id: g_eventUnderEdit === undefined ? getCurrentEvent() : g_eventUnderEdit.id,
        displayname: trim( $('#event-name').val() ),
        date: g_jdPickers['event-date-input'].stringToDate( $('#event-date-input').val() ).getTime(),
        location: trim( $('#edit-event input[name=location]').val() ),
        notes: trim( $('#edit-event-notes').val() ),
        leadernotes: trim( $('#edit-event-leadernotes').val() ),
        shared: getSwitch( '#event-sharing' ),
        closed: g_eventUnderEdit === undefined || g_eventUnderEdit.closed,
        cost: {
            total: trim( $('#edit-event input[name=cost-total]').val() ),
            program: trim( $('#edit-event input[name=cost-program]').val() )
        },
        labels: JSON.parse( JSON.stringify( g_eventUnderEdit.labels ) ),
        youth: new Array(),
        leaders: new Array(),
        links: new Array()
    };

    var isRem = isReminder( outing );

    if ( outing.cost.total == "" )
        outing.cost.total = 0;
    if ( outing.cost.program == "" )
        outing.cost.program = 0;

    // newly created outing's need an ID (which will get subsequently replaced by addOuting in worker.js)
    if ( outing.id == 0 )
    {
        outing.timestamp = getServerTimestamp();
        outing.id = -outing.timestamp;      // give it a temporary unique ID
        console_log( "created temp id '" + outing.id + "'" );
        setCurrentEvent( outing.id );      // update the view
    }

    if ( ! isRem )
        outing.links = g_eventUnderEdit.links;

    var wasClosed = ( outingOld !== undefined && ( outingOld.closed === undefined || outingOld.closed  ) );
    var needsReopen = false;

    if ( wasClosed )
    {
        if ( outing.links.length != outingOld.links.length )
            needsReopen = true;
        else for ( var iReq = 0; iReq < outing.links.length; iReq++ )
            if ( setOldReqs[outing.links[iReq]] === undefined )
                needsReopen = true;

        if ( wasClosed && needsReopen )
        {
             outing.closed = false;
             wasClosed = false;
             openLightBox( { text: "Because the related requirements were changed, this event needs to be re-finalized.", canClose: true } );
        }
    }

    // update the list of upcoming events
    if ( ! isRem )
    {
        for ( var reqID in g_mapUpcomingReqs )
        {
            if ( g_mapUpcomingReqs[reqID] === undefined )
                continue;

            for ( var iOuting = g_mapUpcomingReqs[reqID].length-1; iOuting >= 0; iOuting-- )
                if ( g_mapUpcomingReqs[reqID][iOuting] == outing.id )
                    g_mapUpcomingReqs[reqID].splice( iOuting, 1 );
        }
        buildUpcomingReqs( outing );

        if ( g_mode == "handbook" )
            outing.youth.push( { id: 0 } );         // always add myself
        else
            outing.youth = g_eventUnderEdit.youth; 
    }

    if ( wasClosed )
    {
        if ( outing.youth.length != outingOld.youth.length )
            needsReopen = true;
        else for ( var iYouth = 0; iYouth < outing.youth.length; iYouth++ )
            if ( setOldYouth[outing.youth[iYouth].id] === undefined )
                needsReopen = true;

        if ( wasClosed && needsReopen )
        {
             openLightBox( { text: "Because the participants were changed, this event needs to be re-finalized.", canClose: true } );
             outing.closed = false;
             wasClosed = false;
        }
    }

    if ( ! isRem && g_mode != "handbook" )
        outing.leaders = g_eventUnderEdit.leaders; 

    if ( outing.displayname.length == 0 )
    {
        openLightBox( { text: "Please name this event.", canClose: true } );
        return;
    }
    else if ( outing.labels.length == 0 )
    {
        openLightBox( { text: "Please apply label(s) to this event.", canClose: true } );
        return;
    }
    else if ( ! isRem && outing.cost.total != "" && ! outing.cost.total.match( /^\s*[$]?\s*[0-9.]+\s*$/ ) )
    {
        openLightBox( { text: "Only numeric values are allowed for the total cost.", canClose: true } );
        return;
    }
    else if ( ! isRem && outing.cost.program != "" && ! outing.cost.program.match( /^\s*[$]?\s*[0-9.]+\s*$/ ) )
    {
        openLightBox( { text: "Only numeric values are allowed for the program cost.", canClose: true } );
        return;
    }

    setNoNavigate( null );

    // update the local db
    var mozOutingCombos = getJsonTableMap( 'db-outing-combos' );
    mozOutingCombos["o" + outing.id] = outing;       // replace or insert this value
    setLocalStorage( 'db-outing-combos', JSON.stringify( mozOutingCombos ) );

    finishSaveEvent( outing, true );
};

function getLastMeeting()
{
    var listOutingIDs = sortOutingsByDate( g_mapOutings );

    for ( var iOuting = 0; iOuting < listOutingIDs.length; iOuting++ )
    {
        var outingID = listOutingIDs[iOuting];
        var outing = getOuting(outingID);

        for ( var iOutingLabel = 0; iOutingLabel < outing.labels.length; iOutingLabel++ )
            if ( outing.labels[iOutingLabel].id == g_meetingID && outing.displayname != "" )
                return outingID;
    }

    return -1;
};

function setDefaultMeetingParams( lastMeetingID )
{
    var outingParams = {
        date: (new Date()).getTime(),
        displayname: lastMeetingID == -1 ? "#1 - " : "",
        youth: new Array(),
        location: ""
    };

    // increment the date by 7 days, update the label if it starts with #32, prepopulate with all the youth
    var oldOuting = getOuting( lastMeetingID );
    if ( oldOuting !== undefined )
    {
        lastMeetingID = oldOuting.id;
        outingParams.date = oldOuting.date + 7*24*3600*1000;

        if ( oldOuting.displayname.match( /^([#]?)(\d+)(\W*)/ ) )
            outingParams.displayname = RegExp.$1 + (parseInt(RegExp.$2) + 1) + RegExp.$3;
        else if ( oldOuting.displayname.match( /(.+?)([#]?)(\d+)(\W*)/ ) )
            outingParams.displayname = RegExp.$1 + RegExp.$2 + (parseInt(RegExp.$3) + 1) + RegExp.$4;

        outingParams.location = oldOuting.location;
    }

    return outingParams;
};

function doNewEvent( timestamp )
{
    if ( ! ensureUpdatable() )
        return false;

    if ( ! ensureNotExpired() )
        return false;

    if ( timestamp === undefined )
        timestamp = getServerTimestamp();

    var outing = {
        id: 0,
        date: timestamp,
        displayname: "",
        notes: "",
        cost: { total: 0, program: 0 },
        labels: new Array(),
        shared: true,
        closed: false,
        youth: new Array(),
        links: new Array(),
        leaders: new Array()
    };

    setCurrentEvent( 0 );

    if ( g_outingLabelID > -1 )
    {
        var outingLabel = {
            id: g_outingLabelID,
            attributes: new Array(),
            count: 1
        };
        outing.labels.push( outingLabel );
    }

    if ( g_outingYouthID != -1 && outing.youth.length == 0 )
        outing.youth.push( { id: g_outingYouthID } );

    buildViewEvent( outing, true ); // we build this so we have somewhere to return to after cancelling
    buildEditEvent( outing );
};

function getYouthOverrides( youth, useDataURI )
{
   if ( youth != null && youth.labels !== undefined )
       return buildEventReportRow( youth.labels, useDataURI );
   
   return null;
};

function buildEventSummary( summary, outing, isInteractive )
{
    summary.empty();
     
    var htmlShared = "";
    if ( outing.shared !== undefined && ! outing.shared )
       htmlShared += "<img style='float:right;' src='./images/notshared.gif' title='This event is not shared with " + STR_YOUTHS +  "/parents' />";
    if ( isOutingUnresolved( outing, true ) )
       htmlShared += "<img class='no-print' style='float:right;margin-top:-5px;' src='./images/questionmark.gif' title='This event needs to be finalized' />";
    htmlShared += "<div class='no-print' style='float:right;margin-right:10px;margin-top:6px;'><a style='font-weight:normal;font-family:Helvetica, Arial, Sans serif;font-size:16px;text-decoration:underline;text-transform:none;' href='javascript:void(0)' onclick='window.print();'>Print</a></div>";

    var isRem = isReminder( outing );

    var o = new Outing(outing);
    summary.append( "<div class='title'>" + highliteSearch( o.getName() ) + htmlShared + "</div>" );
    if ( ! isRem ) 
         summary.append( "<p class='summary-labels'>???</p>" );
    //var strDateRange = formatOutingDateRange( outing );
    var strDateRange = o.formatDateRange();
    if ( isInteractive )
    {
        summary.append( "<h1>Date</h1>" );
        summary.append( "<p>" + strDateRange + "</p>" );
        if ( ! isRem && outing.location != null && outing.location != "" )
        {
            summary.append( "<h1>Location</h1>" );
            summary.append( "<p>" + linkifyDescription( outing.location.replace( /(http(s?):\/\/[^,; \n\r]+)/g, "[[$1]]" ), -1, false ) );
        }
    }
    else
    {
        summary.append( "<div style='margin-top:10px;'><h1 style='display:inline;margin-right:5px;'>Date:</h1>" + strDateRange + "</div>" );
        if ( ! isRem && outing.location != null && outing.location != "" )
        {
            summary.append( "<div style='margin-top:10px;'><h1 style='display:inline;width:100px;margin-right:5px;'>Location:</h1>" + htmlEncode( outing.location ) + "</div>" );
        }
    }

    if ( outing.cost === undefined )
        outing.cost = { total: 0, program: 0 };

    if ( ! isRem && outing.cost.total > 0 )
    {
        var showProgramCost = ! g_isEmbedded && g_mode != "handbook" && getLoginID() && getRole() != "p" && getRole() != "i";
        if ( SECTION_TYPE == "troop_au" || SECTION_TYPE == "unit" )
            showProgramCost = false;

        if ( isInteractive )
        {
            summary.append( "<h1>Cost</h1>" );
            if ( showProgramCost )
                summary.append( "<p>$" + outing.cost.total + (outing.cost.program > 0 ? " <span style='color:#621;font-size:90%'>($" + outing.cost.program + " program-related)</span>" : "" ) + "</p>" );
            else
                summary.append( "<p>$" + outing.cost.total + "</p>" );
        }
        else
        {
            if ( showProgramCost )
                summary.append( "<div style='margin-top:10px;'><h1 style='display:inline;margin-right:5px;'>Cost:</h1>$" + outing.cost.total + (outing.cost.program > 0 ? " <span style='color:#621;font-size:90%'>($" + outing.cost.program + " program-related)</span>" : "" ) );
            else
                summary.append( "<div style='margin-top:10px;'><h1 style='display:inline;margin-right:5px;'>Cost:</h1>$" + outing.cost.total );
        }
    }

    if ( g_mode != "handbook" )
    {
        var listYouth = new Array();
        var listOverrides = new Array();
        var mapOverrides = {};
        var mapFootnotes = {};
        var foundSelf = false;

        $.each( outing.youth, function( i, youth ) {
            if ( youth.id == g_currentUser ) 
                foundSelf = true;

            if ( g_dbTables['Youth'][youth.id] !== undefined && g_dbTables['Youth'][youth.id].active )
            {
                listYouth.push( htmlEncode( g_mapYouthNames[youth.id] ) );
                var overrides = getYouthOverrides( youth, true );
                if ( overrides ) 
                {
                    if ( mapOverrides[overrides] === undefined )
                    {
                        listOverrides.push( overrides );
                        mapOverrides[overrides] = new Array();
                        mapFootnotes[overrides] = listOverrides.length;
                    }
                    mapOverrides[overrides].push( youth.id );
                }
            }
        });

        if ( ! isRem && ! g_isEmbedded && (isInteractive || outing.youth.length > 0) )
        {
            summary.append( "<h1>Participants</h1>" );

            if ( outing.youth.length > 0 ) 
            {
                var setYouth = {};
                $.each( outing.youth, function( i, youth ) {
                    setYouth[youth.id] = 1;
                });
                var listSortedYouth = sortMembersByName( "Youth", setYouth );

                if ( getRole() != 'p' )
                {
                    var htmlYouth = "";
                    $.each( listSortedYouth, function( i, youthID ) {
                        if ( g_dbTables['Youth'][youthID] !== undefined )
                        {
                            if ( htmlYouth != "" )
                                htmlYouth += ", ";
                            var htmlNewYouth = htmlEncode( g_mapYouthNames[youthID] );
                            if ( ! g_dbTables['Youth'][youthID].active )
                                htmlNewYouth = "<span class='inactive'>" + htmlNewYouth + "</span>";
                            htmlYouth += htmlNewYouth;

                            var youth = getOutingYouth( outing.youth, youthID );
                            var overrides = getYouthOverrides( youth, true );
                            if ( mapOverrides[overrides] !== undefined )
                                htmlYouth += "<sup>" + mapFootnotes[overrides] + "</sup>";
                        }
                    });
                    var htmlFootnotes = "";
                    $.each( listOverrides, function( i, overrides ) {
                        htmlFootnotes += "<p class='footnote'><sup>" + (i+1) + "</sup><span>Overridden labels:</span>&nbsp;" + overrides + "</p>";
                    });
                    // need to add footnotes for overrides
                    summary.append( "<p>" + htmlYouth + "</p>" + htmlFootnotes );
                }

                // note, the counts for parent/youth accounts differ from the leader accounts, because as a non-leader we can't
                // ascertain whether any of the outing.youth are inactive or not.  I.e., we know there are 5, and one of the is "me",
                // so the best we can say is "You, plus 4 other Scouts" whereas for a leader we'd report "John, Jessica, Rachel, Scott"
                // while omitting "Christopher" because he is no longer active.
                else if ( foundSelf )
                {
                    if ( outing.youth.length == 1 )
                        summary.append( "<p>Me</p>" );
                    else
                        summary.append( "<p>Me, plus " + (outing.youth.length-1) + " other " + (outing.youth.length==2?STR_YOUTH:STR_YOUTHS) + "</p>" );
                }

                else
                    summary.append( "<p>" + outing.youth.length + " " + (outing.youth.length==1?STR_YOUTH:STR_YOUTHS) + "</p>" );
            }
            else if ( isOutingUnresolved( outing, true ) )
                summary.append( "<p class='comment'><img src='./images/questionmark.gif' style='padding-right:5px;width:18px;margin-bottom:-2px;' title='This event has no participants'>None</p>" );
                         
            else
                summary.append( "<p class='comment'>None</p>" );
        }

        if ( ! isRem && ! g_isEmbedded && (isInteractive || outing.leaders.length > 0) && getRole() != "i" )
        {
            summary.append( "<h1>" + STR_LEADERS + "</h1>" );
            if ( outing.leaders !== undefined && outing.leaders.length > 0 ) 
            {
                var htmlLeaders = "";
                $.each( outing.leaders, function( i, leader ) {
                    //if ( g_dbTables['Leaders'][leader.id] !== undefined && g_dbTables['Leaders'][leader.id].active )
                    if ( g_dbTables['Leaders'][leader.id] !== undefined )
                    {
                        if ( htmlLeaders != "" )
                            htmlLeaders += ", ";
                        var htmlNewLeader = htmlEncode( g_mapLeaderNames[leader.id] );
                        if ( ! g_dbTables['Leaders'][leader.id].active )
                            htmlNewLeader = "<span class='inactive'>" + htmlNewLeader + "</span>";
                        htmlLeaders += htmlNewLeader;
                    }
                });
                summary.append( "<p>" + htmlLeaders + "</p>" );
            }
                         
            else
                summary.append( "<p class='comment'>None</p>" );
        }

    }

    if ( isInteractive || (outing.notes !== undefined && outing.notes != "" ) )
    {
        if ( outing.notes !== undefined && outing.notes != "" )
        {
            summary.append( "<h1>Description</h1>" );
            summary.append( "<div class='creole'/>" );
            g_creole.parse( summary.children( '.creole' )[0], outing.notes )
            // this fixes an iPhone bug in which the first paragraph of the creole div was HUGE
            summary.children( '.creole' ).children( 'p:first-child' ).css( 'font-size','100%' );
        }
        else if ( ! isRem )
        {
            summary.append( "<h1>Description</h1>" );
            summary.append( "<p class='comment'>None</p>" );
        }
    }

    buildEventLinksView( summary, outing, isInteractive );

    if ( ! g_isEmbedded && g_mode == "group" && getRole() != "p" && getRole() != "i" )
    {
        if ( outing.leadernotes !== undefined && outing.leadernotes != "" )
        {
            summary.append( "<h1>" + STR_LEADERS + "' Notes</h1>" );
            summary.append( "<div class='creole leadernotes'/>" );
            g_creole.parse( summary.children( '.creole.leadernotes' )[0], outing.leadernotes );
            // this fixes an iPhone bug in which the first paragraph of the creole div was HUGE
            summary.children( '.creole.leadernotes' ).children( 'p:first-child' ).css( 'font-size','100%' );
        }
    }

    summary.children( '.creole' ).each( function() {
        $(this).html( highliteSearch( $(this).html() ) );
        $(this).children( 'ul' ).addClass( 'simple' );
    });

    updateLabelSummaryView( summary, outing );
};

function buildViewEvent( outing, bNavigate ) 
{
    if ( outing === undefined )
        return;

    $( "#view-event div[data-role=header] h1" ).html( isReminder( outing ) ? "Important Date" : "Event <span class='print-only'>Details</span>" ); // todo: get label text dynamically

    if ( outing.id == 0 )
        $('#delete-event').parent().parent().toggle( false );
    else
        $('#delete-event').parent().parent().toggle( g_mode == "handbook" || getLoginID() && ( getRole() == "v" || getRole() == "i" ) );

    var isEditable = g_mode == "handbook" || getRole() == 'v' || getRole() == "i";
    $( '#view-event div[data-role=header] a + a' ).toggle( isEditable );

    setCurrentEvent( outing.id );

    buildEventSummary( $('#event-summary'), outing, true );

    g_iEvent = -1;
    for ( var iEvent = 0; iEvent < g_listEvents.length; iEvent++ )
    {
        if ( outing.id == g_listEvents[iEvent] )
        {
            g_iEvent = iEvent;
            break;
        }
    }
    
    if ( g_iEvent < 0 )
        g_iEvent = 0;

    $('#view-event div.nextprev-page > div:first-child').toggleClass( "invisible", g_iEvent <= 0 );
    $('#view-event div.nextprev-page > div:first-child + div').toggleClass( "invisible", g_iEvent >= g_listEvents.length-1 );

    if ( bNavigate )
    {
        dumpUpPoints( "build-event" );
        var strCurrentPage = getCurrentPage();

        var strText = "Back";
        if ( strCurrentPage == "events" )
            strText = isReminder( outing ) ? "Dates" : "Events";
        else if ( strCurrentPage == "upcoming" )
            strText = "Schedule";
        else if ( strCurrentPage == "events-report" )
            strText = isReminder( outing ) ? "Dates" : "Events";
        else if ( strCurrentPage == "alphabetic" && g_filter == "search" )
            strText = "Search";
        else if ( strCurrentPage.match( /^badge-details-/ ) )
            strText = "Award";

        setBackText( "#view-event", strText );

        addPage( '#view-event' );
    }
};

function positionOfflineIcon( selector )
{
    var button = $( selector + ' div[data-role=header] a[data-icon=arrow-l]');
    if ( button.length == 0 ) 
        button = $( selector + ' div[data-role=header] a[data-icon=delete]');

    var offset = 0;
    if ( button.length > 0 ) 
        offset += button.width() + 20;

    var icon = $(selector + ' div[data-role=header] .offline-icon' );
    if ( icon.length > 0 )
        icon.css( 'margin-left', offset + "px" );
};

function setBackText( selector, strText )
{
    var button = $( selector + ' div[data-role=header] a[data-icon=arrow-l]');
    button.text( strText );

    var icon = $(selector + ' div[data-role=header] .offline-icon' );
    if ( icon.length > 0 ) 
        positionOfflineIcon( selector );
};

function doEditEvent()
{
    if ( ! ensureUpdatable() )
        return false;

    if ( ! ensureNotExpired() )
        return false;

    buildEditEvent( getOuting(getCurrentEvent()) );
};

function getCurrentEvent()
{
    return $('#view-event').attr( "eventid" );
};

function getCurrentRelatedEvent()
{
    return $('#related-event-selector').val();
};

function setCurrentEvent( outingID )
{
    $('#view-event').attr( "eventid", outingID );
};

function cancelEditEvent()
{
    var nEventID = getCurrentEvent();

    if ( restorePage() )
    {
        setNoNavigate( null );

        if ( nEventID == 0 )    // if it's new...
            restorePage();      // there is no event to land on if we cancel, do an extra restore to get back to the event list
    }
};

function checkEventNavigation( selector )
{
    if ( ! g_hasUnsavedEventEdits )
        return true;

    if ( selector == "#edit-event" )
        return true;

    if ( selector == "#edit-event-labels" )
        return true;

    if ( selector == "#event-youth-picker" )
        return true;

    if ( selector == "#event-leader-picker" )
        return true;

    if ( selector == "#event-badges-picker" )
        return true;

    if ( selector == "#event-badge-reqs-picker" )
        return true;

    if ( selector == "#badge-reqs-details" )
        return true;

    if ( confirm( "There are unsaved edits to this event.  Are you sure you want navigate away from this event?" ) )
    {
        setNoNavigate( null );
        return true;
    }

    return false;
};

function buildEditEvent( outing ) 
{
    // make a deep copy of the outing to be edited
    g_eventUnderEdit = JSON.parse( JSON.stringify( outing ) );

    var isRem = isReminder( outing );

    $( "#edit-event div[data-role=header] h1" ).text( isRem ? "Important Date" : "Event" ); // todo: get label text dynamically

    setNoNavigate( checkEventNavigation );

    $('#event-name').val( outing.displayname );

    $('#event-date-input').val( formatDate( outing.date ) );
    $('#event-date-input').parent().parent().children( "div.jdpicker-val" ).html( formatDate( outing.date ) );
    g_jdPickers['event-date-input'].selectDate();
    $('#edit-event-notes').val( "" );
    $('#edit-event-notes').val( outing.notes );
    $('#edit-event-leadernotes').val( "" );
    $('#edit-event-leadernotes').val( outing.leadernotes );
    $('#edit-event input[name=location]').val( outing.location === undefined ? "" : outing.location );
    $('#edit-event input[name=cost-total]').val( outing.cost.total );
    $('#edit-event input[name=cost-program]').val( outing.cost.program );
   
    $('#edit-event input[name=cost-program]').parent().toggle( PROGRAM_COSTS );
    $('#event-cost').toggle( ! isRem )
    $('#event-participants').parent().toggle( ! isRem )
    $('#event-leaders').parent().toggle( ! isRem )
    $('#event-related-badge-reqs').parent().toggle( ! isRem )
    
    $('#event-sharing li > span').text( windowWidth() <= WIDE ? "Share?" : ("Share with " + STR_YOUTHS + "/parents") );
    doEventSharing( outing.shared === undefined || outing.shared );

    $('#event-badges-picker ul li.checker' ).removeClass( "selected" );
    $('#badge-reqs-details tr[reqid] td[glyph].complete').removeClass( "complete" ).addClass( "incomplete" );
    for ( var iBadge = 0; iBadge < outing.links.length; iBadge++ )
    {
        toggleBadgeSelection( outing.links[iBadge] );
        updateRelatedBadgeReqsCount( deriveBadgeID( outing.links[iBadge] ) );
    }
    updateRelatedBadgeReqsTotalCount();

    var containerID = "#event-youth-picker";
    $( containerID + " li.checker" ).removeClass( "selected" );
    $( containerID + " li.divider.selector img").attr( "src", BIMG + "/images/incomplete.gif" );
    for ( var iYouth = 0; iYouth < outing.youth.length; iYouth++ )
        toggleYouthSelection( containerID, outing.youth[iYouth].id );

    var containerID = "#event-leader-picker";
    $( containerID + " li.checker" ).removeClass( "selected" );
    $( containerID + " li.divider.selector img").attr( "src", BIMG + "/images/incomplete.gif" );
    if ( outing.leaders !== undefined )
        for ( var iLeader = 0; iLeader < outing.leaders.length; iLeader++ )
            toggleLeaderSelection( containerID, outing.leaders[iLeader].id );

    updateEventYouthCount();
    updateEventLeaderCount();

    setupEventLabelPicker( outing.labels, outing.id, -1 );
    updateLabelSummary2( outing.labels );

    for ( var youthID in g_dbTables['Youth'] )
        updateYouthLabels( youthID )

    addPage( '#edit-event' );
};

function setupEventLabelPicker( labels, outingID, youthID )
{
    g_allowDefaulting = youthID == -1;

    // reset all the labels
    $('#event-labels li.checker').removeClass( "selected" ); 
    $('#event-labels li.checker[attributeid]').hide();
    $('#event-labels li.checker[labelid] select').val( 1 );
    $('#event-labels li.checker[labelid] span.rollup').hide();

    $('#event-labels li.checker[labelid] select' ).attr( "old", 1 );    // default value

    // iterate through the outing's labels, and set the appropriate UI elements
    if ( labels !== undefined )
    {
        $.each( labels, function( i, outingLabel ) {
            if ( outingLabel.attributes !== undefined ) 
                for ( var iAttribute = 0; iAttribute < outingLabel.attributes.length; iAttribute++ )
                    toggleAttributeSelection( outingLabel.attributes[iAttribute], false );

            $('#event-labels li.checker[labelid=' + outingLabel.id + '] select' ).val( outingLabel.count );
            $('#event-labels li.checker[labelid=' + outingLabel.id + '] select' ).attr( "old", outingLabel.count );
            toggleLabelSelection( outingLabel.id, outingID == 0 );     // normally we don't update the summary, except for new events
        });
    }
};

function getOutingLabelCount( labelID )
{
    return $('#event-labels li.checker[labelid=' + labelID + '] select' ).val();
};

function getSortedLinkBadges( outing )
{
    var setBadges = {};
    for ( var iReq = 0; iReq < outing.links.length; iReq++ )
        setBadges[deriveBadgeID(outing.links[iReq])] = 1;

    return sortBadgesByName( setBadges );
};

function getSortedLinksAnnotation( jsonSortedReqs, setReqs, outing )
{
    var strReqs = "";
    for ( var i in jsonSortedReqs.IDs )
    {
        var req = jsonSortedReqs.reqs[jsonSortedReqs.IDs[i].replace(/^([A-Z]?)0/,"$1")];
        if ( setReqs[req.id] !== undefined )
        {
            var strRequirementLabel = req.requirement;
            if ( strRequirementLabel.match( /^([A-Z]?)(\d+[a-z]?)$/ ) )    // parse the requiremnt into "B"+"15c" or ""+"7"
            {
                strRequirementLabel = RegExp.$2;        // assume we just need the number part
                var strPart = RegExp.$1;
                if ( strPart )      // so we have an "A" or "B" etc?
                {
                     for ( var reqID in jsonSortedReqs.reqs )
                     {
                         var otherReq = jsonSortedReqs.reqs[reqID];
                         if ( otherReq.requirement == "B1" )
                         {
                             strRequirementLabel = req.requirement;         // we need the part in the label
                             break;
                         }
                     }
                }
            }
            strRequirementLabel = "#" + strRequirementLabel;
            if ( outing !== undefined && isTallyReq( req.id ) )
            {
                var sRollup = getReqRollup( req.id );
                if ( sRollup != "" )
                    sRollup = "" + getOutingTallyCount( outing, -1, req.id ) + sRollup;
                strRequirementLabel = strRequirementLabel + "<span class='tally'>+" + sRollup + "</span>";
            }
            strReqs += ", " + strRequirementLabel;
        }
    }

    return strReqs;
};

function getReqRollup( reqID )
{
    var autolink = g_dbTables['Requirements'][reqID].autolinks;
    if ( autolink === undefined || ! autolink || autolink.match( /<\d+>/ ) )
        return "";

    if ( autolink.match( /^([^.]+)/ ) )
    {
        for ( var iLabel = 0; iLabel < g_listSortedLabelIDs.length; iLabel++ )
        {
            var label = g_dbTables['Labels'][g_listSortedLabelIDs[iLabel]];
            if ( label.rollup != ROLLUP_NONE )
                return getRollupUnit( label.rollup );
        }
    }

    return "";
};

function buildEventLinksView( summary, outing, isInteractive )
{
    var listBadges = getSortedLinkBadges( outing );

    if ( isInteractive )
    {
        updateNextPrevBadgeList( listBadges );
        $('#event-links').empty();
    }

    var htmlBadges = "";

    for ( var iBadge = 0; iBadge < listBadges.length; iBadge++ )
    {
        var badgeID = listBadges[iBadge];

        // get a list of all the link requirements associated with this badge
        var setReqs = {};
        for ( var iReq = 0; iReq < outing.links.length; iReq++ )
            if ( deriveBadgeID( outing.links[iReq] ) == badgeID )
                setReqs[outing.links[iReq]] = 1;

        // append these reqs in the correct order
        var jsonSortedReqs = getSortedBadgeReqs( badgeID );
        var strReqs = getSortedLinksAnnotation( jsonSortedReqs, setReqs, outing );

        if ( strReqs.length > 0 )
            strReqs = "<span class='plapl' style='padding-left:5px'>[" + strReqs.substring(2) + "]</span>";

        if ( isInteractive )
        {
            $('#event-links').append( "<li class='forward'><a linkbadgeid='" + badgeID + "' href='javascript:void(0)' onclick='saveUpPoint();$(\"#related-event-selector\").val(" + outing.id + ");setCurrentBadge(\"" + badgeID + "\", true, \"dissolve\");'>" + htmlEncode( getBadgeName(badgeID) ) + strReqs + "</a></li>" );
            for ( var i in jsonSortedReqs.IDs )
            {
                var req = jsonSortedReqs.reqs[jsonSortedReqs.IDs[i].replace(/^([A-Z]?)0/,"$1")];
                if ( setReqs[req.id] === undefined ) 
                    continue;       // not related
                var strDescription = htmlEncode( simplifyRequirementDescription( req.description.replace( /<\/?[bi]>/g, "" ) ) );
                if ( hasSubRequirements( req.id ) )
                {
                    for ( var iSubReq = 0; iSubReq < 26; iSubReq++ )
                    {
                        var subReqLetter = String.fromCharCode( 97 + iSubReq );
                        var subReq = g_dbTables['Requirements'][req.id+subReqLetter];
                        if ( subReq === undefined )
                            break;
                
