<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -1,5 +1,7 @@
 === (new)
 
+* Recall previously entered transactions for easy entry of similar events [Jamis Buck]
+
 * Hide bucket list if there are less than 2 buckets for an account [Jamis Buck]
 
 * Increase &quot;recent buckets&quot; window size to 10 (from 5) [Jamis Buck]</diff>
      <filename>CHANGELOG.rdoc</filename>
    </modified>
    <modified>
      <diff>@@ -3,6 +3,7 @@ KNOWN ISSUES
 --------------------------------------------------------------------------
 
 * After adding an event with a new tag, the tag autocompletion won't pick up the new tag until the page is reloaded.
+* After adding an event with a new actor, the actor autocompletion won't pick up the new actor until the page is reloaded.
 * Merging one bucket into another can result in events having two line-items referencing the same bucket.
 
 --------------------------------------------------------------------------
@@ -18,7 +19,6 @@ FEATURES that would be nice to have someday (in no particular order)
 * better 404 and 500 error pages
 * searching
 * reporting
-* transaction templates (&quot;saved&quot; or &quot;memorized&quot; transactions)
 * scheduled transactions (occur automatically at specified intervals)
 * print stylesheet
 * oauth authentication for API</diff>
      <filename>TODO</filename>
    </modified>
    <modified>
      <diff>@@ -7,6 +7,13 @@ class EventsController &lt; ApplicationController
 
   def index
     respond_to do |format|
+      format.js do
+        json = events.to_json(eager_options(:root =&gt; &quot;events&quot;, :include =&gt; { :tagged_items =&gt; { :only =&gt; [:amount, :id], :methods =&gt; :name }, :line_items =&gt; { :only =&gt; [:account_id, :bucket_id, :amount, :role], :methods =&gt; [] }}))
+
+        render :update do |page|
+          page &lt;&lt; &quot;Events.doneLoadingRecalledEvents(#{json})&quot;
+        end
+      end
       format.xml do
         render :xml =&gt; events.to_xml(eager_options(:root =&gt; &quot;events&quot;))
       end
@@ -73,7 +80,7 @@ class EventsController &lt; ApplicationController
   protected
 
     attr_reader :event, :container, :account, :bucket, :tag, :events
-    helper_method :event
+    helper_method :event, :container, :account, :bucket
 
     def find_event
       @event = Event.find(params[:id])
@@ -114,7 +121,7 @@ class EventsController &lt; ApplicationController
         raise ArgumentError, &quot;unsupported container type #{container.class}&quot;
       end
 
-      more_pages, list = container.send(association).send(method, params[:page], :size =&gt; params[:size])
+      more_pages, list = container.send(association).send(method, params[:page], :size =&gt; params[:size], :actor =&gt; params[:actor])
       unless list.first.is_a?(Event)
         list = list.map do |item| 
           event = item.event</diff>
      <filename>app/controllers/events_controller.rb</filename>
    </modified>
    <modified>
      <diff>@@ -2,9 +2,16 @@ class Actor &lt; ActiveRecord::Base
   belongs_to :subscription
   has_many :events
 
+  validates_presence_of :name, :sort_name
+  attr_accessible :name, :sort_name
+
+  def self.normalize_name(name)
+    name.strip.upcase
+  end
+
   def self.normalize(name)
     name = name.strip
-    sort_name = name.upcase
+    sort_name = normalize_name(name)
 
     actor = find_by_sort_name(sort_name)
     if actor</diff>
      <filename>app/models/actor.rb</filename>
    </modified>
    <modified>
      <diff>@@ -130,6 +130,12 @@ class Event &lt; ActiveRecord::Base
     super(options.merge(:methods =&gt; methods, :except =&gt; except))
   end
 
+  def to_json(options={})
+    methods = Array(options[:methods]).dup
+    methods |= [:balance, :value, :role]
+    super(options.merge(:methods =&gt; methods))
+  end
+
   protected
 
     def line_item_validations</diff>
      <filename>app/models/event.rb</filename>
    </modified>
    <modified>
      <diff>@@ -12,8 +12,20 @@ class Subscription &lt; ActiveRecord::Base
       size = (options[:size] || DEFAULT_PAGE_SIZE).to_i
       n = n.to_i
 
-      records = find(:all, :include =&gt; :account_items,
-        :order =&gt; &quot;created_at DESC&quot;,
+      joins = []
+      conditions = []
+      parameters = []
+
+      if options[:actor]
+        joins &lt;&lt; &quot;LEFT JOIN actors ON actors.id = events.actor_id&quot;
+        conditions &lt;&lt; &quot;actors.sort_name = ?&quot;
+        parameters &lt;&lt; Actor.normalize_name(options[:actor])
+      end
+
+      records = find(:all, :joins =&gt; joins,
+        :conditions =&gt; conditions.any? ? [conditions.join(&quot; AND &quot;), *parameters] : nil,
+        :include =&gt; :account_items,
+        :order =&gt; &quot;events.created_at DESC&quot;,
         :limit =&gt; size + 1,
         :offset =&gt; n * size)
 </diff>
      <filename>app/models/subscription.rb</filename>
    </modified>
    <modified>
      <diff>@@ -9,6 +9,8 @@ class TaggedItem &lt; ActiveRecord::Base
 
   attr_accessible :tag, :tag_id, :amount
 
+  delegate :name, :to =&gt; :tag
+
   def tag_id=(value)
     case value
     when Fixnum, /^\s*\d+\s*$/ then super(value)</diff>
      <filename>app/models/tagged_item.rb</filename>
    </modified>
    <modified>
      <diff>@@ -8,18 +8,21 @@
     = form.calendar_date_select :occurred_on, :size =&gt; 10
 
   %p
-    %span.expense_label &lt;strong&gt;How much&lt;/strong&gt; was paid?
-    %span.deposit_label &lt;strong&gt;How much&lt;/strong&gt; was deposited?
-    %span.transfer_label &lt;strong&gt;How much&lt;/strong&gt; was transferred?
-    == $#{text_field_tag :amount, event_amount_value, :size =&gt; 8, :id =&gt; &quot;expense_total&quot;, :class =&gt; &quot;number&quot;, :onchange =&gt; &quot;Events.updateUnassigned()&quot;}
-
-  %p
     %span.expense_label &lt;strong&gt;Who&lt;/strong&gt; received the payment?
     %span.deposit_label &lt;strong&gt;Where&lt;/strong&gt; did this deposit come from?
     %span.transfer_label &lt;strong&gt;What&lt;/strong&gt; was this transfer for?
     = form.text_field :actor_name, :size =&gt; 30
-    #event_actor_name_select.autocomplete_select{:style =&gt; &quot;display: none&quot;}
-    = javascript_tag &quot;Events.autocompleteActorField()&quot;
+    - if form.object.new_record?
+      %span#recall_event{:style =&gt; &quot;display: none&quot;}= link_to_function &quot;(recall)&quot;, &quot;Events.recallEvent(#{subscription_events_path(subscription).to_json})&quot;
+
+  #event_actor_name_select.autocomplete_select{:style =&gt; &quot;display: none&quot;}
+  = javascript_tag &quot;Events.autocompleteActorField()&quot;
+
+  %p
+    %span.expense_label &lt;strong&gt;How much&lt;/strong&gt; was paid?
+    %span.deposit_label &lt;strong&gt;How much&lt;/strong&gt; was deposited?
+    %span.transfer_label &lt;strong&gt;How much&lt;/strong&gt; was transferred?
+    == $#{text_field_tag :amount, event_amount_value, :size =&gt; 8, :id =&gt; &quot;expense_total&quot;, :class =&gt; &quot;number&quot;, :onchange =&gt; &quot;Events.updateUnassigned()&quot;}
 
   %p#memo_link{:style =&gt; visible?(!event_wants_memo?)}
     &lt;strong&gt;Got more to say?&lt;/strong&gt;</diff>
      <filename>app/views/events/_form_general.html.haml</filename>
    </modified>
    <modified>
      <diff>@@ -135,6 +135,8 @@ Returns the most recent events to have been added to BucketWise. They will be so
 
 You can also specify the &quot;include&quot; query parameter as a comma-delimited list of any combination of &quot;user&quot;, &quot;line_items&quot;, and &quot;tagged_items&quot;. Specifying &quot;user&quot; will include the user who created the event in the response. &quot;line_items&quot; will nest the line-items for the event in the response. &quot;tagged_items&quot; will nest the tagged items for the event in the response.
 
+Lastly, you can specify the &quot;actor&quot; query parameter to return only transactions where the given actor is involved.
+
 === GET /accounts/1/events.xml
 
 Returns a single page of events associated with the given account, ordered by the date they were said to have occurred. As above, it accepts both &quot;page&quot; and &quot;size&quot; query parameters to control which page, and how many results are returned.</diff>
      <filename>doc/API.rdoc</filename>
    </modified>
    <modified>
      <diff>@@ -142,13 +142,19 @@ var Events = {
     var li = document.createElement(&quot;li&quot;);
     li.innerHTML = $('template.' + section).innerHTML;
     ol.appendChild(li);
+    var input = li.down(&quot;input&quot;);
     if(populate) {
       var acctSelect = $('account_for_' + section);
       var acctId = $F('account_for_' + section);
-      Events.populateBucket(li.down(&quot;select&quot;), acctId,
+      var bucketSelect = li.down(&quot;select&quot;);
+      Events.populateBucket(bucketSelect, acctId,
         {'skipAside':(section=='credit_options')});
+      if(populate != true) {
+        bucketSelect.setValue(populate.bucket_id);
+        input.setValue(Money.formatValue(Math.abs(populate.amount)));
+      }
     }
-    li.down(&quot;input&quot;).focus();
+    input.focus();
   },
 
   removeLineItem: function(li) {
@@ -156,7 +162,7 @@ var Events = {
     Events.updateUnassigned();
   },
 
-  addTaggedItem: function() {
+  addTaggedItem: function(item) {
     var ol = $('tagged_items');
     var li = document.createElement(&quot;li&quot;);
     var content = $('template.tags').innerHTML;
@@ -166,6 +172,11 @@ var Events = {
 
     Events.autocompleteTagField(id);
     li.down(&quot;input&quot;).focus();
+
+    if(item) {
+      li.down(&quot;input.number&quot;).setValue(Money.formatValue(item.amount));
+      li.down(&quot;input.tag&quot;).setValue(item.name);
+    }
   },
 
   autocompleteTagField: function(id, options) {
@@ -195,6 +206,17 @@ var Events = {
 
     new Autocompleter.Local('event_actor_name', &quot;event_actor_name_select&quot;,
       Events.actors, options);
+
+    var element = $('event_actor_name');
+
+    element.observe('keyup', function() {
+      Events.recalledEvents = null;
+      if(element.present()) {
+        $('recall_event').show();
+      } else {
+        $('recall_event').hide();
+      }
+    });
   },
 
   removeTaggedItem: function(li) {
@@ -536,14 +558,18 @@ var Events = {
     $('tags').down(&quot;input&quot;).focus();
   },
 
-  revealPartialTags: function() {
-    Events.addTaggedItem();
-    Events.addTaggedItem();
+  revealPartialTags: function(bare) {
+    if(!bare) {
+      Events.addTaggedItem();
+      Events.addTaggedItem();
+    }
 
     $('tag_items_collapsed').hide();
     $('tag_items').show();
 
-    $('tag_items').down('input').focus();
+    if(!bare) {
+      $('tag_items').down('input').focus();
+    }
   },
 
   reset: function() {
@@ -568,6 +594,8 @@ var Events = {
     $('tagged_items').innerHTML = &quot;&quot;;
     $('tags').hide();
     $('tags_collapsed').show();
+
+    $('recall_event').hide();
   },
 
   cancel: function() {
@@ -647,5 +675,145 @@ var Events = {
 
   returnToCaller: function() {
     window.location.href = Events.return_to;
+  },
+
+  recallEvent: function(url) {
+    if(!Events.recalledEvents) {
+      Events.loadRecalledEvents(url);
+      return;
+    }
+
+    if(Events.recalledEvents.length == 0) {
+      alert(&quot;No transactions matched the criteria you specified.&quot;);
+      return;
+    }
+
+    Events.currentEvent = (Events.currentEvent + 1) % Events.recalledEvents.length;
+    var event = Events.recalledEvents[Events.currentEvent].event;
+
+    Events.rehydrate(event);
+  },
+
+  loadRecalledEvents: function(url) {
+    parameters = 'page=0&amp;size=10&amp;actor=' + encodeURIComponent($F('event_actor_name'));
+    new Ajax.Request(url, {
+      asynchronous:true,
+      evalScripts:true,
+      method:'get',
+      parameters:parameters
+    });
+  },
+
+  doneLoadingRecalledEvents: function(events) {
+    Events.recalledEvents = events;
+    Events.currentEvent = -1;
+    Events.recallEvent();
+  },
+
+  rehydrate: function(event) {
+    var saved_date = $F('event_occurred_on');
+    var saved_actor = $F('event_actor_name');
+
+    Events.reset();
+
+    $('event_occurred_on').value = saved_date;
+    $('event_actor_name').value = saved_actor;
+    $('expense_total').value = Money.formatValue(event.value);
+    $('event_memo').value = event.memo;
+    if($('event_memo').present()) Events.revealMemo();
+
+    $('recall_event').show();
+
+    switch(event.role) {
+      case &quot;expense&quot;: 
+        Events.rehydrateExpenseEvent(event);
+        break;
+      case &quot;deposit&quot;:
+        Events.rehydrateDepositEvent(event);
+        break;
+      case &quot;transfer&quot;:
+        Events.rehydrateTransferEvent(event);
+        break;
+      case &quot;reallocation&quot;:
+        alert(&quot;Not yet implemented: can't rehydrate bucket reallocations&quot;);
+        return;
+      default:
+        alert(&quot;Can't rehydrate '&quot; + event.role + &quot;' events&quot;);
+        return;
+    }
+
+    Events.rehydrateTagsForEvent(event);
+  },
+
+  rehydrateSection: function(section, event) {
+    var items = event.line_items.select(function(item) { return item.role == section; });
+    if(items.length == 0) return;
+
+    $(section).show();
+
+    var account = Events.accounts[items[0].account_id];
+    $('account_for_' + section).setValue(account.id);
+    if(account.role == &quot;checking&quot; &amp;&amp; Events.sectionWantsCheckOptions(section)) {
+      $(section + '.check_options').show();
+      $('event_check_number').setValue(event.check_number);
+    }
+
+    Events.updateBucketsFor(section);
+
+    if(items.length == 1) {
+      var select = $(section + '.single_bucket').down('select');
+      Events.selectBucket(select, items[0].bucket_id);
+    } else {
+      $(section + '.multiple_buckets').show();
+      $(section + '.single_bucket').hide();
+
+      items.each(function(item) {
+        Events.addLineItemTo(section, item);
+      });
+    }
+  },
+
+  rehydrateExpenseEvent: function(event) {
+    Events.revealExpenseForm();
+    Events.rehydrateSection('payment_source', event);
+    Events.rehydrateSection('credit_options', event);
+  },
+
+  rehydrateDepositEvent: function(event) {
+    Events.revealDepositForm();
+    Events.rehydrateSection('deposit', event);
+  },
+
+  rehydrateTransferEvent: function(event) {
+    Events.revealTransferForm();
+    Events.rehydrateSection('transfer_from', event);
+    Events.rehydrateSection('transfer_to', event);
+  },
+
+  rehydrateTagsForEvent: function(event) {
+    var whole_tags = $A(), partial_tags = $A();
+
+    event.tagged_items.each(function(item) {
+      if(item.amount &lt; event.value) {
+        partial_tags.push(item);
+      } else {
+        whole_tags.push(item);
+      }
+    });
+
+    if(whole_tags.length &gt; 0 || partial_tags.length &gt; 0) {
+      Events.revealTags();
+      if(whole_tags.length &gt; 0) {
+        var tags = whole_tags.map(function(item) { return item.name }).join(&quot;, &quot;)
+        $('tags').down('input').setValue(tags);
+      }
+
+      if(partial_tags.length &gt; 0) {
+        Events.revealPartialTags(true);
+        partial_tags.each(function(item) {
+          Events.addTaggedItem(item);
+        });
+      }
+    }
   }
 }</diff>
      <filename>public/javascripts/events.js</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>093e6208c206a8495683c0bfbfda7413e1c1318f</id>
    </parent>
  </parents>
  <author>
    <name>Jamis Buck</name>
    <email>jamis@37signals.com</email>
  </author>
  <url>http://github.com/jamis/bucketwise/commit/290f3241a822c0b9b67d2847c5021f468537a961</url>
  <id>290f3241a822c0b9b67d2847c5021f468537a961</id>
  <committed-date>2009-05-09T21:46:31-07:00</committed-date>
  <authored-date>2009-05-09T21:46:31-07:00</authored-date>
  <message>&quot;recall&quot; prior events for easy entry of similar events

This is the &quot;memorized&quot; transactions feature. You enter an actor
name, click the &quot;recall&quot; link, and Bucketwise pulls up the last
event you entered that used that actor. Click &quot;recall&quot; again and
it gets the one before that, etc.

Note that, for usability purposes, this swaps the position of
the actor and amount fields. Before, you'd enter the amount and
then the actor. Now, you enter the actor and then the amount
(because you may want to recall the amount based on the actor).</message>
  <tree>bbede6f64597c0ddac091b702fa82fe474ec98ca</tree>
  <committer>
    <name>Jamis Buck</name>
    <email>jamis@37signals.com</email>
  </committer>
</commit>
