Google Drive Accounting Doc Router For Israeli Freelancers (Apps Script)

Google Drive Accounting Doc Router For Israeli Freelancers (Apps Script)

As a freelancer or business owner dealing with both Israeli and international clients, managing accounting documents can quickly become a complex task. That's why I developed a specialized Google Apps Script application that automates the organization of invoices, receipts, and expenses in Google Drive.

Github Project Link

The Challenge

Managing accounting documents across different tax jurisdictions presents unique challenges:

  • Separating Israeli and international documents for VAT purposes

  • Maintaining a consistent folder structure across years and months

  • Efficiently handling multiple document uploads

  • Ensuring proper naming conventions for easy retrieval

The Solution

I created a Google Apps Script application that provides an intuitive web interface for routing accounting documents to their appropriate folders in Google Drive. The application automatically:

  • Creates and maintains a structured folder hierarchy by year and month

  • Separates Israeli and non-Israeli documents

  • Handles batch uploads of up to 10 documents

  • Allows custom file renaming for better organization

Technical Implementation

The application is built using Google Apps Script, which provides seamless integration with Google Drive. It consists of two main components:

  1. Frontend (index.html)

    • Clean, user-friendly interface

    • File upload functionality

    • Document type selection

    • Israeli/non-Israeli toggle

    • File renaming capabilities

  2. Backend (Code.gs)

    • Folder structure management

    • File routing logic

    • Google Drive API integration

    • Error handling and validation

Key Features

routing

1. Intelligent Folder Structure

The application maintains a deterministic folder structure:

2. Batch Upload Processing

Users can upload up to 10 documents simultaneously, streamlining the process of organizing multiple files.

3. Smart Document Routing

The application intelligently routes documents based on:

  • Document type (invoices, receipts, expenses)

  • Israeli/non-Israeli classification

  • Current date (for automatic month/year folder selection)

4. Custom File Naming

Users can rename files before routing to make them more descriptive and easier to find later.

Deployment Process

Deploying the application is straightforward:

  1. Create a new Google Apps Script project

  2. Add the Code.gs and index.html files

  3. Configure deployment settings as a web app

  4. Set appropriate access permissions

  5. Deploy and obtain the public URL

Benefits

file routing

This application offers several key advantages:

  1. Time Savings: Automates the tedious process of manually organizing documents

  2. Consistency: Ensures a uniform folder structure across all document types

  3. Compliance: Separates Israeli and international documents for easier tax reporting

  4. Scalability: Handles growing document volumes with ease

  5. User-Friendly: Intuitive interface requires minimal training

Open Source and Available for All

The project is licensed under CC-BY-4.0, allowing anyone to:

  • Use the application for their own document management needs

  • Modify and adapt the code for specific requirements

  • Share and distribute the solution with others

Code (V1)

Create a new project in Google Apps Script.

Save the first of these code blocks as Code.js (note: capitalisation) and the second as index.html (note: lower case ‘i’). The images are served from Cloudinary so you don't need to worry about hosting them locally.

Make sure to choose the appropriate permission for your deployment and select type as web app.

Code.js

javascript
1// Month names mapping
2const MONTHS = [
3  '01_Jan', '02_Feb', '03_Mar', '04_Apr', '05_May', '06_Jun',
4  '07_Jul', '08_Aug', '09_Sep', '10_Oct', '11_Nov', '12_Dec'
5];
6
7function doGet() {
8  return HtmlService.createHtmlOutputFromFile('index')
9    .setTitle('Accounting File Router')
10    .setFaviconUrl('https://www.google.com/images/drive/drive-48.png');
11}
12
13/**
14 * Process uploaded files and route them to the appropriate folder
15 * @param {Object[]} files - Array of file blobs
16 * @param {string} folderType - Type of folder (EXPENSE, INVOICES, RECEIPTS)
17 * @returns {Object} Processing result
18 */
19/**
20 * Create folder structure for selected folder types
21 * @param {string[]} folderTypes - Array of folder types to create structure for
22 * @returns {Object} Creation result
23 */
24function createFolderStructure(folderTypes) {
25  try {
26    const config = JSON.parse(PropertiesService.getUserProperties().getProperty('accountingRouterConfig') || '{}');
27    const now = new Date();
28    const year = now.getFullYear().toString();
29    const month = MONTHS[now.getMonth()];
30    let created = 0;
31
32    folderTypes.forEach(folderType => {
33      const folderId = config[folderType];
34      if (!folderId) {
35        throw new Error(`Folder ID not configured for ${folderType}`);
36      }
37
38      // Get or create the folder structure
39      const rootFolder = DriveApp.getFolderById(folderId);
40      const yearFolder = findOrCreateFolder(rootFolder, year);
41      const monthFolder = findOrCreateFolder(yearFolder, month);
42      findOrCreateFolder(monthFolder, 'Israeli'); // Create Israeli subfolder
43      created++;
44    });
45
46    return {
47      success: true,
48      message: `Successfully created folder structure for ${created} folder type(s)`
49    };
50  } catch (error) {
51    return {
52      success: false,
53      message: `Error creating folder structure: ${error.toString()}`
54    };
55  }
56}
57
58function processFiles(files, folderType, monthOverride) {
59  try {
60    // Get folder ID from the client-side configuration
61    const config = JSON.parse(PropertiesService.getUserProperties().getProperty('accountingRouterConfig') || '{}');
62    const folderId = config[folderType];
63
64    if (!folderId) {
65      throw new Error('Folder ID not configured for ' + folderType);
66    }
67
68    // Get the target date (either from override or current)
69    let year, month;
70    if (monthOverride) {
71      const [targetYear, targetMonth] = monthOverride.split('-');
72      year = targetYear;
73      month = MONTHS[parseInt(targetMonth) - 1];
74    } else {
75      const now = new Date();
76      year = now.getFullYear().toString();
77      month = MONTHS[now.getMonth()];
78    }
79
80    // Get the root folder based on type
81    const rootFolder = DriveApp.getFolderById(folderId);
82
83    // Find or create year folder
84    let yearFolder = findOrCreateFolder(rootFolder, year);
85
86    // Find or create month folder
87    let monthFolder = findOrCreateFolder(yearFolder, month);
88
89    // Process each file
90    const results = [];
91    files.forEach(file => {
92      let targetFolder = monthFolder;
93
94      // If file is marked as Israeli, create/get Israeli subfolder
95      if (file.isIsraeli) {
96        targetFolder = findOrCreateFolder(monthFolder, 'Israeli');
97      }
98
99      const blob = Utilities.newBlob(
100        Utilities.base64Decode(file.bytes),
101        file.mimeType,
102        file.fileName
103      );
104      const newFile = targetFolder.createFile(blob);
105      results.push({
106        name: file.fileName,
107        success: true,
108        url: newFile.getUrl()
109      });
110    });
111
112    return {
113      success: true,
114      message: `Successfully processed ${files.length} file(s)`,
115      results: results
116    };
117  } catch (error) {
118    return {
119      success: false,
120      message: `Error processing files: ${error.toString()}`,
121      results: []
122    };
123  }
124}
125
126/**
127 * Get saved configuration from user properties
128 * @returns {Object} Configuration object with folder IDs
129 */
130function getConfiguration() {
131  try {
132    const config = PropertiesService.getUserProperties().getProperty('accountingRouterConfig');
133    return config ? JSON.parse(config) : null;
134  } catch (error) {
135    console.error('Failed to get configuration:', error);
136    return null;
137  }
138}
139
140/**
141 * Find a folder by name or create it if it doesn't exist
142 * @param {GoogleAppsScript.Drive.Folder} parentFolder - Parent folder
143 * @param {string} folderName - Name of folder to find/create
144 * @returns {GoogleAppsScript.Drive.Folder} The found or created folder
145 */
146
147/**
148 * Save configuration to user properties
149 * @param {Object} config - Configuration object with folder IDs
150 * @returns {Object} Save result
151 */
152function saveConfiguration(config) {
153  try {
154    PropertiesService.getUserProperties().setProperty('accountingRouterConfig', JSON.stringify(config));
155    return { success: true };
156  } catch (error) {
157    throw new Error('Failed to save configuration: ' + error.toString());
158  }
159}
160
161function findOrCreateFolder(parentFolder, folderName) {
162  const folders = parentFolder.getFoldersByName(folderName);
163  if (folders.hasNext()) {
164    return folders.next();
165  }
166  return parentFolder.createFolder(folderName);
167}

Index.html (but save with lower case ‘i’)!

html
1<!DOCTYPE html>
2<html>
3<head>
4  <base target="_top">
5  <meta charset="UTF-8">
6  <title>Google Drive Accounting File Router</title>
7  <style>
8    @keyframes slideUp {
9      from {
10        transform: translateY(100%);
11        opacity: 0;
12      }
13      to {
14        transform: translateY(0);
15        opacity: 1;
16      }
17    }
18
19    @keyframes slideDown {
20      from {
21        transform: translateY(0);
22        opacity: 1;
23      }
24      to {
25        transform: translateY(100%);
26        opacity: 0;
27      }
28    }
29
30    .israeli-animation {
31      animation: slideUp 0.5s ease forwards;
32    }
33
34    .israeli-animation-exit {
35      animation: slideDown 0.5s ease forwards;
36    }
37
38    /* Tab Styles */
39    .tabs {
40      display: flex;
41      margin-bottom: 20px;
42      border-bottom: 2px solid #ddd;
43    }
44    .tab {
45      padding: 10px 20px;
46      cursor: pointer;
47      border: none;
48      background: none;
49      font-weight: bold;
50      color: #666;
51    }
52    .tab.active {
53      color: #4285f4;
54      border-bottom: 2px solid #4285f4;
55      margin-bottom: -2px;
56    }
57    .tab-content {
58      display: none;
59    }
60    .tab-content.active {
61      display: block;
62    }
63    /* Config Styles */
64    .config-group {
65      margin-bottom: 15px;
66    }
67    .config-group input {
68      width: 100%;
69      padding: 8px;
70      border: 1px solid #ddd;
71      border-radius: 4px;
72    }
73    .help-text {
74      display: block;
75      margin-top: 4px;
76      color: #666;
77      font-size: 0.9em;
78    }
79    body {
80      font-family: Arial, sans-serif;
81      max-width: 800px;
82      margin: 20px auto;
83      padding: 20px;
84      background-color: #f5f5f5;
85    }
86    .container {
87      background-color: white;
88      padding: 20px;
89      border-radius: 8px;
90      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
91    }
92    .form-group {
93      margin-bottom: 20px;
94    }
95    label {
96      display: block;
97      margin-bottom: 8px;
98      font-weight: bold;
99    }
100    select {
101      width: 100%;
102      padding: 8px;
103      border: 1px solid #ddd;
104      border-radius: 4px;
105      margin-bottom: 16px;
106    }
107    .file-list {
108      border: 2px dashed #ddd;
109      border-radius: 4px;
110      padding: 10px;
111      margin-bottom: 16px;
112      min-height: 150px;
113      max-height: 300px;
114      overflow-y: auto;
115      transition: all 0.3s ease;
116      background-color: #fff;
117      position: relative;
118    }
119    .file-list.drag-over {
120      border-color: #4285f4;
121      background-color: #f8f9fa;
122    }
123    .drop-message {
124      position: absolute;
125      top: 50%;
126      left: 50%;
127      transform: translate(-50%, -50%);
128      color: #666;
129      font-size: 1.1em;
130      text-align: center;
131      pointer-events: none;
132    }
133    .file-item {
134      display: flex;
135      justify-content: space-between;
136      align-items: center;
137      padding: 8px;
138      border-bottom: 1px solid #eee;
139    }
140    .file-item:last-child {
141      border-bottom: none;
142    }
143    .button-group {
144      display: flex;
145      gap: 10px;
146      margin-top: 20px;
147    }
148    button {
149      padding: 10px 20px;
150      border: none;
151      border-radius: 4px;
152      cursor: pointer;
153      font-weight: bold;
154    }
155    .upload-btn {
156      background-color: #4285f4;
157      color: white;
158    }
159    .clear-btn {
160      background-color: #dc3545;
161      color: white;
162    }
163    .status {
164      margin-top: 20px;
165      padding: 10px;
166      border-radius: 4px;
167      display: none;
168    }
169    .success {
170      background-color: #d4edda;
171      color: #155724;
172      border: 1px solid #c3e6cb;
173    }
174    .error {
175      background-color: #f8d7da;
176      color: #721c24;
177      border: 1px solid #f5c6cb;
178    }
179    .loading {
180      display: none;
181      text-align: center;
182      margin: 20px 0;
183    }
184    .checkbox-label {
185      display: inline-flex;
186      align-items: center;
187      margin: 0;
188      gap: 5px;
189      font-weight: normal;
190    }
191    .file-rename {
192      width: 100%;
193      padding: 4px;
194      margin-bottom: 4px;
195      border: 1px solid #ddd;
196      border-radius: 4px;
197    }
198    .file-original {
199      color: #666;
200      font-size: 0.8em;
201      margin-bottom: 4px;
202    }
203  </style>
204</head>
205<body>
206  <div class="container">
207    <h1>Google Drive Accounting File Router</h1>
208    <h2 style="color: #666; margin: 0 0 15px 0;">Routes accounting documents in Google Drive</h2>
209    <div id="currentMonth" style="color: #666; margin-bottom: 15px; display: flex; align-items: center; gap: 10px;">
210      <span></span>
211      <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAALCAYAAAB24g05AAAAxUlEQVQoU2P8//8/AyUAIwUGwA2orKxk/P//P8PmzZvRvQszYAEQnwBiZSB+D8SbgPgMEP8F4t1AvBiIQWwUgGKAGxCvBeI/QCwAxIeB+BUQGwAxyIA9QCwOxMgGIGvGMOA/EDsC8WMg/gTEpkD8GIjPArEFEIMM2QTESkCMzQCQpBUQPwBiEH8BEL8BYgMgBhmyE4jFgBjDAJBkHhDfAmKQQisg/gDEukAMMmQDECsCMU4DQAoSgfg6EIMM+QvEq4AYJM6AAgAp8kAx/Db8YAAAAABJRU5ErkJggg==" alt="Israeli Flag" style="width: 24px; height: 24px;">
212    </div>
213
214    <div class="tabs">
215      <button class="tab active" onclick="showTab('upload')">Upload Files</button>
216      <button class="tab" onclick="showTab('config')">Configuration</button>
217      <button class="tab" onclick="showTab('howto')">How To Use</button>
218      <button class="tab" onclick="showTab('create')">Create Folders</button>
219    </div>
220
221    <!-- Upload Tab -->
222    <div id="uploadTab" class="tab-content active">
223      <div class="form-group">
224        <label for="folderType">Select Document Type:</label>
225        <select id="folderType">
226          <option value="">Please select...</option>
227          <option value="EXPENSE">Expense</option>
228          <option value="INVOICES">Invoice</option>
229          <option value="RECEIPTS">Receipt</option>
230        </select>
231
232        <label for="monthOverride">Target Month:</label>
233        <select id="monthOverride">
234          <option value="">Current Month</option>
235        </select>
236      </div>
237
238      <div class="form-group">
239        <label>Select Files (up to 10):</label>
240        <input type="file" id="files" multiple accept="application/pdf,image/*,.doc,.docx,.xls,.xlsx" style="display: none;">
241        <button onclick="document.getElementById('files').click()" class="upload-btn">Choose Files</button>
242      </div>
243
244      <div class="file-list" id="fileList" ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)">
245        <div class="drop-message">
246          Drag and drop files here<br>
247          <small>or use the Choose Files button</small>
248        </div>
249        <div id="fileListContent"></div>
250      </div>
251
252      <div class="button-group">
253        <button onclick="processFiles()" class="upload-btn">Upload Files</button>
254        <button onclick="clearAll()" class="clear-btn">Clear All</button>
255      </div>
256
257      <div class="loading" id="loading">
258        Processing files... Please wait...
259      </div>
260
261      <div id="statusImage" style="text-align: center; margin: 20px 0;">
262        <img src="/images/hashnode-imports/google-drive-accounting-doc-router-for-israeli-freelancers-apps-script-otidlvt8pofbzcpknbc6.png" alt="Status" style="max-width: 200px;">
263      </div>
264
265      <div class="status" id="status"></div>
266    </div>
267
268    <!-- Config Tab -->
269    <div id="configTab" class="tab-content">
270      <h2>Folder Configuration</h2>
271      <p>Configure the Google Drive folders for each document type. You can paste the full folder URL or just the folder ID.</p>
272
273      <div class="config-group">
274        <label for="expenseFolder">Expense Folder:</label>
275        <input type="text" id="expenseFolder" placeholder="Paste folder URL or ID" onchange="handleFolderInput(this)">
276        <small class="help-text">Example: https://drive.google.com/drive/folders/YOUR_FOLDER_ID</small>
277      </div>
278
279      <div class="config-group">
280        <label for="invoicesFolder">Invoices Folder:</label>
281        <input type="text" id="invoicesFolder" placeholder="Paste folder URL or ID" onchange="handleFolderInput(this)">
282        <small class="help-text">Example: https://drive.google.com/drive/folders/YOUR_FOLDER_ID</small>
283      </div>
284
285      <div class="config-group">
286        <label for="receiptsFolder">Receipts Folder:</label>
287        <input type="text" id="receiptsFolder" placeholder="Paste folder URL or ID" onchange="handleFolderInput(this)">
288        <small class="help-text">Example: https://drive.google.com/drive/folders/YOUR_FOLDER_ID</small>
289      </div>
290
291      <button onclick="saveConfig()" class="upload-btn">Save Configuration</button>
292      <div class="status" id="configStatus"></div>
293    </div>
294
295    <!-- How To Use Tab -->
296    <div id="howtoTab" class="tab-content">
297      <h2>How To Use</h2>
298      <div style="line-height: 1.6">
299        <p style="margin-bottom: 20px;">This app was developed by Daniel Rosehill (danielrosehill.com) using Claude Sonnet 3.5 & Cline.</p>
300
301        <h3>Setup</h3>
302        <ol>
303          <li>Go to the Configuration tab and set up your folder IDs for each document type.</li>
304          <li>Use the Create Folders tab to create the initial folder structure if needed.</li>
305        </ol>
306
307        <h3>Uploading Files</h3>
308        <ol>
309          <li>Select the document type (Expense, Invoice, or Receipt).</li>
310          <li>Choose files using the button or drag and drop them into the designated area.</li>
311          <li>Optionally rename files before uploading.</li>
312          <li>For Israeli documents, check the "Israeli" checkbox next to the file.</li>
313          <li>Click "Upload Files" to process the documents.</li>
314        </ol>
315
316        <h3>Folder Structure</h3>
317        <p>Files are automatically organized in the following structure:</p>
318        <pre style="background: #f5f5f5; padding: 10px; border-radius: 4px;">
319Root Folder (Expense/Invoices/Receipts)
320└── Year (e.g., 2024)
321    └── Month (e.g., 01_Jan)
322        ├── Regular files
323        └── Israeli (folder for Israeli documents)
324        </pre>
325      </div>
326    </div>
327
328    <!-- Create Folders Tab -->
329    <div id="createTab" class="tab-content">
330      <h2>Create Folder Structure</h2>
331      <p>Use this section to create the initial folder structure for your documents. This will create the year and month folders according to the current date.</p>
332
333      <div class="form-group">
334        <label>Select folder types to create:</label>
335        <div style="margin: 10px 0;">
336          <label class="checkbox-label">
337            <input type="checkbox" id="createExpense" checked> Expense Folders
338          </label>
339        </div>
340        <div style="margin: 10px 0;">
341          <label class="checkbox-label">
342            <input type="checkbox" id="createInvoices" checked> Invoice Folders
343          </label>
344        </div>
345        <div style="margin: 10px 0;">
346          <label class="checkbox-label">
347            <input type="checkbox" id="createReceipts" checked> Receipt Folders
348          </label>
349        </div>
350      </div>
351
352      <button onclick="createFolderStructure()" class="upload-btn">Create Folders</button>
353    </div>
354  </div>
355
356  <script>
357    const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
358
359    function updateStatusImage(state) {
360      const statusImage = document.getElementById('statusImage').querySelector('img');
361      switch(state) {
362        case 'ready':
363          statusImage.src = 'https://res.cloudinary.com/domtm8wiy/image/upload/v1738782120/accounting-router/otidlvt8pofbzcpknbc6.png';
364          break;
365        case 'uploading':
366          statusImage.src = 'https://res.cloudinary.com/domtm8wiy/image/upload/v1738782120/accounting-router/lvameumllsc7lny3pddj.png';
367          break;
368        case 'saved':
369          statusImage.src = 'https://res.cloudinary.com/domtm8wiy/image/upload/v1738782120/accounting-router/izle2zu9xgfkmkskmv2r.png';
370          break;
371        case 'israeli':
372          const img = statusImage;
373          img.src = 'https://res.cloudinary.com/domtm8wiy/image/upload/v1738782120/accounting-router/yiw6yhd5r6oc4icka3ad.png';
374          img.classList.add('israeli-animation');
375          setTimeout(() => {
376            img.classList.add('israeli-animation-exit');
377            setTimeout(() => {
378              img.classList.remove('israeli-animation', 'israeli-animation-exit');
379              updateStatusImage('ready');
380            }, 3000);
381          }, 500);
382          break;
383      }
384    }
385
386    function populateMonthsDropdown() {
387      const monthSelect = document.getElementById('monthOverride');
388      const now = new Date();
389      const currentYear = now.getFullYear();
390
391      // Add past 12 months as options
392      for (let i = 0; i < 12; i++) {
393        const date = new Date(currentYear, now.getMonth() - i, 1);
394        const monthNum = String(date.getMonth() + 1).padStart(2, '0');
395        const monthName = months[date.getMonth()];
396        const year = date.getFullYear();
397        const value = `${year}-${monthNum}`;
398        const text = `${monthNum}_${monthName} ${year}`;
399
400        const option = new Option(text, value);
401        monthSelect.add(option);
402      }
403    }
404
405    window.onload = function() {
406      // Display current accounting month and populate months dropdown
407      populateMonthsDropdown();
408      const now = new Date();
409      document.getElementById('currentMonth').querySelector('span').textContent = `Current Accounting Month: ${String(now.getMonth() + 1).padStart(2, '0')}_${months[now.getMonth()]} ${now.getFullYear()}`;
410
411      // First try to load from localStorage for immediate display
412      const localConfig = JSON.parse(localStorage.getItem('accountingRouterConfig') || '{}');
413      document.getElementById('expenseFolder').value = localConfig.EXPENSE || '';
414      document.getElementById('invoicesFolder').value = localConfig.INVOICES || '';
415      document.getElementById('receiptsFolder').value = localConfig.RECEIPTS || '';
416
417      // Then load from server-side storage which is the source of truth
418      google.script.run
419        .withSuccessHandler(config => {
420          if (config) {
421            document.getElementById('expenseFolder').value = config.EXPENSE || '';
422            document.getElementById('invoicesFolder').value = config.INVOICES || '';
423            document.getElementById('receiptsFolder').value = config.RECEIPTS || '';
424            // Update localStorage to match server state
425            localStorage.setItem('accountingRouterConfig', JSON.stringify(config));
426          }
427        })
428        .getConfiguration();
429
430      // Set initial status image
431      updateStatusImage('ready');
432    };
433
434    function extractFolderId(input) {
435      // Handle various Google Drive URL formats
436      const patterns = [
437        /\/folders\/([a-zA-Z0-9_-]+)(?:\/|\?|$)/,  // /folders/ID format
438        /id=([a-zA-Z0-9_-]+)(?:&|$)/,              // id=ID format
439        /^([a-zA-Z0-9_-]+)$/                       // Direct ID format
440      ];
441
442      for (const pattern of patterns) {
443        const match = input.match(pattern);
444        if (match) return match[1];
445      }
446
447      return input; // Return original input if no pattern matches
448    }
449
450    function handleFolderInput(input) {
451      const value = input.value.trim();
452      if (value) {
453        const folderId = extractFolderId(value);
454        input.value = folderId;
455      }
456    }
457
458    function showTab(tabName) {
459      // Hide all tabs
460      document.querySelectorAll('.tab-content').forEach(tab => {
461        tab.classList.remove('active');
462      });
463      document.querySelectorAll('.tab').forEach(tab => {
464        tab.classList.remove('active');
465      });
466
467      // Show selected tab
468      document.getElementById(tabName + 'Tab').classList.add('active');
469      document.querySelector(`.tab[onclick="showTab('${tabName}')"]`).classList.add('active');
470    }
471
472    function saveConfig() {
473      const config = {
474        EXPENSE: document.getElementById('expenseFolder').value,
475        INVOICES: document.getElementById('invoicesFolder').value,
476        RECEIPTS: document.getElementById('receiptsFolder').value
477      };
478
479      localStorage.setItem('accountingRouterConfig', JSON.stringify(config));
480
481      const statusDiv = document.getElementById('configStatus');
482      statusDiv.className = 'status success';
483      statusDiv.style.display = 'block';
484      statusDiv.innerHTML = 'Saving configuration...';
485
486      // Sync with Google Apps Script properties
487      google.script.run
488        .withSuccessHandler(() => {
489          statusDiv.innerHTML = 'Configuration saved successfully! ✓';
490          setTimeout(() => {
491            statusDiv.style.display = 'none';
492          }, 3000);
493        })
494        .withFailureHandler(error => {
495          statusDiv.className = 'status error';
496          statusDiv.innerHTML = 'Error saving configuration: ' + error.message;
497        })
498        .saveConfiguration(config);
499    }
500
501    function createFolderStructure() {
502      const config = JSON.parse(localStorage.getItem('accountingRouterConfig') || '{}');
503      const folderTypes = [];
504
505      if (document.getElementById('createExpense').checked) folderTypes.push('EXPENSE');
506      if (document.getElementById('createInvoices').checked) folderTypes.push('INVOICES');
507      if (document.getElementById('createReceipts').checked) folderTypes.push('RECEIPTS');
508
509      if (folderTypes.length === 0) {
510        showStatus('Please select at least one folder type.', true);
511        return;
512      }
513
514      document.getElementById('loading').style.display = 'block';
515
516      google.script.run
517        .withSuccessHandler(response => {
518          document.getElementById('loading').style.display = 'none';
519          showStatus(response.message, !response.success);
520        })
521        .withFailureHandler(error => {
522          document.getElementById('loading').style.display = 'none';
523          showStatus('Error creating folders: ' + error.message, true);
524        })
525        .createFolderStructure(folderTypes);
526    }
527
528    let selectedFiles = [];
529
530    document.getElementById('files').addEventListener('change', function(e) {
531      const files = Array.from(e.target.files).map(file => ({
532        file,
533        isIsraeli: false,
534        newName: file.name
535      }));
536
537      if (files.length > 10) {
538        alert('Please select up to 10 files only.');
539        return;
540      }
541
542      selectedFiles = files;
543      updateFileList();
544    });
545
546    function toggleIsraeli(index) {
547      selectedFiles[index].isIsraeli = !selectedFiles[index].isIsraeli;
548      if (selectedFiles[index].isIsraeli) {
549        updateStatusImage('israeli');
550        setTimeout(() => updateStatusImage('ready'), 1000);
551      }
552      updateFileList();
553    }
554
555    function updateFileName(index, newName) {
556      selectedFiles[index].newName = newName;
557    }
558
559    function handleDragOver(event) {
560      event.preventDefault();
561      event.stopPropagation();
562      document.getElementById('fileList').classList.add('drag-over');
563    }
564
565    function handleDragLeave(event) {
566      event.preventDefault();
567      event.stopPropagation();
568      document.getElementById('fileList').classList.remove('drag-over');
569    }
570
571    function handleDrop(event) {
572      event.preventDefault();
573      event.stopPropagation();
574
575      const fileList = document.getElementById('fileList');
576      fileList.classList.remove('drag-over');
577
578      const droppedFiles = Array.from(event.dataTransfer.files).map(file => ({
579        file,
580        isIsraeli: false,
581        newName: file.name
582      }));
583
584      if (selectedFiles.length + droppedFiles.length > 10) {
585        alert('Total number of files cannot exceed 10.');
586        return;
587      }
588
589      selectedFiles = [...selectedFiles, ...droppedFiles];
590      updateFileList();
591    }
592
593    function updateFileList() {
594      const fileListContent = document.getElementById('fileListContent');
595      if (selectedFiles.length === 0) {
596        fileListContent.innerHTML = '';
597        return;
598      }
599
600      fileListContent.innerHTML = selectedFiles.map((fileObj, index) => `
601        <div class="file-item">
602          <div style="flex: 1;">
603            <input type="text" 
604                   class="file-rename"
605                   value="${fileObj.newName}"
606                   onchange="updateFileName(${index}, this.value)"
607                   placeholder="Enter new filename">
608            <div class="file-original">Original: ${fileObj.file.name}</div>
609          </div>
610          <div style="display: flex; align-items: center; gap: 10px; margin-left: 10px;">
611            <label class="checkbox-label">
612              <input type="checkbox" 
613                     onchange="toggleIsraeli(${index})" 
614                     ${fileObj.isIsraeli ? 'checked' : ''}>
615              Israeli
616            </label>
617            <button onclick="removeFile(${index})" style="color: red; background: none; border: none;">×</button>
618          </div>
619        </div>
620      `).join('');
621    }
622
623    function removeFile(index) {
624      selectedFiles.splice(index, 1);
625      updateFileList();
626    }
627
628    function clearAll() {
629      selectedFiles = [];
630      document.getElementById('folderType').value = '';
631      document.getElementById('files').value = '';
632      document.getElementById('status').style.display = 'none';
633      updateStatusImage('ready');
634      updateFileList();
635    }
636
637    function showStatus(message, isError = false) {
638      const status = document.getElementById('status');
639      status.className = 'status ' + (isError ? 'error' : 'success');
640      status.style.display = 'block';
641      status.innerHTML = message;
642      if (!isError) {
643        setTimeout(() => {
644          status.style.display = 'none';
645        }, 5000);
646      }
647    }
648
649    function processFiles() {
650      const folderType = document.getElementById('folderType').value;
651
652      if (!folderType) {
653        showStatus('Please select a document type.', true);
654        return;
655      }
656
657      if (selectedFiles.length === 0) {
658        showStatus('Please select at least one file.', true);
659        return;
660      }
661
662      document.getElementById('loading').style.display = 'block';
663      showStatus('Preparing to upload files...', false);
664      updateStatusImage('uploading');
665
666      // Create an array of promises for each file
667      const promises = selectedFiles.map(fileObj => {
668        return new Promise((resolve, reject) => {
669          const reader = new FileReader();
670          reader.onload = (e) => {
671            resolve({
672              fileName: fileObj.newName,
673              mimeType: fileObj.file.type,
674              bytes: e.target.result.split(',')[1],
675              isIsraeli: fileObj.isIsraeli
676            });
677          };
678          reader.onerror = (e) => reject(e);
679          reader.readAsDataURL(fileObj.file);
680        });
681      });
682
683      // Process all files
684      Promise.all(promises)
685        .then(fileObjects => {
686          const monthOverride = document.getElementById('monthOverride').value;
687          showStatus('Uploading files to Google Drive...', false);
688          google.script.run
689            .withSuccessHandler(response => {
690              document.getElementById('loading').style.display = 'none';
691              if (response.success) {
692                let message = response.message + '<br><br>Files processed:';
693                response.results.forEach(result => {
694                  message += `<br>• ${result.name} - <a href="${result.url}" target="_blank">View file</a>`;
695                });
696                showStatus(message);
697                updateStatusImage('saved');
698                setTimeout(() => {
699                  clearAll();
700                  updateStatusImage('ready');
701                }, 3000);
702              } else {
703                showStatus(response.message, true);
704                updateStatusImage('ready');
705              }
706            })
707            .withFailureHandler(error => {
708              document.getElementById('loading').style.display = 'none';
709              showStatus('Error processing files: ' + error.message, true);
710              updateStatusImage('ready');
711            })
712            .processFiles(fileObjects, folderType, monthOverride);
713        })
714        .catch(error => {
715          document.getElementById('loading').style.display = 'none';
716          showStatus('Error preparing files: ' + error.message, true);
717          updateStatusImage('ready');
718        });
719    }
720  </script>
721</body>
722</html>

This project was created by Daniel Rosehill (public at danielrosehill dot com) and is licensed under CC-BY-4.0.

Daniel Rosehill

Automation specialist and technical communications professional bridging AI systems, workflow orchestration, and strategic communications for enhanced business performance.

Learn more about Daniel