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.
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:
Frontend (index.html)
Clean, user-friendly interface
File upload functionality
Document type selection
Israeli/non-Israeli toggle
File renaming capabilities
Backend (Code.gs)
Folder structure management
File routing logic
Google Drive API integration
Error handling and validation
Key Features

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:
Create a new Google Apps Script project
Add the Code.gs and index.html files
Configure deployment settings as a web app
Set appropriate access permissions
Deploy and obtain the public URL
Benefits

This application offers several key advantages:
Time Savings: Automates the tedious process of manually organizing documents
Consistency: Ensures a uniform folder structure across all document types
Compliance: Separates Israeli and international documents for easier tax reporting
Scalability: Handles growing document volumes with ease
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
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’)!
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.
Automation specialist and technical communications professional bridging AI systems, workflow orchestration, and strategic communications for enhanced business performance.
Learn more about Daniel