The Gravey Framework and RATS RIA

rats.js

Summary

Collection of application level classes. This collection implements a rich-internet-application, RATS (Reconcile Account Transaction System), which is a demonstration of the Gravey framework.

Version: 2.5

Requires:

Author: Bruce Wallace (PolyGlotInc.com)


Class Summary
Account DOMAIN-OBJECT: This class encapsulates the identifying attributes of a Account.
AccountSelectController This class manages a popup menu controller specifically for the Account Selection List.
AccountSelectionModel This class encapsulates the data model for "which Account is currently selected".
Balance DOMAIN-OBJECT: This class encapsulates an Account balance.
BalanceListModel DOMAIN-OBJECT: This class encapsulates the data model for the Balance list of a particular Account.
BalanceListView This class produces a view of a BALANCE List.
BalanceView This class produces a view of a Balance.
CancelAllAccountChangesCmd This class implements the "revert this account" command.
CloseWindowIfDataSavedCmd This class implements the "close this window if all data is saved" command.
ComplexView This class produces a view of the complex type of a Transaction.
CountersView This class produces a view of the current selection and total size of a selection model (ie "I of N").
CreateTranCmd This class implements the append and insert transaction cmds.
DataPanelView This class produces the view of the entire data panel.
DebugView This class acts as quick and dirty debug view that pops up a window showing the current state of the arbitrary data model being watched.
DeleteTranCmd This class implements the delete transaction cmd.
EditButtonController This class is an MVCImgButtonController for "edit" buttons which shouldnt be enabled while app is in "read only" mode.
EditTranCmd This class implements the edit transaction cmd.
LoadAccountDataCmd This class implements the "load account data" command.
RatsCmd This is the abstract base class for RATS commands.
SaveAllAccountChangesCmd This class implements the "save changes" command.
SelectAccountCmd This class implements the "select account" command.
SelectFirstUnreconciledCmd This class implements the "select first unreconciled account" command.
ToDoView This class produces a view of how many unreconciled accounts are left.
ToggleBalCmd This class implements the toggle balance display cmd.
ToggleTranCmd This class implements the toggle transaction selection cmd.
TotalsView This class produces a view of the unreconciled totals for an entire Account.
Transaction DOMAIN-OBJECT: This class encapsulates a Account "transaction".
TransactionListView This class produces a view of a Transaction List.
TransactionSelectedModel This class encapsulates the data model for the flag indicating whether a Transaction is selected.
TransactionView This class produces a view of a Transaction.
TrTypeMenuCmd This class implements the trans type menu edit command.
UnreconciledView This class produces a view of the unreconciled totals of a Balance.

Method Summary
static Object _RATSRequest( <String> aRequestName, <Function> aReplyEventHandler, <String> aBANK, <String> anACCT, <String> optPostData, <boolean> optWaitFlag )
           Make a REST-style AJAX request to the RATS server.
NOTE: this is a Command helper function.
NEVER call this from outside the context of a Command object!
static void _requestAccountLoad( <String> aBANK, <String> anACCT )
           Make a Load-Account-Data request to the server.
NOTE: this is a Command helper function.
NEVER call this from outside the context of a Command object!
static void _requestAccountSave( <String> aBANK, <String> anACCT, <String> updates )
           Make a Save-Account-Data request to the server.
NOTE: this is a Command helper function.
NEVER call this from outside the context of a Command object!
static boolean _selectFirstUnreconciled( <boolean> selectFirst )
           Select the first unreconciled Account
NOTE: this is a Command helper function.
NEVER call this from outside the context of a Command object!
static String findComplexCode( <MVCCollection> transtypes )
           static routine to search for the special "complex" code
static void onAccountLoadReply( <grvXMLreq> xmlReq )
           handle AJAX event for 'reply received from "load account data" server-request'
static void onAccountSaveReply( <grvXMLreq> xmlReq )
           handle AJAX event for 'reply received from "save account data" server-request'
static void onAccountSelect( <event> e, <int> selectedAccountKey )
           handle event for 'user selects from the account menu'
static void onBalanceClick( <event> e, <int> balanceIndex )
           handle event for 'click on a balance record'
static Object onBeforeUnLoad()
           handle event for 'about to leave web page'; warn if attempting to leave with unsaved changes
static void onCancelBtnPressed()
           handle event for 'cancel button pressed'
static void onDeleteBtnPressed()
           handle event for 'delete button pressed'
static void onEditBtnPressed()
           handle event for 'edit button pressed'
static void onExitBtnPressed()
           handle event for 'exit button pressed'
static void onInsertBtnPressed()
           handle event for 'insert button pressed'
static void onLoad()
           handle event for 'entering web page'
static void onSaveBtnPressed()
           handle event for 'save button pressed'
static void onTransactionClick( <event> e, <int> balanceIndex, <int> transIndex )
           handle event for 'click on a transaction record'
static Object onTrTypeSelect( <event> e, <String> viewID )
           handle event for 'user selects from the transaction type menu'
static void onUnreconciledClick( <event> e, <int> balanceIndex )
           handle event for 'click on an unreconciled panel'

// This file uses JSDoc-friendly comments [ http://jsdoc.sourceforge.net/ ]

/**
 * @file         rats.js
 * @fileoverview Collection of application level classes.
 * This collection implements a rich-internet-application,
 * RATS (Reconcile Account Transaction System), which is
 * a demonstration of the Gravey framework.
 * @author       Bruce Wallace  (PolyGlotInc.com)
 * @requires     grvMVC.js
 * @requires     grvAJAX.js
 * @version      2.5
 */

//////////////////////////////////////////////////////////////////
// GRAVEY LEXICAL CODING CONVENTIONS:
// (*) All private variables or functions start with "_"
// (*) All variables and functions start with a lowercase letter
// (*) All Classes start with an uppercase letter
// (*) All Class methods and instance variables start with lowercase
// (*) All Class "static" methods and variables start with uppercase
// (*) All constants start with "k"
// (*) All global variables start with "g"
// (*) All event handler functions start with "on"
//
// (*) All Gravey utility global variables start with "gGrv"
// (*) All Gravey MVC     global variables start with "gMVC"
// (*) All Gravey EDO     global variables start with "gEDO"
// (*) All Gravey utility functions start with "grv"
// (*) All Gravey MVC     functions start with "mvc"
// (*) All Gravey MVC event handler functions start with "onMVC"
// (*) All Gravey EDO event handler functions start with "onEDO"
// (*) All Gravey MVC classes start with "MVC"
// (*) All Gravey EDO classes start with "EDO"
//////////////////////////////////////////////////////////////////

grvTraceCmp("rats.js: Begin");

/** global flag to enable/disable the totals column of the UI */
var kEnableTotalsColumn = true;

				////////////////////////////////////////////
				//////////// Domain Objects ////////////////
				////////////////////////////////////////////

/** constant defining "broadcast" message separator character */
var kMsgSep = "~";


Class(Account,["Bank ID","Account #","Reconciled Flag"]);
/**
 * @class DOMAIN-OBJECT: This class encapsulates the identifying
 * attributes of a Account.
 * This class implements the {@link MVCMenuItem} interface.
 * @extends GrvObject
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function Account()
{
	/** @param {String} bankID account's bank ID
	 *  @param {String} acct account number
	 *  @param {boolean} reconciled true if account is already reconciled
	 */
	this.konstructor = function( bankID, acct, reconciled )
	{
		// define instance variables
		this.bank = bankID;
		this.acct = acct;
		this.todo = !reconciled; //todo===not-reconciled
	}

	/** set reconciled status to given value and "broadcast" */
	this.updateStatus = function( reconciled ){
		this.todo = ! reconciled;
		this.broadcastReconciledStatus();
	}

	/** return "this" formatted for account menu item @type String */
	this.getDescription = function() {
		return grvDiamond(!this.todo) + this.toString();
	}

	/** Send Account Reconciled Status Message */
	this.broadcastReconciledStatus = function() {
		grvSendStatusMessage( this.toBroadcastString() );
	}

	/** return Account Reconciled Status Message.
	 * msg format: "UP~acct~bank~reconciledFlag"
	 * where reconciledFlag is encoded as 'Y'==true, 'N'==false...
	 * @return message
	 * @type String
	 */
	this.toBroadcastString = function()
	{
	 	var		s = new Array();
				s.push( "UP" );
				s.push( this.acct );
				s.push( this.bank );
				s.push( this.todo ?'N':'Y' );
		return	s.join( kMsgSep );
	}

	/** return "this" as human-readable string @type String */
	this.toString = function()
	{
	 	var		s = new Array();
	 			s.push( "Acct" );
				s.push( this.acct );
				s.push( this.bank );
		return	s.join( "-" );
	}
}


/** static routine to search for the special "complex" code
 * @param {MVCCollection} transtypes Decode collection of transaction-types
 * @return the "code" of the special "complex" transaction type
 * @type String
 */
function findComplexCode( transtypes )
{
	var code = 2; //heuristic ;-)
	transtypes.iterate
	(
	  function( k, decode )
	  {
	  	if (decode.aux && decode.aux.length>0
	  	 && decode.aux[ decode.aux.length-1 ] == 'C')
	  	{
		  	code = decode.code;
		  	return true;
	  	}
	  }
	);
	return code;
}

// Constants defining IDs for the 4 balance properties
var kBalanceIdA = 'BalanceA';
var kBalanceIdB = 'BalanceB';
var kBalanceIdC = 'BalanceC';
var kBalanceIdD = 'BalanceD';

// Constants defining IDs for the 2 code properties
var kCodeIdTrType  = 'TransactionType';
var kCodeIdComplex = 'ComplexType';


Class(Balance,["Date","Balance A","Balance B","Balance C",
"Balance D","Is Reconciled?","System","Transactions List Model"]);
/**
 * @class DOMAIN-OBJECT: This class encapsulates an Account balance.
 * @extends GrvObject
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function Balance()
{
	/** @param {String} sdate balance date
	 *  @param {Float} balA balance A
	 *  @param {Float} balB balance B
	 *  @param {Float} balC balance C
	 *  @param {Float} balD balance D
	 *  @param {boolean} reconciled true if balance is already reconciled
	 *  @param {String} system where did balance come from
	 *  @param {MVCListModel} rList {@link Transaction} list
	 */
	this.konstructor = function( sdate, balA, balB, balC, balD, reconciled, system, rList )
	{
		// define instance variables
		this.sdate = sdate;
		this.todo  = !reconciled;
		this.whom  = system;
		this.list  = rList;
		this.open  = false;
	
		this[kBalanceIdA] = balA;
		this[kBalanceIdB] = balB;
		this[kBalanceIdC] = balC;
		this[kBalanceIdD] = balD;
	}

	/** return the debug details of "this" @type String */
	this.dump = function()
	{
		return "S...["
		      +"\n    date="+this.sdate
		      +"\n    todo="+this.todo
		      +"\n    whom="+this.whom
		      +"\n    balA="+this[kBalanceIdA]
		      +"\n    balB="+this[kBalanceIdB]
		      +"\n    balC="+this[kBalanceIdC]
		      +"\n    balD="+this[kBalanceIdD]
		      +"\n    list="+this.list.dump()
		      +"\n]...S\n";
	}

	/** return whether any of our transactions are in Edit mode @type boolean */
	this.anyTransactionsInEdit = function()
	{
		var inEdit = false;
		this.list.iterate( function(i,r){ if (r.inEdit()) return inEdit = true; } );
		return inEdit;
	}

	/** return whether all of our transactions are valid @type boolean */
	this.isValid = function()
	{
		var valid = true;
		this.list.iterate( function(i,r){ if (!r.isValid()){valid = false; return true;} } );
		return valid;
	}

	/** return the specified transaction @type Transaction
	 * @param {int} rIndex index into transaction list for this balance
	 */
	this.getTransaction = function( rIndex )
	{
	    return this.list.getItem( rIndex );
	}

	/** return sum of balances for this balance @type Float */
	this.getTotal = function()
	{
		var a = parseFloat( this[kBalanceIdA] );
		var b = parseFloat( this[kBalanceIdB] );
		var c = parseFloat( this[kBalanceIdC] );
		var d = parseFloat( this[kBalanceIdD] );
		return a + b + c + d;
	}

	/** return sum of all Net totals for this balance @type Float */
	this.getNetTotal = function()
	{
		var a = parseFloat( this.getNetA() );
		var b = parseFloat( this.getNetB() );
		var c = parseFloat( this.getNetC() );
		var d = parseFloat( this.getNetD() );
		return a + b + c + d;
	}

	/** return sum of specified balance for this balance and
	 * all its transactions.
	 * @param {String} b attribute name of desired balance 
	 * @type Float
	 */
	this.getNet = function(b)
	{
		var sum = parseFloat( this[b] );//force the data type to number
		this.list.iterate( function(i,r){ if (!r.inactive()) sum -= r[b]; } );
		return sum;
	}

	/** convenience call to {@link #getNet} for balance D @type Float */
	this.getNetD = function(){ return this.getNet( kBalanceIdD ); }

	/** convenience call to {@link #getNet} for balance A @type Float */
	this.getNetA = function(){ return this.getNet( kBalanceIdA ); }
	
	/** convenience call to {@link #getNet} for balance B @type Float */
	this.getNetB = function(){ return this.getNet( kBalanceIdB ); }
	
	/** convenience call to {@link #getNet} for balance C @type Float */
	this.getNetC = function(){ return this.getNet( kBalanceIdC ); }
	
	/** return the formatted title for this balance @type String */
	this.getTitle = function(){ return "Balances - "+this.sdate; }
}



Class(Transaction, [
	"complex Code",
	"transaction Type",
	"amount A",
	"amount B",
	"amount C",
	"amount D",
	"edit Date",
	"edit User",
	"unique Key"
]);
/**
 * @class DOMAIN-OBJECT: This class encapsulates a Account "transaction".
 * @extends GrvObject
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function Transaction()
{
	/** @param {String} complexCode complex code
	 *  @param {String} transType "transaction type" code
	 *  @param {Float} amountA balance A
	 *  @param {Float} amountB balance B
	 *  @param {Float} amountC balance C
	 *  @param {Float} amountD balance D
	 *  @param {String} editDate date of last edit
	 *  @param {String} editUser ID of last user to edit
	 *  @param {int} theKey Oracle primary key of this record
	 *  @param {int} optEditStatus optional edit status where the
	 * status codes are: 0=no-change; 1=delete; 2=create; 3=hidden; 4=update
	 */
	this.konstructor = function
	(
		complexCode,
		transType,
		amountA,
		amountB,
		amountC,
		amountD,
		editDate,
		editUser,
		theKey,
		optEditStatus
	){
		// define instance variables
		this.date  = editDate;
		this.whom  = editUser;
		this.key   = theKey;
	
		// each of these have a "validity" attribute associated with it
		this[ kCodeIdComplex ] = complexCode;
		this[ kCodeIdTrType  ] = transType;
	
		this[ kBalanceIdA ] = amountA;
		this[ kBalanceIdB ] = amountB;
		this[ kBalanceIdC ] = amountC;
		this[ kBalanceIdD ] = amountD;
	
		//0=no-change; 1=delete; 2=create; 3=hidden; 4=update
		this.editStatus = optEditStatus?optEditStatus:0;
	}

	/** return the debug details of "this" @type String */
	this.dump = function()
	{
		return "T...["
		      +"\n    date="+this.date
		      +"\n    whom="+this.whom
		      +"\n     key="+this.key
		      +"\n    edit="+this.editStatus
		      +"\n     src="+this[kCodeIdTrType]
		      +"\n     col="+this[kCodeIdComplex]
		      +"\n    balA="+this[kBalanceIdA]
		      +"\n    balB="+this[kBalanceIdB]
		      +"\n    balC="+this[kBalanceIdC]
		      +"\n    balD="+this[kBalanceIdD]
		      +"\n]...R\n";
	}
	
	/** return a clone of this object @type Transaction */
	this.clone = function()
	{
		return new Transaction(
						this[ kCodeIdComplex  ],
						this[ kCodeIdTrType   ],
						this[ kBalanceIdA ],
						this[ kBalanceIdB  ],
						this[ kBalanceIdC    ],
						this[ kBalanceIdD ],
						this.date,
						this.whom,
						this.key,
						this.editStatus
					);
	}

	/** return a string of updates for this transaction formatted as<pre>
	 * "{key}~{action}~{sdate}~{ttype}~{ccode}~{balA}~{balB}~{balC}~{balD}"
	 * where the fields are defined as follows...
	 *     {key}    = the transaction's unique key number
	 *     {action} = the transaction's action code
	 * (encode as strings 'C','D', and 'U' for values create, delete, and update.)
	 *     {sdate}  = the transaction's parent balance date
	 *     {ttype}  = the transaction's new transaction Type
	 *     {ccode}  = the transaction's new complex code
	 *     {balA}   = the transaction's new balance A
	 *     {balB}   = the transaction's new balance B
	 *     {balC}   = the transaction's new balance C
	 *     {balD}   = the transaction's new balance D
	 *</pre>
	 * @type String
	 * @param {Balance} parentBalance balance "this" is attached to
	 */
	this.asUpdateString = function( parentBalance )
	{
	 	var		uStr = new Array();
				uStr.push( this.key ? this.key : "0" );
				uStr.push( this.action()             );
				uStr.push( parentBalance.sdate       );
				uStr.push( this[ kCodeIdTrType   ]   );
				uStr.push( this[ kCodeIdComplex  ]   );
				uStr.push( this[ kBalanceIdA     ]   );
				uStr.push( this[ kBalanceIdB     ]   );
				uStr.push( this[ kBalanceIdC     ]   );
				uStr.push( this[ kBalanceIdD     ]   );
		return	uStr.join( kMsgSep );
	}

	/** set the validity code of the specified attribute
	 * @param {String} ID transaction attribute name
	 * @param {String} v validity "code" (actually an error message or null)
	 */
	this.setValidity = function(ID,v){ this[ID+MVCScalarModel.kValiditySuffix] = v; }
	
	/** return the validity "code" of the specified attribute
	 * @param {String} ID transaction attribute name
	 * @type String
	 */
	this.getValidity = function(ID){ return this[ID+MVCScalarModel.kValiditySuffix]; }

	/** mark this transaction as deleted */
	this.deleteMe = function(){
		this.preDeleteStatus = this.editStatus;
		this.editStatus      = this.editStatus==2 ? 3 : 1;
	}
	
	/** restore this transaction to its pre-deleted state */
	this.undeleteMe = function(){
		this.editStatus = this.preDeleteStatus;
		delete this.preDeleteStatus;
	}

	/** return whether this transaction needs to update server
	 * @return true if NO UPDATE is needed
	 * @type boolean
	 */
	this.inactive = function(){ return this.editStatus==1 || this.editStatus==3; }
	
	/** mark this transaction as being newly added */
	this.addMe    = function(){ this.editStatus = 2; }
	
	/** return whether this transaction has been changed since last save @type boolean */
	this.inEdit   = function(){ return this.editStatus > 0 && this.editStatus!=3; }

	/** mark this record as having been edited */
	this.editMe = function()
	{
		this.preEdit    = this.clone();//save previous state
		this.editStatus = this.editStatus==2 ? 2 : 4;
		this.whom       = kUserID;
		this.date       = grvGetTodayAsMMDDYYYY();
	}

	/** restore this record to its pre-edited state */
	this.uneditMe = function()
	{
		this[ kCodeIdComplex ] = this.preEdit[ kCodeIdComplex ];
		this[ kCodeIdTrType  ] = this.preEdit[ kCodeIdTrType  ];
		this[ kBalanceIdA    ] = this.preEdit[ kBalanceIdA    ];
		this[ kBalanceIdB    ] = this.preEdit[ kBalanceIdB    ];
		this[ kBalanceIdC    ] = this.preEdit[ kBalanceIdC    ];
		this[ kBalanceIdD    ] = this.preEdit[ kBalanceIdD    ];
		this.date              = this.preEdit.date;
		this.whom              = this.preEdit.whom;
		this.key               = this.preEdit.key;
		this.editStatus        = this.preEdit.editStatus;
		delete this.preEdit;
	}

	/** return the formatted version of our edit state @type String */
	this.state = function()
	{
		switch( this.editStatus )
		{
			case 0: return "Unchanged";
			case 1: return "Deleted";
			case 2: return "Added";
			case 3: return "Limbo";
			case 4: return "Updated";
		}
		return "unknown state:["+this.editStatus+"]";
	}

	/** return our edit state translated to a broadcast message code @type String */
	this.action = function()
	{
		switch( this.editStatus )
		{
			case 1: return "D";
			case 2: return "C";
			case 4: return "U";
		}
		return "";
	}

	/** return whether this transaction can be viewed if desired @type String */
	this.isViewable = function()
	{
		switch( this.editStatus )
		{
			case 0:
			case 2:
			case 4: return true;
		}
		return false;
	}

	/** return whether this transaction is a complex type @type boolean */
	this.isComplex = function(){ return this[kCodeIdTrType]==kComplexKey; }

	/** return the edit/validation rule for the specified balance.
	 * If no balance is specified then return entire set of rules
	 * for the transaction type that "this" is currently set to.
	 * @type String
	 * @param {String} optBalanceID optional object element name specifying desired balance
	 */
	this.getEditRule = function( optBalanceID )
	{
		var decode = kTransTypeMap.getItem( this[kCodeIdTrType] );
		if (decode==null) return null;
		var rules = decode.aux;
		if (grvIsEmpty(rules) || rules.length<4) return null;

		/*********************************************************
		 *** NOTE: At this point, translation of the codes would
		 *** need to take place if the codes used in the MVC lib
		 *** were different than the codes used by the Reconcile
		 *** configuration database but they're the same for now
		 *********************************************************/
 
		//if optional balance ID not specified then return all rules
		if (grvIsEmpty(optBalanceID)) return rules;

		//optional balance ID was specified so return specific rule
		if (optBalanceID==kBalanceIdA) return rules.charAt(0);
		if (optBalanceID==kBalanceIdB) return rules.charAt(1);
		if (optBalanceID==kBalanceIdC) return rules.charAt(2);
		if (optBalanceID==kBalanceIdD) return rules.charAt(3);
		throw "bad balance ID in getEditRule()";
	}

	/** return sum of balances for this transaction @type Float */
	this.getTotal = function()
	{
		var a = parseFloat( this[kBalanceIdA] );
		var b = parseFloat( this[kBalanceIdB] );
		var c = parseFloat( this[kBalanceIdC] );
		var d = parseFloat( this[kBalanceIdD] );
		return a + b + c + d;
	}
	
	/** return whether this transaction has validated data @type boolean */
	this.isValid = function()
	{
		//if I am deleted then I am not invalid.
		if (this.inactive()) return true;

		return this.getValidity( kBalanceIdA    )==null
			&& this.getValidity( kBalanceIdB    )==null
			&& this.getValidity( kBalanceIdC    )==null
			&& this.getValidity( kBalanceIdD    )==null
			&& this.getValidity( kCodeIdTrType  )==null
			&& this.getValidity( kCodeIdComplex )==null;
	}
}

				////////////////////////////////////////////
				////////////// Data Models /////////////////
				////////////////////////////////////////////


Class(AccountSelectionModel,["Account List Model"]).Extends(MVCSelectionModel);
/**
 * @class This class encapsulates the data model for
 * "which Account is currently selected".
 * @extends MVCSelectionModel
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function AccountSelectionModel()
{
	/** @param {ListModel} acctListModel {@link Account} list
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( acctListModel, optName ){
		 this.souper( acctListModel, optName );
	}

	/** set isReconciled flag of selected Account to given value */
	this.updateSelected = function( isReconciled )
	{
		// we jump thru hoops here to get observers/watchers to notice
		// that the account data has changed but not think that
		// "which account is selected" has changed (which would trigger
		// reloading already-loaded Account data).
		this.getSelection().updateStatus( isReconciled );
		this.updateStamp();
//		this.publish(true);
	}

	/** return how many accounts are unreconciled @type int */
	this.getToDoCount = function()
	{
		var sum = 0;
		this.model.iterate( function(i,f){ if (f.todo) ++sum; } );
		return sum;
	}
}


Class(TransactionSelectedModel,["Account Selection Model","balance list model"])
.Extends(MVCBoolModel);
/**
 * @class This class encapsulates the data model for the flag
 * indicating whether a Transaction is selected.
 * If a new account is selected (thus loading new data)
 * this flag gets reset to false.
 * This model also keeps track of which Transaction is selected.
 * @extends MVCBoolModel
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function TransactionSelectedModel()
{
	/** @param {AccountSelectionModel} accountSelectionModel selection model
	 *  @param {BalanceListModel} sModel balance list model
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( accountSelectionModel, sModel, optName )
	{
		this.souper( optName );

		// init instance variables
		this.subscribe( accountSelectionModel );
		this.transholder = sModel;
		this._unselect();
	}

	/** return whether a transaction is selected @type boolean */
	this.hasSelection = function(){ return this.bal_Index!=null
	                                    && this.tranIndex!=null; }

	/** return the specified transaction @type Transaction
	 * @param {int} si index into balance list for current Account
	 * @param {int} ri index into transaction list for specified balance
	 */
	this.getSelected = function(si,ri){
        if ( !this.hasSelection() ) return null;
		return this.transholder.getTransaction(si,ri);
	}

	/** force the "something is selected" flag to specified value and publish */
	this.forceFlag  = function(f){ this.setFlag(f); this.publish(); }

	/** change to "nothing selected" */
	this._unselect  = function(){ this._select( null, null ); }

	/** {@link #_unselect} and publish */
	this.unselect   = function(){ this._unselect(); this.publish(); }

	/** handle update events from the Account Selection Model */
	this.update     = function(){ /*if (this.model.hasChanged)*/ this.unselect(); }

	/** select the specified transaction
	 * @param {int} si index into balance list for current Account
	 * @param {int} ri index into transaction list for specified balance
	 */
	this._select    = function(si,ri){ this.bal_Index = si;
									   this.tranIndex = ri;
									   this.setFlag( si!=null && ri!=null );
								     }

	/** select (and publish) the specified transaction
	 * @param {int} si index into balance list for current Account
	 * @param {int} ri index into transaction list for specified balance
	 */
	this.select     = function(si,ri){ this._select(si,ri); this.publish(); }

	/** return whether specified transaction is selected @type boolean
	 * @param {int} si index into balance list for current Account
	 * @param {int} ri index into transaction list for specified balance
	 */
	this.isSelected = function(si,ri){ return this.bal_Index==si && this.tranIndex==ri; }

	/** Make the specified transaction selected, unless it is
	 * currently selected then merely deselect it.
	 * @param {int} si index into balance list for current Account
	 * @param {int} ri index into transaction list for specified balance
	 */
	this.toggle     = function(si,ri){
		if ( this.isSelected(si,ri) )
			 this.unselect();
	    else this.select(si,ri);
	}

	/** delete selected transaction */
	this.deleteSelected = function() {
		this.transholder.deleteTransaction( this.bal_Index, this.tranIndex );
		this.unselect();
	}

	/** undelete specified transaction
	 * @param {int} si index into balance list for current Account
	 * @param {int} ri index into transaction list for specified balance
	 */
	this.undelete = function( si, ri ) {
		 this.transholder.undeleteTransaction( si, ri );
	}

	/** edit selected transaction */
	this.editSelected = function() {
		this.transholder.editTransaction( this.bal_Index, this.tranIndex );
	}

	/** return whether selected transaction is in edit mode @type boolean
	 */
	this.inEdit = function()
	{
		var selected = this.getSelected( this.bal_Index, this.tranIndex );
		return selected==null ? false : selected.inEdit();
	}

	/** unedit selected transaction */
	this.uneditSelected = function() {
		this.transholder.uneditTransaction( this.bal_Index, this.tranIndex );
	}

	/** insert new transaction after selected transaction and select it */
	this.newAfterSelected = function() {
		this.select( this.bal_Index,
		 this.transholder.createTransaction( this.bal_Index, this.tranIndex )
		);
	}

	/** insert new transaction at end of specified balance list and select it
	 * @param {int} balanceIndex index into balance list for current Account
	 */
	this.appendTransaction = function( balanceIndex )
	{
		this.select( balanceIndex,
		 this.transholder.appendTransaction( balanceIndex )
		);
	}
}


Class(BalanceListModel,["Account Selection Model"]).Extends(MVCListModel);
/**
 * @class DOMAIN-OBJECT: This class encapsulates the data model
 * for the {@link Balance} list of a particular Account.
 * This model subscribes to a {@link AccountSelectionModel}
 * so that a new balance list can be downloaded whenever
 * a new account is selected.
 * Secondarily, this model implements the {@link MVCBoolModel#isTrue}
 * interface where the value reflects whether this model is "dirty".
 * @extends MVCListModel
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function BalanceListModel()
{
	/** @param {AccountSelectionModel} accountSelectionModel model of which account is selected */
	this.konstructor = function( accountSelectionModel )
	{
		this.souper();

		// init instance variables
		this.subscribe( accountSelectionModel );
	}

	/** {@link MVCBoolModel} API: return "I am modified" flag @type boolean */
	this.isTrue = function(){ return this.isDirty(); }

	/** return whether any transactions are changed for the whole Account @type boolean */
	this.isDirty = function(){
		var changed = false;
		var self    = this;
		this.iterate( function(i,s){ if (self.isBalanceDirty(i)) return changed = true; } );
		return changed;
	}

	/** handle update events from the Account Selection Model */
	this.update = function()
	{
		// clear out old data now so that user doesnt keep seeing
		// old data while new data is being requested and loaded
		this.reset();
		
		// TODO make gMVCUndoCmds watch AccountSelectionModel
		gMVCUndoCmds.reset();//must reset here because cmds in queue
						  //can refer to balances that dont exist
						  //at this point because of this.reset()

		// request balance/transaction data from server (after small delay)
		var acct = this.model.getSelection();
		_requestAccountLoad( acct.bank, acct.acct );
	}

	/** save all of the edits into the database via service layer */
	this.save = function()
	{
		var acct   = this.model.getSelection(); //current Account
	 	var posts = new Array();
			posts.push( "USER_ID=" + kUserID );

							//for every edited transaction in every balance,
							// generate update request
							this.iterate( function(i,s){
								s.list.iterate( function(j,r){
									if (r.inEdit())
			posts.push( "UPDATES=" + r.asUpdateString(s) );
								} );
							} );

		// immediate invocation of AJAX request with asynchronous reply handling
		_requestAccountSave( acct.bank, acct.acct, posts.join('&') );
	}

	/** return opaque key of our Account */ 
	this.getAccountKey = function(){ return this.model.getValue(); }

	///// methods to manipulate transaction lists embedded within this model /////

	/** return a new blank transaction @type Transaction */
	this.newTransaction = function()
	{
	    var r = new Transaction( "0", "0", 0,0,0,0, grvGetTodayAsMMDDYYYY(), kUserID, null );
	    r.addMe();
	    return r;
	}

	/** return the specified transaction
	 * @type Transaction
	 * @param {int} sIndex index into "this" balance list
	 * @param {int} rIndex index into transaction list for specified balance
	 */
	this.getTransaction = function( sIndex, rIndex )
	{
	    return this.getItem(sIndex).getTransaction(rIndex);
	}

	/** return the specified transaction with the intent to change its attributes
	 * @type Transaction
	 * @param {int} sIndex index into "this" balance list
	 * @param {int} rIndex index into transaction list for specified balance
	 */
	this.openTransaction = function( sIndex, rIndex )
	{
	    this.publish();
	    return this.getTransaction( sIndex, rIndex );
	}

	/** delete the specified transaction
	 * @param {int} sIndex index into "this" balance list
	 * @param {int} rIndex index into transaction list for specified balance
	 */
	this.deleteTransaction = function( sIndex, rIndex ){
		this.openTransaction(sIndex,rIndex).deleteMe();
	}

	/** undelete the specified transaction
	 * @param {int} sIndex index into "this" balance list
	 * @param {int} rIndex index into transaction list for specified balance
	 */
	this.undeleteTransaction = function( sIndex, rIndex ){
		this.openTransaction(sIndex,rIndex).undeleteMe();
	}

	/** insert a new blank transaction associated with the specified balance
	 * @return index of new transaction
	 * @type int
	 * @param {int} sIndex index into "this" balance list
	 * @param {int} rIndex index into transaction list for specified balance
	 */
	this.createTransaction = function( sIndex, rIndex )
	{
	    this.getItem(sIndex).list.addBefore( rIndex, this.newTransaction() );
	    return rIndex;
	}

	/** append a new blank transaction associated with the specified balance
	 * @return index of new transaction
	 * @type int
	 * @param {int} sIndex index into "this" balance list
	 */
	this.appendTransaction = function( sIndex )
	{
	    var rList = this.getItem(sIndex).list;
	    rList.addItem( this.newTransaction() );
	    return rList.getCount()-1;
	}

	/** enable editing on the specified transaction
	 * @param {int} sIndex index into "this" balance list
	 * @param {int} rIndex index into transaction list for specified balance
	 */
	this.editTransaction = function( sIndex, rIndex ){
		this.openTransaction(sIndex,rIndex).editMe();
	}

	/** rollback edit on the specified transaction
	 * @param {int} sIndex index into "this" balance list
	 * @param {int} rIndex index into transaction list for specified balance
	 */
	this.uneditTransaction = function( sIndex, rIndex ){
		this.openTransaction(sIndex,rIndex).uneditMe();
	}

	/** return whether the specified balance is being reconciled @type boolean
	 * @param {int} i index into "this" balance list
	 */
	this.isBalanceDirty = function(i){
		if (i==0) return false; //by definition, first balance is clean
		return this.getItem(i).anyTransactionsInEdit();
	}

	/** return whether the specified balance is expanded @type boolean
	 * @param {int} i index into "this" balance list
	 */
	this.isBalanceExpanded = function(i){
		return this.isBalanceOpen (i)
			|| this.isBalanceDirty(i);
	}

	/** return whether the specified balance can expand/collapse @type boolean
	 * @param {int} i index into "this" balance list
	 */
	this.canBalanceToggle = function(i){
		return i /*first balance cant be toggled*/
			&&  this.isBalanceReconciled(i)
			&& !this.isBalanceDirty     (i);
	}

	/** return whether balance(s) can expand/collapse @type boolean
	 * @param {boolean} doAllFlag if true, check all of the balances
	 * @param {int} si index into "this" balance list
	 */
	this.canToggle = function( doAllFlag, si )
	{
		si -= 0; //force to int
		var start = doAllFlag ? 0               : si;
		var stop  = doAllFlag ? this.getCount() : si+1;
		for (var i=start; i<stop; ++i)
			if (this.canBalanceToggle(i)) return true;
		return false;
	}

	/** return whether the specified balance is collapsed or open
	 * @return true if open
	 * @type boolean
	 * @param {int} i index into "this" balance list
	 */
	this.isBalanceOpen = function(i){
		if (i==0) return false; //by definition, first balance cant expand
		return this.getItem(i).open;
	}

	/** toggle the "open" flag of the specified balance.
	 * @param {boolean} doAllFlag if true, set all of the balances to new value
	 * @param {int} si index into "this" balance list
	 */
	this.toggleOpen = function( doAllFlag, si )
	{
		si -= 0; //convert to int
		var start = doAllFlag ? 0               : si;
		var stop  = doAllFlag ? this.getCount() : si+1;
		var setTo = !this.getItem(si).open;
		for (var i=start; i<stop; ++i){
			var balance = this.getItem(i);
			balance.open = setTo;
		}
	}

	/** return how many transactions are associated with the specified balance
	 * @type int
	 * @param {int} i index into "this" balance list
	 */
	this.getTransactionCount = function(i)
	{
		if (i==0) return false; //by definition, first balance has none
		return this.getItem(i).list.getCount();
	}

	///// methods to get differences between the net of this      /////
	///// balance and the previous i.e. the unreconciled amounts /////

	/** return whether the specified balance is reconciled
	 * and update the balance todo flag as a side effect.
	 * @type boolean
	 * @param {int} i index into "this" balance list
	 */
	this.isBalanceReconciled = function(i)
	{
		var s = this.getItem(i);
		var isReconciled = true;
		//by definition, first balance is always reconciled
		if (i) isReconciled = s.isValid() && this.hasAllZeroDiff(i);
		s.todo = !isReconciled;
		return isReconciled;
	}

	/** return true iff all unreconciled amounts for specified balance are zero
	 * @type boolean
	 * @param {int} i index into "this" balance list
	 */
	this.hasAllZeroDiff = function(i)
	{
	    if (i<1) throw "illegal index in getDiffTotal() call";
	    return grvIsZeroDollars( this.getDiffA(i) )
			&& grvIsZeroDollars( this.getDiffB (i) )
			&& grvIsZeroDollars( this.getDiffC (i) )
			&& grvIsZeroDollars( this.getDiffD(i) );
	}

	/** return total of unreconciled amount for specified balance
	 * @type Float
	 * @param {int} i index into "this" balance list
	 */
	this.getDiffTotal = function(i)
	{
	    if (i<1) throw "illegal index in getDiffTotal() call";
	    var prev = this.getItem(i-1).getTotal();
	    var curr = this.getItem(i  ).getNetTotal();
	    return curr - prev;
	}

	/** return unreconciled balance A for specified balance
	 * @type Float
	 * @param {int} i index into "this" balance list
	 */
	this.getDiffA = function(i)
	{
	    if (i<1) throw "illegal index in getDiffA() call";
	    var prev = this.getItem(i-1)[kBalanceIdA];
	    var curr = this.getItem(i  ).getNetA();
	    return curr - prev;
	}

	/** return unreconciled balance B for specified balance
	 * @type Float
	 * @param {int} i index into "this" balance list
	 */
	this.getDiffB = function(i)
	{
	    if (i<1) throw "illegal index in getDiffB() call";
	    var prev = this.getItem(i-1)[kBalanceIdB];
	    var curr = this.getItem(i  ).getNetB();
	    return curr - prev;
	}

	/** return unreconciled balance C for specified balance
	 * @type Float
	 * @param {int} i index into "this" balance list
	 */
	this.getDiffC = function(i)
	{
	    if (i<1) throw "illegal index in getDiffC() call";
	    var prev = this.getItem(i-1)[kBalanceIdC];
	    var curr = this.getItem(i  ).getNetC();
	    return curr - prev;
	}

	/** return unreconciled balance D for specified balance
	 * @type Float
	 * @param {int} i index into "this" balance list
	 */
	this.getDiffD = function(i)
	{
	    if (i<1) throw "illegal index in getDiffD() call";
	    var prev = this.getItem(i-1)[kBalanceIdD];
	    var curr = this.getItem(i  ).getNetD();
	    return curr - prev;
	}

	///// methods to get aggregate unreconciled amounts of whole list /////

	/** is the whole Account (i.e. this whole balance list) reconciled? @type boolean */
	this.isAccountReconciled = function()
	{
		//the grand total can be zero but we are still unreconciled, its
		//just that the unreconciled balance amts cancel each other out!
		//return grvIsZeroDollars( this.getNetTotal() );
		var N = this.getCount();
		for (var i=0; i<N; ++i)
		  if (!this.isBalanceReconciled(i)) return false;
		return true;
	}

	/** return unreconciled balance A for the whole Account @type Float */
	this.getNetA = function(){
		var subtotal = 0, N = this.getCount();
		for (var i=1; i<N; ++i) subtotal += this.getDiffA(i);
		return subtotal;
	}

	/** return unreconciled balance B for the whole Account @type Float */
	this.getNetB = function(){
		var subtotal = 0, N = this.getCount();
		for (var i=1; i<N; ++i) subtotal += this.getDiffB(i);
		return subtotal;
	}

	/** return unreconciled balance C for the whole Account @type Float */
	this.getNetC = function(){
		var subtotal = 0, N = this.getCount();
		for (var i=1; i<N; ++i) subtotal += this.getDiffC(i);
		return subtotal;
	}

	/** return unreconciled balance D for the whole Account @type Float */
	this.getNetD = function(){
		var subtotal = 0, N = this.getCount();
		for (var i=1; i<N; ++i) subtotal += this.getDiffD(i);
		return subtotal;
	}

	/** return total unreconciled balances for the whole Account @type Float */
	this.getNetTotal = function(){
		var a = this.getNetA();
		var b = this.getNetB();
		var c = this.getNetC();
		var d = this.getNetD();
		return a + b + c + d;
	}

	/** return whether all transactions are valid for the whole Account @type Float */
	this.isValid = function(){
		var valid = true;
		this.iterate( function(i,s){ if (!s.isValid()){valid = false; return true;} } );
		return valid;
	}
}

				////////////////////////////////////////////
				////////////// Controllers /////////////////
				////////////////////////////////////////////

Class(EditButtonController,["button image filename", "descriptive text", "event handler","enable model"])
.Extends(MVCImgButtonController);
/**
 * @class This class is an MVCImgButtonController for "edit" buttons
 * which shouldnt be enabled while app is in "read only" mode.
 * @extends MVCImgButtonController
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EditButtonController()
{
	/**
	 * @param {MVCBoolModel} optEnableModel optional "is enabled" data model
	 * @param {String} evtHandlerName name of event handler function
	 * @param {String} optName optional name of this instance
	 * @param {String} imgFN filename of the (enabled) button image
	 * @param {String} desc tooltip description for this button
	 * @param {boolean} optIsEdit editor flag says dont enable if already editing
	 */
	this.konstructor = function ( imgFN, desc, evtHandler, enableModel, optName, optIsEdit ){
		this.souper( imgFN, desc, evtHandler, enableModel, optName );
		this.isEdit = optIsEdit;
	}

	/** Return whether this button should be enabled.
	 * If in readonly mode then never enable.
	 * If a transaction is selected then enable
	 * UNLESS this is the edit button and we are already editing.
	 * @type boolean
	 */
	this.isEnabled = function()
	{
		var enabled = kReadOnly ? false : this.model.isTrue(); //anything selected?
		if (enabled && this.isEdit && gTransactionSelected.inEdit()) enabled = false;
		return enabled;
	}
}

Class(AccountSelectController,["selection model"]).Extends(MVCPopupMenuController);
/**
 * @class This class manages a popup menu controller specifically for
 * the Account Selection List.
 * @extends MVCPopupMenuController
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function AccountSelectController()
{
	/** @param {AccountSelectionModel} selectionModel selection model
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor  =  function( selectionModel, optName ){
	  this.souper( selectionModel, optName );
	}

	this.currentContext = function(){ return new MVCContext( this.model.timestamp ); }

	/** return event handler invocation string specific to this controller @type String */
	this.getEvtHandlerStr = function(){ return "onAccountSelect(event,this.value)"; }
}

				////////////////////////////////////////////
				////////////// Data Views  /////////////////
				////////////////////////////////////////////


Class(DebugView,["A Data Model"]).Extends(MVCView);
/**
 * @class This class acts as quick and dirty debug view that
 * pops up a window showing the current state of the
 * arbitrary data model being watched.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function DebugView()
{
	/** @param {Model} aModel a data model to watch */
	this.konstructor = function( aModel )
	{
		this.souper();

		// init instance variables
		this.watchModel( aModel );
	}

	this.mustRebuild = function(){ return this.model.hasChanged; }

	/** implement this view as an alert box */
	this.buildHTML   = function(){
		grvBreak( "=== "+this.name+" ==="
		  +"\nUpdate Event from Model["+this.model.name+"]...\n"
	      + this.model.hasChanged +this.model );
	}
}


Class(CountersView,["Account Selection Model"]).Extends(MVCView);
/**
 * @class This class produces a view of the current selection
 * and total size of a selection model (ie "I of N").
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function CountersView()
{
	/** @param {SelectionModel} sModel account selection model */
	this.konstructor = function( sModel )
	{
		this.souper();

		// init instance variables
		this.watchModel( sModel );
	}

	this.currentContext = function(){
		return new MVCContext( this.model.getValue() );
	}

	this.buildHTMLstr = function(){
		var x = this.model.getIndex()+1;
		return x+"&nbsp;of&nbsp;"+this.model.getCount();
	}
}


Class(ToDoView,["Account Selection Model"]).Extends(MVCView);
/**
 * @class This class produces a view of how many unreconciled
 * accounts are left.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function ToDoView()
{
	/** @param {AccountSelectionModel} sModel account selection model */
	this.konstructor = function( sModel )
	{
		this.souper();

		// init instance variables
		this.watchModel( sModel );
	}

	this.currentContext = function(){
		return new MVCContext( this.model.getToDoCount() );
	}

	this.buildHTMLstr = function(){
	    var x = this.model.getToDoCount();
		return "<span class='"+(x>0?"td-more":"td-none")
		+"' title='Number of Unreconciled Accounts'>"
		+x+"&nbsp;Accts Unreconciled</span>";
	}
}


Class(ComplexView,["Transaction object","index of parent balance","index of transaction"])
.Extends(MVCView);
/**
 * @class This class produces a view of the complex type of a {@link Transaction}.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function ComplexView()
{
	/** @param {Transaction} transaction transaction we are to observe
	 *  @param {int} balanceIndex index into balance list for current Account
	 *  @param {int} transIndex index into transaction list for specified balance
	 */
	this.konstructor = function( transaction, balanceIndex, transIndex )
	{
		this.souper();

		// init instance variables
		this.si = balanceIndex;
		this.ri = transIndex;
		this.watchModel( transaction );
	}

	/** return the view ID of our status panel @type String */
	this.statID = function(){ return this.getWidgetID() + ".stat"; }

	this.paintHTML = function()
	{
		var row           = this.getWidget(); if (!row) return;
		var r             = this.model;
		var isSelected    = gTransactionSelected.isSelected( this.si, this.ri );
		var isDirty       = r.inEdit();
		var isBeingEdited = isSelected && isDirty;
		var isVisible     = !r.inactive()
						 && r.isComplex()
		                 && (gAccountData.isBalanceExpanded  ( this.si )
		                 || !gAccountData.isBalanceReconciled( this.si ));

		this.setVisible( isVisible );

		var menu = this[ kCodeIdComplex ];//originally set in mvcEmbedDualMenu
		if ( isVisible )
		{
			grvSelectElem( row, isSelected );

			//TODO refactor state display into its own view
			grvSelectElemID( this.statID(), false );

			//Since this global data model is just a temp utility model,
			//shared by all collateral menus, we must manually set it
			//to the current value of the REAL data model (iff this is
			//the ONE AND ONLY currently-being-edited transaction). In
			//other words, this trick of sharing a single gComplexSelected
			//data model is because we only allow one collateral menu
			//to be editable at a time.
			if (isBeingEdited) gComplexSelected._select( r[kCodeIdComplex] );

			//TODO refactor these views to have a visibility model (like ButtonController)
			menu.setVisible( true );
			menu.setEditMode( MVCEditRule.kNonZ, isBeingEdited );
		}
		else menu.setEditMode( MVCEditRule.kEdit, false );
	}

	this.currentContext = function(){ return new MVCContext( this.model ); }

	/** return HTML version of a complex type panel. @type String */
	this.buildHTMLstr = function()
	{
		function buildStat( hookID, si, ri ){
		 	var evtHandlerStr = 'onTransactionClick(event,"'+ si+'","'+ ri +'")';
			return "<td onclick='"+evtHandlerStr+"' id='"+hookID+"'>&nbsp;</td>";
		}
		function buildHead( editorHTML ){
			var cols = kEnableTotalsColumn ? 6 : 5;
			return "<td colspan='"+cols+"'>"+editorHTML+"</td>";
		}
		function buildRow( hookID ){
			return "<tr class='py-row' id='"+hookID+"'>";
		}

	 	var HTML  = new Array();
		HTML.push( buildRow ( this.getWidgetID() ) );
		HTML.push( buildStat( this.statID(), this.si, this.ri ) );
		HTML.push( buildHead( mvcEmbedDualMenu( this, kCodeIdComplex, gComplexSelected, "py-coll" ) ) );
		HTML.push( "</tr>" );
		return HTML.join('');
	}
}


Class(TransactionView,["Transaction object","index of parent balance","index of transaction"])
.Extends(MVCView);
/**
 * @class This class produces a view of a {@link Transaction}.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function TransactionView()
{
	/** @param {Transaction} transaction transaction we are to observe
	 *  @param {int} balanceIndex index into balance list for current Account
	 *  @param {int} transIndex index into transaction list for specified balance
	 */
	this.konstructor = function( transaction, balanceIndex, transIndex )
	{
		this.souper();

		// init instance variables
		this.si = balanceIndex;
		this.ri = transIndex;
		this.watchModel( transaction );
	}

	/** return the view ID of our status panel @type String */
	this.statID = function(){ return this.getWidgetID() + ".stat"; }

	/** set the "can edit" status of the specified balance
	 * @param {boolean} enableEdit if true enable editor else viewer
	 * @param {String} balID object element name specifying desired balance
	 */
	this.updateEditMode = function( enableEdit, balID )
	{
		var mode = this.model.getEditRule( balID );
		var dual = this[ balID ];//originally set in mvcEmbedDollarDualEditor()
		if (mode==MVCEditRule.kCopy) dual.setModels( this.model[kBalanceIdA] );
		dual.setEditMode( mode, enableEdit );
	}

	this.paintHTML = function()
	{
		function buildTip( r ){
			return "Last Edit by "+r.whom+" on "+r.date+" State:"+r.state()+" Key="+r.key;
		}

		var row           = this.getWidget(); if (!row) return;
		var r             = this.model;
		var isSelected    = gTransactionSelected.isSelected( this.si, this.ri );
		var isDirty       = r.inEdit();
		var isBeingEdited = isSelected && isDirty;
		var isVisible     = !r.inactive()
		                 && (gAccountData.isBalanceExpanded  ( this.si )
		                 || !gAccountData.isBalanceReconciled( this.si ));
		this.setVisible( isVisible );

		if ( isVisible )
		{
			this.setSubViewsVisible(true);
			row.title = buildTip( r );
			grvSelectElem( row, isSelected );

			//Since this global data model is just a temp utility model,
			//shared by all transaction-type menus, we must manually set
			//it to the current value of the REAL data model (iff this
			//is the ONE AND ONLY currently-being-edited transaction). In
			//other words, this trick of sharing a single gTrTypeSelected
			//data model is because we only allow one transaction-type
			//menu to be editable at a time.
			if (isBeingEdited) gTrTypeSelected._select( r[kCodeIdTrType] );

            //update editability of trans type menu
            var menu = this[ kCodeIdTrType ];//originally set in mvcEmbedDualMenu
            menu.enableEdit( isBeingEdited );

			//set the edit rule for each balance
			this.updateEditMode( isBeingEdited, kBalanceIdA );
			this.updateEditMode( isBeingEdited, kBalanceIdB );
			this.updateEditMode( isBeingEdited, kBalanceIdC );
			this.updateEditMode( isBeingEdited, kBalanceIdD );	

			//TODO refactor state display into its own subview
			var isInValid = ! r.isValid();
			var state     = grvGetHook( this.statID() );
			grvEditElem( state, isSelected, isDirty, isInValid );
			grvSetElemText( state, grvSubitem( isInValid ) );
			//once a span title is set it cant be deleted and will
			//always obscure the row title that it happily displayed
			//before that span title was set...F. U. I. E.
			//state.title = isInValid ? "This transaction has errors." : row.title;
		}
	}

	this.currentContext = function(){
		return new MVCContext( this.model );
	}

	this.buildHTMLstr = function()
	{
		function buildStat( hookID, si, ri ){
		 	var evtHandlerStr = 'onTransactionClick(event,"'+ si+'","'+ ri +'")';
			return "<td class='py-state' onclick='"+evtHandlerStr+"' id='"+hookID+"'></td>";
		}
        function buildHead( editorHTML ){
                return "<td>"+editorHTML+"</td>";
        }
		function buildData( editorHTML ){
			return "<td>"+editorHTML+"</td>";
		}
		function buildTotl( viewerHTML ){
			return "<td class='py-total'>"+viewerHTML+"</td>";
		}
		function buildRow( hookID ){
			return "<tr class='py-row' id='"+hookID+"'>";
		}

	 	var HTML = new Array();
	 	HTML.push( buildRow ( this.getWidgetID()              ) );
		HTML.push( buildStat( this.statID(), this.si, this.ri ) );
		HTML.push( buildHead( mvcEmbedDualMenu( this, kCodeIdTrType, gTrTypeSelected, "py-src", "onTrTypeSelect" ) ) );
		HTML.push( buildData( mvcEmbedDollarDualEditor( this, kBalanceIdA,'py-data' ) ) );
		HTML.push( buildData( mvcEmbedDollarDualEditor( this, kBalanceIdB,'py-data' ) ) );
		HTML.push( buildData( mvcEmbedDollarDualEditor( this, kBalanceIdC,'py-data' ) ) );
		HTML.push( buildData( mvcEmbedDollarDualEditor( this, kBalanceIdD,'py-data' ) ) );
if (kEnableTotalsColumn)
		HTML.push( buildTotl( mvcEmbedAttributeViewer( this, "getTotal", grvFormatDollar ) ) );
		HTML.push( "</tr>" );
		return HTML.join('');
	}
}


Class(UnreconciledView,["Balance List model","index of parent balance"])
.Extends(MVCView);
/**
 * @class This class produces a view of the unreconciled totals
 * of a {@link Balance}.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function UnreconciledView()
{
	/** @param {Balance} sModel balance we are to observe
	 *  @param {int} balanceIndex index into balance list for current Account
	 */
	this.konstructor = function( sModel, balanceIndex )
	{
		this.souper();

		// init instance variables
		this.si = balanceIndex;
		this.watchModel( sModel );
	}

	/** return the view ID of our header panel @type String */
	this.headID    = function(){ return this.getWidgetID() + ".head"; }

	this.paintHTML = function()
	{
		var row          = this.getWidget(); if (!row) return;
		var isReconciled = this.model.isBalanceReconciled( this.si );
		var isExpanded   = this.model.isBalanceExpanded  ( this.si );
		var tranCount    = this.model.getTransactionCount( this.si );
		var isDirty      = this.model.isBalanceDirty     ( this.si );
		var isVisible    = !isReconciled || isDirty || (isExpanded && tranCount==0);
		this.setVisible( isVisible );
		if (isVisible)
		{
			var head = grvGetHook( this.headID() );
			 row.className = isReconciled ? "tr-row"  : "df-row";
			head.className = isReconciled ? "ur-head" : "df-head";
			this.setSubViewsVisible( true );
		}
	}

	this.currentContext = function(){
		return new MVCContext( this.si );
	}

	this.buildHTMLstr = function()
	{
		function buildHead( hookID, sIndex ){
		 	var event = 'onUnreconciledClick(event,"'+ sIndex +'")';
			return "<td>&nbsp;</td><td onclick='"+event+"' id='"+hookID+"'>Amount Unreconciled</td>";
		}
		function buildData( viewer ){
			var cls = "df-data";
			return "<td class='"+cls+"'>"+viewer+"</td>"
		}
		function buildTotl( viewer ){
			return "<td class='df-total'>"+viewer+"</td>";
		}
		function buildRow( hookID, balanceAsString ){
		 	var tip = "Unreconciled Amounts for " + balanceAsString;
			return "<tr title='"+tip+"' id='"+hookID+"'>";
		}

		var i = this.si;
	 	var HTML = new Array();
		HTML.push( buildRow ( this.getWidgetID(), this.model.getItem(i).getTitle() ) );
		HTML.push( buildHead( this.headID(), i ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getDiffA"    , grvFormatDollarNot, i ) ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getDiffB"    , grvFormatDollarNot, i ) ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getDiffC"    , grvFormatDollarNot, i ) ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getDiffD"    , grvFormatDollarNot, i ) ) );
if (kEnableTotalsColumn)
		HTML.push( buildTotl( mvcEmbedAttributeViewer( this, "getDiffTotal", grvFormatDollar   , i ) ) );
		HTML.push( "</tr>" );
		return HTML.join('');
	}
}


Class(TotalsView,["Balance List model"]).Extends(MVCView);
/**
 * @class This class produces a view of the unreconciled totals
 * for an entire {@link Account}.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function TotalsView()
{
	/** @param {BalanceListModel} sModel Account balances we are to observe */
	this.konstructor = function( sModel )
	{
		this.souper();

		// init instance variables
		this.watchModel( sModel );
	}

	/** return the view ID of our header panel @type String */
	this.headID = function(){ return this.getWidgetID() + ".head"; }

	this.paintHTML = function()
	{
		var row  = this.getWidget(); if (!row) return;
		var head = grvGetHook( this.headID() );
		var isReconciled = this.model.isAccountReconciled();

		 row.className = isReconciled ? "tr-row"  : "df-row";
		head.className = isReconciled ? "tr-head" : "tu-head";
	}

	this.currentContext = function(){
		return new MVCContext( this.model.getAccountKey() );
	}

	this.buildHTMLstr = function()
	{
		function buildHead( hookID ){
			return "<td colspan='2' id='"+hookID+"'>Total Unreconciled</td>";
		}
		function buildData( viewer ){
			var cls = "df-data";
			return "<td class='"+cls+"'>"+viewer+"</td>"
		}
		function buildTotl( viewer ){
			return "<td class='df-total'>"+viewer+"</td>";
		}
		function buildRow( hookID, accountAsString ){
		 	var tip  = "Total Unreconciled Amounts for " + accountAsString;
			return "<tr title='"+tip+"' id='"+hookID+"'>";
		}

	 	var HTML = new Array();
		HTML.push( buildRow ( this.getWidgetID(), this.model.model.getSelectionStr() ) );
		HTML.push( buildHead( this.headID() ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getNetA"    , grvFormatDollar ) ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getNetB"    , grvFormatDollar ) ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getNetC"    , grvFormatDollar ) ) );
		HTML.push( buildData( mvcEmbedAttributeViewer( this, "getNetD"    , grvFormatDollar ) ) );
if (kEnableTotalsColumn)
		HTML.push( buildTotl( mvcEmbedAttributeViewer( this, "getNetTotal", grvFormatDollar ) ) );
		HTML.push( "</tr>" );
		return HTML.join('');
	}
}


Class(BalanceView,["Balance object","index of balance"]).Extends(MVCView);
/**
 * @class This class produces a view of a {@link Balance}.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function BalanceView()
{
	/** @param {Balance} balance balance we are to observe
	 *  @param {int} balanceIndex index into balance list for current Account
	 */
	this.konstructor = function( balance, balanceIndex )
	{
		this.souper();

		// init instance variables
		this.si = balanceIndex;
		this.watchModel( balance );
	}

	this.paintHTML = function()
	{
	  var active = !kReadOnly && this.si && gAccountData.isBalanceReconciled(this.si);
	  var row = this.getWidget();
	  row.style.cursor = active ? "pointer" : "default";
	}

	this.currentContext = function(){
		return new MVCContext( this.model.list.getCount() );
	}

	this.buildHTMLstr = function()
	{
		function buildHead(title){
			return "<td class='sn-head' colspan='2'>"+title+"</td>";
		}
		function buildData(x){
			return "<td class='sn-data'>"+grvFormatDollar(x)+"</td>";
		}
		function buildTotl(x){
			return "<td class='sn-total'>"+grvFormatDollar(x)+"</td>";
		}
		function buildRow(i,s,hookID){
		 	var cls  = "";
		 	var event= 'onBalanceClick(event,"'+ i +'")';
		 	var tip  = "From:" + kSystemsMap.getItem( s.whom ).desc
		 	         + "...Click to Toggle; Ctrl-Click to toggle all";
			return "<tr class='sn-row' onclick='"+event+"' title='"+tip+"' id='"+hookID+"'>";
		}

		var s = this.model;
		var i = this.si;
		var h = this.getWidgetID();

	 	var HTML = new Array();
		HTML.push( buildRow ( i, s, h        ) );
		HTML.push( buildHead( s.getTitle()   ) );
		HTML.push( buildData( s[kBalanceIdA] ) );
		HTML.push( buildData( s[kBalanceIdB] ) );
		HTML.push( buildData( s[kBalanceIdC] ) );
		HTML.push( buildData( s[kBalanceIdD] ) );
if (kEnableTotalsColumn)
		HTML.push( buildTotl( s.getTotal()   ) );
		HTML.push( "</tr>" );
		return HTML.join('');
	}
}


Class(TransactionListView,["Transaction List model","index of parent balance"])
.Extends(MVCListView);
/**
 * @class This class produces a view of a Transaction List.
 * @extends MVCListView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function TransactionListView()
{
	/** @param {MVCListModel} transListModel transaction list we are to observe
	 *  @param {int} balanceIndex index into balance list for current Account
	 */
	this.konstructor = function( transListModel, balanceIndex  )
	{
		this.souper();

		// init instance variables
		this.si = balanceIndex;
		this.watchModel( transListModel );
	}

	this.currentContext = function(){
		return new MVCContext( this.model.getCount() );
	}

	this.itemHTMLstr = function( index, item, itemID )
	{
		var HTML = new Array();

	 	HTML.push( this.embedHTML( itemID,
	 			new TransactionView(item,this.si,index) ) );

		HTML.push( this.embedHTML( itemID+".coll",
	 			new     ComplexView(item,this.si,index) ) );

		return HTML.join('');
	}
}


Class(BalanceListView,["Balance List model"]).Extends(MVCListView);
/**
 * @class This class produces a view of a BALANCE List.
 * @extends MVCListView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function BalanceListView()
{
	/** @param {BalanceListModel} sModel balance list we are to observe */
	this.konstructor = function( sModel )
	{
		this.souper();

		// init instance variables
		this.watchModel( sModel );
	}

	this.currentContext = function(){
		return new MVCContext( this.model.timestamp );
	}

	this.itemHTMLstr = function( index, item, itemID )
	{
		var HTML = new Array();

	  if (index!=0) //there is nothing to reconcile for 1st balance!
	  {
	 	HTML.push( this.embedHTML( itemID+".py",
	 			new TransactionListView(item.list,index) ) );

		HTML.push( this.embedHTML( itemID+".unrec",
	 			new UnreconciledView(this.model,index) ) );
	  }
		HTML.push( this.embedHTML( itemID,
	 			new BalanceView(item,index) ) );

		return HTML.join('');
	}
}


Class(DataPanelView,["Balance List model"]).Extends(MVCView);
/**
 * @class This class produces the view of the entire data panel.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function DataPanelView()
{
	/** @param {BalanceListModel} ssModel Account balances we are to observe */
	this.konstructor = function( ssModel )
	{
		this.souper();

		// init instance variables
		this.watchModel( ssModel );
	}

	this.currentContext = function(){
		return new MVCContext( this.model.timestamp );
	}

	this.buildHTMLstr = function()
    {
    	if (this.model.getCount()<1) return "";

		var vwID = this.getWidgetID();
		var HTML = new Array(30);
		HTML.push( '<table class="data-panel">' );
		HTML.push( '<thead><tr>' );
		HTML.push( '<th colspan="2" class="table-head" id="headTitle">Transaction Type</th>' );
		HTML.push( '<th class="table-head" id="headA">Balance A</th>' );
		HTML.push( '<th class="table-head" id="headB">Balance B</th>' );
		HTML.push( '<th class="table-head" id="headC">Balance C</th>' );
		HTML.push( '<th class="table-head" id="headD">Balance D</th>' );
if (kEnableTotalsColumn)
		HTML.push( '<th class="table-head" id="headTotal"><i>Row Total</i></th>' );
		HTML.push( '</tr></thead>' );
		HTML.push( '<tbody>'
				+ this.embedHTML( vwID+".sn", new BalanceListView( this.model ) )
				+ '</tbody>' );
		HTML.push( '<tr><td colspan="7">'+mvcSECTIONBREAK()+'</td></tr>' );
		HTML.push( '<tfoot>'
				+ this.embedHTML( vwID+".totalunrec", new TotalsView( this.model ) )
				+ '</tfoot>' );
		HTML.push( '</table>' );
		return HTML.join('');
	}
}

				////////////////////////////////////////////
				//////////////// Commands //////////////////
				////////////////////////////////////////////

// ----------------------------------------------------------------------------
// Utility routines for making AJAX requests
// ----------------------------------------------------------------------------
/** Make a REST-style AJAX request to the RATS server.</br>
 *  NOTE: this is a Command helper function.</br>
 *  NEVER call this from outside the context of a Command object!
 * @param {String} aRequestName "controller" name
 * @param {Function} aReplyEventHandler reply event handler static function
 *                   (called as a method of grvXMLrec and passed grvXMLrec as param)
 * @param {String} aBANK   Account bank number
 * @param {String} anACCT  Account number
 * @param {String} optPostData optional string to pass as POST data
 * @param {boolean} optWaitFlag optional flag that if true says
 *                  call synchronously (i.e. wait for reply)
 */
function _RATSRequest( aRequestName, aReplyEventHandler, aBANK,
						anACCT, optPostData, optWaitFlag )
{
	grvWAIT();
	var url;
	var parms = new Array();
	if (_kGrvAjaxTestData)
	{
		parms.push( aBANK   );
		parms.push( anACCT  );
		url = kXMLPath + "RATS" + parms.join("_") + ".xml";
	}
	else
	{
		parms.push(              aRequestName         );
		parms.push(    'user=' + grvGetArg("user")    );
		parms.push( 'testing=' + grvGetArg("testing") );
		parms.push(    'BANK=' + aBANK                );
		parms.push(    'ACCT=' + anACCT               );
		url = location.pathname + "?action=" + parms.join("&");
	}

	return new grvXMLreq( url, aReplyEventHandler, optPostData, optWaitFlag );
}

/** Make a Load-Account-Data request to the server.<br>
 *  NOTE: this is a Command helper function.<br>
 *  NEVER call this from outside the context of a Command object!
 * @param {String} aBANK   Account bank number
 * @param {String} anACCT  Account number
 */
function _requestAccountLoad( aBANK, anACCT ){
	_RATSRequest( "GetXMLAccountData", onAccountLoadReply, aBANK, anACCT );
}

/** Make a Save-Account-Data request to the server.<br>
 *  NOTE: this is a Command helper function.<br>
 *  NEVER call this from outside the context of a Command object!
 * @param {String} aBANK   Account bank number
 * @param {String} anACCT  Account number
 * @param {String} updates list of transaction update commands
 */
function _requestAccountSave( aBANK, anACCT, updates ){
	_RATSRequest( "UpdateAccountData", onAccountSaveReply, aBANK, anACCT, updates );
}

/** Select the first unreconciled Account<br>
 *  NOTE: this is a Command helper function.<br>
 *  NEVER call this from outside the context of a Command object!
 * @param {boolean} selectFirst if FALSE then leave selection alone if none unreconciled
 * otherwise selected the first Account if none unreconciled.
 * @return true iff new selection was made
 * @type boolean
 */
function _selectFirstUnreconciled( selectFirst )
{
	// Select 1st unreconciled account but
	// if all are reconciled then select the 1st account if requested
	// otherwise leave the selection where it is.
	var N = gAccountList.getCount();
	for (var i=0; i<N; ++i)
	  if (gAccountList.getItem(i).todo) break;

	if ( i>=N && selectFirst ) i = 0;
	if ( i>=N ) return false;
	return gAccountSelected.setValue( i );
}

//////////////////////////////////////////////////////////////////////


Class(LoadAccountDataCmd,["xml request object","auto-select-unreconciled flag"])
.Extends(MVCCommand);
/**
 * @class This class implements the "load account data" command.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function LoadAccountDataCmd()
{
	/** @param {grvXMLreq} xmlReq the XML request being replied to
	 *  @param {boolean} autoFlag if true then auto-advance to next
	 *                   unreconciled Account if current data is reconciled
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( xmlReq, autoFlag, optName )
	{
		// init static member variables
		if (!LoadAccountDataCmd.XSL)
		     LoadAccountDataCmd.XSL = grvLoadXslDOM( kXSLPath+"RATSxmlToJS.xsl" );

		this.souper( optName );
		this.canUndo = false;
		this.auto    = autoFlag;

		//translate received XML into javascript (via XSL) and save
		try { this.script = xmlReq.xform2( LoadAccountDataCmd.XSL ); }
		catch(e) { this.state = -1; }
	}

	this.doit = function()
	{
		grvTraceEvt( this.script );
		       eval( this.script );

		// updated reconcile status notification
		//grvDebugWindow( gAccountData.dump() );//look at the updated data

		// HACK: this normally should have been accomplished via
		// subscriptions, but it would have set up a circular dependency.
		gAccountSelected.updateSelected( gAccountData.isAccountReconciled() );
		
		// automatically select next unreconciled (if requested)
		// (which would mean we must wait on data from server)
		var  stillWaiting = true;
		if (this.auto)
		     stillWaiting = _selectFirstUnreconciled( false );
		else stillWaiting = false;
		if (!stillWaiting) grvUNWAIT();
	}
}


Class(SelectFirstUnreconciledCmd).Extends(MVCCommand);
/**
 * @class This class implements the "select first unreconciled account" command.
 * NOTE: It isnt undoable (because we dont need it to be).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function SelectFirstUnreconciledCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ){
		this.souper( optName );
		this.canUndo = false;
	}

	this.doit = function(){ _selectFirstUnreconciled(true); }
}


Class(SelectAccountCmd,["selected Account Key"]).Extends(MVCCommand);
/**
 * @class This class implements the "select account" command.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function SelectAccountCmd()
{
	/** @param {Object} selectedAccountKey opaque key of Account
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( selectedAccountKey, optName )
	{
		this.souper( optName, (gAccountData.isTrue()
				   ? "You must Save or Cancel data changes first."
				   : null ) );
		this.acctKey  = selectedAccountKey;
		this.canUndo = false;
	}

	this.doit = function(){ gAccountSelected.select( this.acctKey ); }
}


Class(CloseWindowIfDataSavedCmd).Extends(MVCCommand);
/**
 * @class This class implements the "close this window if all
 * data is saved" command.
 * NOTE: It isnt undoable (because we may close this window!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function CloseWindowIfDataSavedCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName )
	{
		this.souper( optName, (gAccountData.isDirty() /*.isTrue()*/
				   ? "You must Save or Cancel data changes before leaving."
				   : null )  );
		this.canUndo = false;
	}

	this.doit = function(){ window.close(); }
}


Class(CancelAllAccountChangesCmd).Extends(MVCCommand);
/**
 * @class This class implements the "revert this account" command.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function CancelAllAccountChangesCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ){
		this.souper( optName );
		this.canUndo = false;
		if (!confirm("Are you sure you want to cancel all changes to this Account?"))
		  this.state = -1;
	}
							//causes reload of original data
	this.doit = function(){	gAccountSelected.reselect(); }
}


Class(SaveAllAccountChangesCmd).Extends(MVCCommand);
/**
 * @class This class implements the "save changes" command.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function SaveAllAccountChangesCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ) {
		this.souper( optName, (gAccountData.isValid() ? null
			: "Can't save while any transactions are invalid." )  );
		this.canUndo = false;
	}

	this.doit = function() {
		gTransactionSelected.unselect();
		gAccountData.save();//which implies a reload of data
	}
}


Class(TrTypeMenuCmd,["view ID"]).Extends(MVCScalarEditCmd);
/**
 * @class This class implements the trans type menu edit command.
 * @extends MVCScalarEditCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function TrTypeMenuCmd()
{
	/** @param {String} viewID viewID of menu generating this event */
	this.konstructor = function( viewID ){
		this.souper( viewID );
		this.si             = this.dual.parentView.si;
		this.transaction    = this.dual.model.base;
		this.oldTransaction = this.transaction.clone();
	}

	/** copy values that can change when transaction type is changed
	 * @param {Transaction} exemplar where to copy attributes from
	 */
	this.updateTransaction = function( exemplar ){
		this.transaction[ kCodeIdComplex ] = exemplar[ kCodeIdComplex ];
		this.transaction[ kBalanceIdA    ] = exemplar[ kBalanceIdA    ];
		this.transaction[ kBalanceIdB    ] = exemplar[ kBalanceIdB    ];
		this.transaction[ kBalanceIdC    ] = exemplar[ kBalanceIdC    ];
		this.transaction[ kBalanceIdD    ] = exemplar[ kBalanceIdD    ];
	}

	/** prefill balance values when transaction type is changed
	 * taking into account that the existing values in this transaction
	 * have to be applied back to the unreconciled balances before
	 * prefilling again.
	 */
	this.prefillTransaction = function(){
		var i = this.si;
		this.transaction[ kBalanceIdA ] = 0;
		this.transaction[ kBalanceIdB ] = 0;
		this.transaction[ kBalanceIdC ] = 0;
		this.transaction[ kBalanceIdD ] = 0;
		this.transaction[ kBalanceIdA ] = gAccountData.getDiffA(i);
		this.transaction[ kBalanceIdB ] = gAccountData.getDiffB (i);
		this.transaction[ kBalanceIdC ] = gAccountData.getDiffC (i);
		this.transaction[ kBalanceIdD ] = gAccountData.getDiffD(i);
	}

	this.doit = function(){
		this.updateController( this.newValue );
		this.prefillTransaction();
	}
	this.undo = function(){
		this.newTransaction = this.transaction.clone();
		this.updateTransaction( this.oldTransaction );
		this.updateController( this.oldValue );
	}
	this.redo = function(){
		this.updateTransaction( this.newTransaction );
		this.updateController( this.newValue );
	}
}


Class(RatsCmd).Extends(MVCCommand);
/**
 * @class This is the abstract base class for RATS commands.
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function RatsCmd()
{
	/** @param {String} cmdDesc general description of this command */
	this.konstructor = function( cmdDesc ){
		this.souper( cmdDesc );
		//save current transaction selection
		this.cmdSI = this.oldSI = gTransactionSelected.bal_Index;
		this.cmdRI = this.oldRI = gTransactionSelected.tranIndex;
	}

	/** return balance description @type String */
	this.balString = function(){ 
		return gAccountData.getItem( this.cmdSI ).sdate;
	}

	/** return transaction description @type String */
	this.tranString = function(){
		this.cmdRI -= 0; //type cast to int
		return "["+(this.cmdRI+1)+"] of " + this.balString();
	}

	/** return details portion of command description @type String */
	this.details = function(){ return this.tranString(); }
}


Class(ToggleBalCmd,["toggle all flag","balance index"]).Extends(RatsCmd);
/**
 * @class This class implements the toggle balance display cmd.
 * @extends RatsCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function ToggleBalCmd()
{
	/** @param {boolean} toggleAll if true then toggle all balance views
	 *  @param {int} bal_Index index into current account's balance list
	 */
	this.konstructor = function( toggleAll, bal_Index )
	{
	  this.souper("toggle balance display");
	  if ( gAccountData.canToggle( toggleAll, bal_Index ) )
	  {
		   this.all   = toggleAll;
		   this.cmdSI = bal_Index;
	  }
	  else this.state = -1;
	}

	this.doit    = function(){ gAccountData.toggleOpen( this.all, this.cmdSI ); }
	this.undo    = function(){ this.doit(); }
	this.details = function(){ return this.all ? "ALL" : this.balString(); }
}


Class(ToggleTranCmd,["balance index","trans index"]).Extends(RatsCmd);
/**
 * @class This class implements the toggle transaction selection cmd.
 * @extends RatsCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function ToggleTranCmd()
{
	/** @param {int} tranIndex index into specified balance's transaction list
	 *  @param {int} bal_Index index into current account's balance list
	 */
	this.konstructor = function( bal_Index, tranIndex ){
		this.souper("toggle transaction selection");
		this.cmdRI = tranIndex;
		this.cmdSI = bal_Index;
	}

	this.doit = function(){ gTransactionSelected.toggle( this.cmdSI, this.cmdRI ); }
	this.undo = function(){ gTransactionSelected.toggle( this.oldSI, this.oldRI ); }
}


kAppendTransaction = true;
kInsertTransaction = false;
Class(CreateTranCmd,["append flag"]).Extends(RatsCmd);
/**
 * @class This class implements the append and insert transaction cmds.
 * @extends RatsCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function CreateTranCmd()
{
	/** @param {boolean} appendFlag if true then append else insert transaction
	 *  @param {int} optBalIndex optional index of balance to insert after
	 */
	this.konstructor = function( appendFlag, optBalIndex )
	{
		this.souper((appendFlag?"append":"insert")+" transaction");
		//TODO refactor balance&unreconciled panels to be enabled/disabled
		//     Controllers instead of putting "read-only" logic here
		if ( ! kReadOnly )
		{
			 this.append = appendFlag;
			 this.cmdSI  = optBalIndex ? optBalIndex : gTransactionSelected.bal_Index;
		}
		else this.state  = -1;
	}

	this.doit = function()
	{
		//create new transaction
		if (this.append)
			 gTransactionSelected.appendTransaction( this.cmdSI );
		else gTransactionSelected.newAfterSelected();
		//save new state
		this.newSI = gTransactionSelected.bal_Index;
		this.newRI = gTransactionSelected.tranIndex;
	}

	this.undo = function(){
		gTransactionSelected.deleteSelected();
		gTransactionSelected.select( this.oldSI, this.oldRI );
	}

	this.redo = function(){
		gTransactionSelected.undelete( this.newSI, this.newRI );
		gTransactionSelected.select  ( this.newSI, this.newRI );
	}

	this.details = function(){ return this.balString(); }
}


Class(DeleteTranCmd).Extends(RatsCmd);
/**
 * @class This class implements the delete transaction cmd.
 * @extends RatsCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function DeleteTranCmd()
{
	this.konstructor = function(){
		this.souper("delete transaction");
	}
	this.doit = function(){
		gTransactionSelected.deleteSelected();//leaves nothing selected
	}
	this.undo = function(){
		gTransactionSelected.undelete( this.oldSI, this.oldRI );
		gTransactionSelected.select  ( this.oldSI, this.oldRI );
	}
}


Class(EditTranCmd).Extends(RatsCmd);
/**
 * @class This class implements the edit transaction cmd.
 * @extends RatsCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EditTranCmd()
{
	this.konstructor = function(){
		this.souper("edit transaction");
	}
	this.doit = function(){ gTransactionSelected.  editSelected(); }
	this.undo = function(){ gTransactionSelected.uneditSelected(); }
}

				////////////////////////////////////////////
				////////// Static Event Handlers ///////////
				////////////////////////////////////////////

/** handle event for 'delete button pressed' */
function onDeleteBtnPressed(){
	mvcDoCmd( new DeleteTranCmd() );
}

/** handle event for 'insert button pressed' */
function onInsertBtnPressed()
{
// dont insert until fix is found for insert goofing up the transaction
// indexes lodged in all those command objects!!!
//	mvcDoCmd( new CreateTranCmd( kInsertTransaction ) );
	mvcDoCmd( new CreateTranCmd( kAppendTransaction ) );
}

/** handle event for 'edit button pressed' */
function onEditBtnPressed(){
	mvcDoCmd( new EditTranCmd() );
}

/** handle event for 'click on a transaction record'
 * @param {event} e browser event
 * @param {int} balanceIndex index into balance list (aka account data)
 * @param {int} transIndex index into transaction list (of balance)
 */
function onTransactionClick( e, balanceIndex, transIndex ){
	mvcDoCmd( new ToggleTranCmd( balanceIndex, transIndex ) );
}

/** handle event for 'click on a balance record'
 * @param {event} e browser event
 * @param {int} balanceIndex index into balance list (aka account data)
 */
function onBalanceClick( e, balanceIndex ){
	var ev = e || window.event;
	var cmdKey = (typeof ev.metaKey == "undefined") ? false : ev.metaKey;
	mvcDoCmd( new ToggleBalCmd( ev.ctrlKey || cmdKey, balanceIndex  ) );
}

/** handle event for 'click on an unreconciled panel'
 * @param {event} e browser event
 * @param {int} balanceIndex index into balance list (aka account data)
 */
function onUnreconciledClick( e, balanceIndex ){
	mvcDoCmd( new CreateTranCmd( kAppendTransaction, balanceIndex ) );
}

/** handle event for 'user selects from the transaction type menu'
 * @param {event} e browser event
 * @param {String} viewID ID of the controller generating this event
 */
function onTrTypeSelect( e, viewID ){
	mvcDoCmd( new TrTypeMenuCmd( viewID ) );
	return true;
}

/** handle event for 'user selects from the account menu'
 * @param {event} e browser event
 * @param {int} selectedAccountKey the new menu-selection
 */
function onAccountSelect( e, selectedAccountKey ){
	mvcDoCmd( new SelectAccountCmd( selectedAccountKey ) );
}

/** handle event for 'entering web page' */
function onLoad()
{
	grvTraceEvt("rats.js: in onload");
	gMVCRootView.enable();

// HACK! windows get goofed up if we dont suppress
// the "please wait" window on launch...
_gGrvHackStartupInhibitWait = true;

	mvcDoCmd( new SelectFirstUnreconciledCmd() );

//_gGrvHackStartupInhibitWait = false;//modern browsers are a pain with popups now..retire this technique
	grvTraceEvt("rats.js: leaving onload");
}

/** handle event for 'save button pressed' */
function onSaveBtnPressed(){
	mvcDoCmd( new SaveAllAccountChangesCmd() );
}

/** handle event for 'cancel button pressed' */
function onCancelBtnPressed(){
	mvcDoCmd( new CancelAllAccountChangesCmd() );
}

/** handle event for 'exit button pressed' */
function onExitBtnPressed(){
	mvcDoCmd( new CloseWindowIfDataSavedCmd() );
}

/** handle event for 'about to leave web page';
 * warn if attempting to leave with unsaved changes
 */
function onBeforeUnLoad()
{	//This cant be done in a Command since this logic must be completed
	//by the time that we return. [A Command may be queued and finished
	//later, and that is out of the callers control.]
	if ( gAccountData.isTrue() ){  // i.e. unsaved data
		return "Edits are NOT saved. Are you sure you want to abandon your changes?";
	}
}

/** handle AJAX event for 'reply received from "load account data" server-request'
 * @param {grvXMLreq} xmlReq the XML request being replied to
 */
function onAccountLoadReply( xmlReq ){
	mvcDoCmd( new LoadAccountDataCmd( xmlReq, false ) );
}

/** handle AJAX event for 'reply received from "save account data" server-request'
 * @param {grvXMLreq} xmlReq the XML request being replied to
 */
function onAccountSaveReply( xmlReq )
{	//since we get back official new version of Account Data, just
	//do the same thing as asking for data load in the first place!
	//...except on save we ask that the next unreconciled be autoselected
	mvcDoCmd( new LoadAccountDataCmd( xmlReq, true ) );
}

grvTraceCmp("rats.js: End");

The Gravey Framework and RATS RIA

Documentation generated by JSDoc on Thu Jan 6 12:46:18 2011