Mobile Cash Transaction Entry Tool for Tiller Foundations

This guide walks you through setting up a lightweight, mobile-optimized Web App that saves as a native-feeling shortcut on your Android or iOS home screen. It writes entries directly to Row 2 of your Transactions sheet with native date formatting, proper signs (+/-), and full Tiller manual metadata.

If you regularly spend or receive cash on the road, opening the full Google Sheets app on a phone to log transactions can feel clunky.

I built this tool using Google Apps Script and Gemini in about 5 minutes. It shouldn’t take you more than 5 minutes to set it up yourelf. I am not a coder so if you have any questions, I’d just copy and paste this into Gemini and discuss there. (Note: I used MMM DD, YYYY eg. Sep 13, 2026 date formatting but if you use something different, just ask Gemini to change the code to your preference.)


Features

  • Instant Access: One-tap web icon on Android (Chrome) or iOS (Safari).
  • Smart Placement: Inserts new entries at Row 2 (top of your ledger) and copies surrounding row formatting automatically.
  • Tiller-Compatible Metadata: Generates unique manual transaction IDs (manual:uuid) and tracks Date Added, Week, and Month fields automatically.
  • Layout Independent: Uses dynamic header matching so your sheet won’t break if you move columns around.

Step 1: Open Apps Script

  1. Open your Tiller Foundation Spreadsheet.
  2. In the top menu, go to Extensions > Apps Script.
  3. Rename the project at the top left to Tiller Mobile Entry.

Step 2: Update Code.gs

Delete any default code in the Code.gs file and paste the following script:

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
      .setTitle('Tiller Manual Entry')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1');
}

function processTransaction(data) {
  try {
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName('Transactions');
    
    if (!sheet) return "Error: 'Transactions' sheet not found.";

    var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    var newRow = new Array(headers.length).fill('');

    function getColIdx(headerName) {
      return headers.indexOf(headerName);
    }

    // Parse YYYY-MM-DD input date
    var dateParts = data.date.split('-');
    var year = parseInt(dateParts[0], 10);
    var month = parseInt(dateParts[1], 10) - 1; 
    var day = parseInt(dateParts[2], 10);
    
    var transactionDate = new Date(year, month, day);
    var dateAdded = new Date();
    
    // Calculate Tiller Month (1st of month) and Week (Sunday)
    var monthDate = new Date(year, month, 1);
    var weekDate = new Date(transactionDate);
    weekDate.setDate(weekDate.getDate() - weekDate.getDay()); 

    // Adjust amount sign based on transaction type
    var amount = parseFloat(data.amount);
    amount = (data.type === 'expense') ? -Math.abs(amount) : Math.abs(amount);

    // Metadata configuration (CUSTOMIZE YOUR ACCOUNT NAME BELOW IF NEEDED)
    var accountName = 'YOUR_ACCOUNT_NAME'; // e.g., 'Cash' or 'Dave Cash'
    var accountId = 'manual:' + accountName.toLowerCase().replace(/\s+/g, '-');
    var txnId = 'manual:' + Utilities.getUuid();
    var metadata = '{"manualTransaction":{"reconcile":false}}';

    var mapping = {
      'Date': transactionDate,
      'Date Added': dateAdded,
      'Account': accountName,
      'Description': data.description,
      'Amount': amount,
      'Note': data.note || '',
      'Month': monthDate,
      'Week': weekDate,
      'Transaction ID': txnId,
      'Account ID': accountId,
      'Metadata': metadata
    };

    for (var key in mapping) {
      var idx = getColIdx(key);
      if (idx !== -1) {
        newRow[idx] = mapping[key];
      }
    }

    // Insert at Row 2 (directly below headers)
    sheet.insertRowBefore(2);
    var targetRange = sheet.getRange(2, 1, 1, newRow.length);
    targetRange.setValues([newRow]);

    // Copy formatting from Row 3 to keep dates and currency looking uniform
    if (sheet.getLastRow() >= 3) {
      var sourceFormatRange = sheet.getRange(3, 1, 1, headers.length);
      sourceFormatRange.copyTo(targetRange, SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
    }

    return "Success";

  } catch (e) {
    return "Error: " + e.toString();
  }
}

Step 3: Create Index.html

  1. In Apps Script, click the “+” (Add a file) icon next to Files and select HTML.
  2. Name the file Index (case-sensitive).
  3. Paste the following HTML UI code:
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <style>
    body { font-family: Roboto, sans-serif; padding: 20px; background-color: #f1f3f4; color: #202124; margin: 0; }
    .container { background: white; padding: 25px 20px; border-radius: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); max-width: 100%; box-sizing: border-box;}
    h2 { margin-top: 0; color: #1a73e8; font-size: 22px; text-align: center; }
    .form-group { margin-bottom: 18px; }
    label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: #5f6368; }
    input[type="text"], input[type="number"], input[type="date"], select { 
      width: 100%; padding: 12px; border: 1px solid #dadce0; border-radius: 8px; box-sizing: border-box; font-size: 16px; background-color: #fff;
    }
    input:focus, select:focus { outline: none; border-color: #1a73e8; border-width: 2px; padding: 11px; }
    button { width: 100%; padding: 14px; background-color: #1a73e8; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; margin-top: 10px; }
    button:disabled { background-color: #aecbfa; }
    #status { margin-top: 15px; font-weight: bold; text-align: center; font-size: 14px; }
    .success { color: #1e8e3e; }
    .error { color: #d93025; }
  </style>
</head>
<body>
  <div class="container">
    <h2>Tiller Manual Entry</h2>
    
    <div class="form-group">
      <select id="type">
        <option value="expense">Expense (Withdrawal)</option>
        <option value="income">Income (Deposit)</option>
      </select>
    </div>
    
    <div class="form-group">
      <label for="date">Date</label>
      <input type="date" id="date" required>
    </div>
    
    <div class="form-group">
      <label for="amount">Amount ($)</label>
      <input type="number" id="amount" step="0.01" min="0.01" placeholder="0.00" inputmode="decimal" required>
    </div>
    
    <div class="form-group">
      <label for="description">Description</label>
      <input type="text" id="description" required>
    </div>
    
    <div class="form-group">
      <label for="note">Note (Optional)</label>
      <input type="text" id="note">
    </div>
    
    <button id="submitBtn" onclick="submitForm()">Save Transaction</button>
    <div id="status"></div>
  </div>

  <script>
    document.getElementById('date').valueAsDate = new Date();

    function submitForm() {
      var btn = document.getElementById('submitBtn');
      var status = document.getElementById('status');
      
      var data = {
        type: document.getElementById('type').value,
        date: document.getElementById('date').value,
        description: document.getElementById('description').value,
        amount: document.getElementById('amount').value,
        note: document.getElementById('note').value
      };

      if (!data.date || !data.description || !data.amount) {
        status.textContent = 'Please fill out all required fields.';
        status.className = 'error';
        return;
      }

      btn.disabled = true;
      btn.textContent = 'Saving to Tiller...';
      status.textContent = '';

      google.script.run
        .withSuccessHandler(function(response) {
          btn.disabled = false;
          btn.textContent = 'Save Transaction';
          if (response === 'Success') {
            status.textContent = 'Saved successfully!';
            status.className = 'success';
            
            document.getElementById('description').value = '';
            document.getElementById('amount').value = '';
            document.getElementById('note').value = '';
            document.getElementById('type').value = 'expense';
            document.getElementById('date').valueAsDate = new Date();
            
            setTimeout(function() { status.textContent = ''; }, 3000);
          } else {
            status.textContent = response;
            status.className = 'error';
          }
        })
        .withFailureHandler(function(error) {
          btn.disabled = false;
          btn.textContent = 'Save Transaction';
          status.textContent = 'Error: ' + error.message;
          status.className = 'error';
        })
        .processTransaction(data);
    }
  </script>
</body>
</html>

Step 4: Customize Your Account Name

In Code.gs, locate line 40:

var accountName = 'YOUR_ACCOUNT_NAME'; 

Replace 'YOUR_ACCOUNT_NAME' with the exact name of your cash account as it appears in your Tiller Accounts tab (e.g., 'Cash', 'Dave Cash', or 'Wallet').


Step 5: Deploy & Authorize

  1. Click the blue Deploy button at the top right > New deployment.
  2. Click the gear icon next to “Select type” and choose Web app.
  3. Set Execute as to Me (your email).
  4. Set Who has access to Only myself.
  5. Click Deploy.

Permissions Warning: Google will prompt you to authorize the app. Click Authorize access, log into your account, click Advanced, and then click Go to Tiller Mobile Entry (unsafe) to grant access. This warning appears simply because it is a private script you created.

  1. Copy the Web App URL provided at the end of deployment.

Step 6: Add Shortcut to Phone

Android (Chrome):

  1. Send the Web App URL to your phone and open it in Chrome.
  2. Tap the three dots menu
  3. Select Install and create shortcut to add to your home screen.

iOS (Safari):

  1. Send the Web App URL to your iPhone and open it in Safari.
  2. Tap the Share button (bottom center rectangle with arrow).
  3. Scroll down and tap Add to Home Screen.
2 Likes

Interesting idea. I’ve been doing this manually, by copying and inserting some past cash entry, while copying many of the fields from the next row. This might be a better option.