incode, admin spreadsheet script

const properties = PropertiesService.getScriptProperties();
const year = properties.getProperty('year');
function BETWEEN(value, min, max) {
return value >= min && value <= max;
}
/**
* Retrieves all the rows in the active spreadsheet that contain data and logs the
* values for each row.
* For more information on using the Spreadsheet API, see
* https://developers.google.com/apps-script/service_spreadsheet
*/
function readRows() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
for (var i = 0; i <= numRows - 1; i++) {
var row = values[i];
Logger.log(row);
}
};
/**
* Adds a custom menu to the active spreadsheet, containing a single menu item
* for invoking the readRows() function specified above.
* The onOpen() function, when defined, is automatically invoked whenever the
* spreadsheet is opened.
* For more information on using the Spreadsheet API, see
* https://developers.google.com/apps-script/service_spreadsheet
*/
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Read Data",
functionName : "readRows"
}];
sheet.addMenu("Script Center Menu", entries);
};
/*====================================================================================================================================*
ImportJSON by Trevor Lohrbeer (@FastFedora)
====================================================================================================================================
Version: 1.1
Project Page: http://blog.fastfedora.com/projects/import-json
Copyright: (c) 2012 by Trevor Lohrbeer
License: GNU General Public License, version 3 (GPL-3.0)
http://www.opensource.org/licenses/gpl-3.0.html
------------------------------------------------------------------------------------------------------------------------------------
A library for importing JSON feeds into Google spreadsheets. Functions include:
ImportJSON For use by end users to import a JSON feed from a URL
ImportJSONAdvanced For use by script developers to easily extend the functionality of this library
Future enhancements may include:
- Support for a real XPath like syntax similar to ImportXML for the query parameter
- Support for OAuth authenticated APIs
Or feel free to write these and add on to the library yourself!
------------------------------------------------------------------------------------------------------------------------------------
Changelog:
1.1 Added support for the noHeaders option
1.0 Initial release
*====================================================================================================================================*/
/**
* Imports a JSON feed and returns the results to be inserted into a Google Spreadsheet. The JSON feed is flattened to create
* a two-dimensional array. The first row contains the headers, with each column header indicating the path to that data in
* the JSON feed. The remaining rows contain the data.
*
* By default, data gets transformed so it looks more like a normal data import. Specifically:
*
* - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
* of the rows representing their parent elements.
* - Values longer than 256 characters get truncated.
* - Headers have slashes converted to spaces, common prefixes removed and the resulting text converted to title case.
*
* To change this behavior, pass in one of these values in the options parameter:
*
* noInherit: Don't inherit values from parent elements
* noTruncate: Don't truncate values
* rawHeaders: Don't prettify headers
* noHeaders: Don't include headers, only the data
* debugLocation: Prepend each value with the row & column it belongs in
*
* For example:
*
* =ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json", "/feed/entry/title,/feed/entry/content",
* "noInherit,noTruncate,rawHeaders")
*
* @param {url} the URL to a public JSON feed
* @param {query} a comma-separated lists of paths to import. Any path starting with one of these paths gets imported.
* @param {options} a comma-separated list of options that alter processing of the data
*
* @return a two-dimensional array containing the data, with the first row containing headers
* @customfunction
**/
function ImportJSON(url, query, options) {
return ImportJSONAdvanced(url, query, options, includeXPath_, defaultTransform_);
}
/**
* An advanced version of ImportJSON designed to be easily extended by a script. This version cannot be called from within a
* spreadsheet.
*
* Imports a JSON feed and returns the results to be inserted into a Google Spreadsheet. The JSON feed is flattened to create
* a two-dimensional array. The first row contains the headers, with each column header indicating the path to that data in
* the JSON feed. The remaining rows contain the data.
*
* Use the include and transformation functions to determine what to include in the import and how to transform the data after it is
* imported.
*
* For example:
*
* =ImportJSON("http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&alt=json",
* "/feed/entry",
* function (query, path) { return path.indexOf(query) == 0; },
* function (data, row, column) { data[row][column] = data[row][column].toString().substr(0, 100); } )
*
* In this example, the import function checks to see if the path to the data being imported starts with the query. The transform
* function takes the data and truncates it. For more robust versions of these functions, see the internal code of this library.
*
* @param {url} the URL to a public JSON feed
* @param {query} the query passed to the include function
* @param {options} a comma-separated list of options that may alter processing of the data
* @param {includeFunc} a function with the signature func(query, path, options) that returns true if the data element at the given path
* should be included or false otherwise.
* @param {transformFunc} a function with the signature func(data, row, column, options) where data is a 2-dimensional array of the data
* and row & column are the current row and column being processed. Any return value is ignored. Note that row 0
* contains the headers for the data, so test for row==0 to process headers only.
*
* @return a two-dimensional array containing the data, with the first row containing headers
**/
function ImportJSONAdvanced(url, query, options, includeFunc, transformFunc) {
var jsondata = UrlFetchApp.fetch(url);
var object = JSON.parse(jsondata.getContentText());
return parseJSONObject_(object, query, options, includeFunc, transformFunc);
}
/**
* Encodes the given value to use within a URL.
*
* @param {value} the value to be encoded
*
* @return the value encoded using URL percent-encoding
*/
function URLEncode(value) {
return encodeURIComponent(value.toString());
}
/**
* Parses a JSON object and returns a two-dimensional array containing the data of that object.
*/
function parseJSONObject_(object, query, options, includeFunc, transformFunc) {
var headers = new Array();
var data = new Array();
if (query && !Array.isArray(query) && query.toString().indexOf(",") != -1) {
query = query.toString().split(",");
}
if (options) {
options = options.toString().split(",");
}
parseData_(headers, data, "", 1, object, query, options, includeFunc);
parseHeaders_(headers, data);
transformData_(data, options, transformFunc);
return hasOption_(options, "noHeaders") ? (data.length > 1 ? data.slice(1) : new Array()) : data;
}
/**
* Parses the data contained within the given value and inserts it into the data two-dimensional array starting at the rowIndex.
* If the data is to be inserted into a new column, a new header is added to the headers array. The value can be an object,
* array or scalar value.
*
* If the value is an object, it's properties are iterated through and passed back into this function with the name of each
* property extending the path. For instance, if the object contains the property "entry" and the path passed in was "/feed",
* this function is called with the value of the entry property and the path "/feed/entry".
*
* If the value is an array containing other arrays or objects, each element in the array is passed into this function with
* the rowIndex incremeneted for each element.
*
* If the value is an array containing only scalar values, those values are joined together and inserted into the data array as
* a single value.
*
* If the value is a scalar, the value is inserted directly into the data array.
*/
function parseData_(headers, data, path, rowIndex, value, query, options, includeFunc) {
var dataInserted = false;
if (isObject_(value)) {
for (key in value) {
if (parseData_(headers, data, path + "/" + key, rowIndex, value[key], query, options, includeFunc)) {
dataInserted = true;
}
}
} else if (Array.isArray(value) && isObjectArray_(value)) {
for (var i = 0; i < value.length; i++) {
if (parseData_(headers, data, path, rowIndex, value[i], query, options, includeFunc)) {
dataInserted = true;
rowIndex++;
}
}
} else if (!includeFunc || includeFunc(query, path, options)) {
// Handle arrays containing only scalar values
if (Array.isArray(value)) {
value = value.join();
}
// Insert new row if one doesn't already exist
if (!data[rowIndex]) {
data[rowIndex] = new Array();
}
// Add a new header if one doesn't exist
if (!headers[path] && headers[path] != 0) {
headers[path] = Object.keys(headers).length;
}
// Insert the data
data[rowIndex][headers[path]] = value;
dataInserted = true;
}
return dataInserted;
}
/**
* Parses the headers array and inserts it into the first row of the data array.
*/
function parseHeaders_(headers, data) {
data[0] = new Array();
for (key in headers) {
data[0][headers[key]] = key;
}
}
/**
* Applies the transform function for each element in the data array, going through each column of each row.
*/
function transformData_(data, options, transformFunc) {
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].length; j++) {
transformFunc(data, i, j, options);
}
}
}
/**
* Returns true if the given test value is an object; false otherwise.
*/
function isObject_(test) {
return Object.prototype.toString.call(test) === '[object Object]';
}
/**
* Returns true if the given test value is an array containing at least one object; false otherwise.
*/
function isObjectArray_(test) {
for (var i = 0; i < test.length; i++) {
if (isObject_(test[i])) {
return true;
}
}
return false;
}
/**
* Returns true if the given query applies to the given path.
*/
function includeXPath_(query, path, options) {
if (!query) {
return true;
} else if (Array.isArray(query)) {
for (var i = 0; i < query.length; i++) {
if (applyXPathRule_(query[i], path, options)) {
return true;
}
}
} else {
return applyXPathRule_(query, path, options);
}
return false;
};
/**
* Returns true if the rule applies to the given path.
*/
function applyXPathRule_(rule, path, options) {
return path.indexOf(rule) == 0;
}
/**
* By default, this function transforms the value at the given row & column so it looks more like a normal data import. Specifically:
*
* - Data from parent JSON elements gets inherited to their child elements, so rows representing child elements contain the values
* of the rows representing their parent elements.
* - Values longer than 256 characters get truncated.
* - Values in row 0 (headers) have slashes converted to spaces, common prefixes removed and the resulting text converted to title
* case.
*
* To change this behavior, pass in one of these values in the options parameter:
*
* noInherit: Don't inherit values from parent elements
* noTruncate: Don't truncate values
* rawHeaders: Don't prettify headers
* debugLocation: Prepend each value with the row & column it belongs in
*/
function defaultTransform_(data, row, column, options) {
if (!data[row][column]) {
if (row < 2 || hasOption_(options, "noInherit")) {
data[row][column] = "";
} else {
data[row][column] = data[row-1][column];
}
}
if (!hasOption_(options, "rawHeaders") && row == 0) {
if (column == 0 && data[row].length > 1) {
removeCommonPrefixes_(data, row);
}
data[row][column] = toTitleCase_(data[row][column].toString().replace(/[\/\_]/g, " "));
}
if (!hasOption_(options, "noTruncate") && data[row][column]) {
data[row][column] = data[row][column].toString().substr(0, 256);
}
if (hasOption_(options, "debugLocation")) {
data[row][column] = "[" + row + "," + column + "]" + data[row][column];
}
}
/**
* If all the values in the given row share the same prefix, remove that prefix.
*/
function removeCommonPrefixes_(data, row) {
var matchIndex = data[row][0].length;
for (var i = 1; i < data[row].length; i++) {
matchIndex = findEqualityEndpoint_(data[row][i-1], data[row][i], matchIndex);
if (matchIndex == 0) {
return;
}
}
for (var i = 0; i < data[row].length; i++) {
data[row][i] = data[row][i].substring(matchIndex, data[row][i].length);
}
}
/**
* Locates the index where the two strings values stop being equal, stopping automatically at the stopAt index.
*/
function findEqualityEndpoint_(string1, string2, stopAt) {
if (!string1 || !string2) {
return -1;
}
var maxEndpoint = Math.min(stopAt, string1.length, string2.length);
for (var i = 0; i < maxEndpoint; i++) {
if (string1.charAt(i) != string2.charAt(i)) {
return i;
}
}
return maxEndpoint;
}
/**
* Converts the text to title case.
*/
function toTitleCase_(text) {
if (text == null) {
return null;
}
return text.replace(/\w\S*/g, function(word) { return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase(); });
}
/**
* Returns true if the given set of options contains the given option.
*/
function hasOption_(options, option) {
return options && options.indexOf(option) >= 0;
}
function addRowsToSheet(sheet, rows) {
var row = rows.length;
var column = rows[0].length;
sheet.getRange(sheet.getLastRow()+1, 1, row, column).setValues(rows);
}
function getInvoices() {
const properties = PropertiesService.getScriptProperties();
var url = "https://api-v2.fattureincloud.it";
var endpoint = "/c/36/issued_documents";
var headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${properties.getProperty('fattureincloud_access_token')}`
};
var options = {
"method": "get",
"headers": headers,
};
var queryParams = [
"type=" + "invoice",
"per_page="+ 100,
"fieldset="+ "detailed",
"sort=-number,-date",
"q="+encodeURIComponent(`date>'${year-1}-01-01'`)
];
var pageKey = "page"
var pageNum = 1;
var nextPageUrl;
var data = {},
output = [];
do {
var currentUrl = url + endpoint + "?" + queryParams.join("&");
currentUrl += "&" + pageKey + "=" + pageNum;
var response = UrlFetchApp.fetch(currentUrl, options);
data = JSON.parse(response.getContentText());
output.push(...data.data)
pageNum++;
nextPageUrl = data.next_page_url;
} while (nextPageUrl);
return output;
}
function getPaymentsDue() {
const invoices = getInvoices();
console.log(invoices)
const payments = [];
invoices.forEach( invoice => payments.push(...invoice.payments_list))
const paymentsDue = payments.filter( payment => {
return (new Date(payment.due_date) < (new Date()) && payment.status == "not_paid")
})
console.log(paymentsDue)
const amountDue = paymentsDue.reduce( (acc, cur) => acc+=cur.amount , 0)
console.log(amountDue)
return amountDue;
}
function revenueYear(year=null) {
if(!year) year = (new Date()).getFullYear();
const invoices = getInvoices()
.filter( i => i.year == year)
const revenue = invoices.reduce((sum, i) => {return sum += i.amount_net}, 0)
console.log(invoices)
return revenue;
}
function flycalPlanningDefaultOptions() {
return {
from: (new Date().toISOString().slice(0, 10) + "T00:00:00Z"),
calendar: "incode - antonio|incode - luca|incode - matteo|incode - andrea",
to: "2026-12-31",
keys: "placeholder|rui|composer|solver|sanlorenzo|fraghe|comoonboat|vitaloni|hangerstore|danesi",
}
}
function getEventHoursByKeys(options={}) {
console.log("getEventHoursByKeys", options)
options = {...flycalPlanningDefaultOptions(), ...options}
const url = `https://app.flycal.it/api/flycal/search`;
const body = {
from: options.from, //aggiunto il from
to: options.to,
calendar: options.calendar, //aggiunto calendario
description: options.keys,
groupBy: options.keys,
}
var options = {
"method": "get",
"headers": headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${properties.getProperty('flycal_api_token')}`
},
payload: JSON.stringify(body),
};
const response = UrlFetchApp.fetch(url, options);
const data = JSON.parse(response.getContentText());
const output = {};
data.groups.forEach(g => {
output[g.key] = g.total_hours
})
return data;
}
function parseGroupsHours_(data) {
const rows = [];
const test = (data.groups || []).map(g => [
g.key,
g.events_found,
(g.items || []).length,
[...new Set((g.items || []).map(i => i.calendarSummary))]
])
console.log("test", test);
(data.groups || []).forEach(group => {
const totals = {}; // { calendarSummary: ore }
(group.items || []).forEach(item => {
const start = new Date(item.start.dateTime);
const end = new Date(item.end.dateTime);
const hours = (end - start) / (1000 * 60 * 60);
const cal = item.calendarSummary || item.calendarId || "unknown";
totals[cal] = (totals[cal] || 0) + hours;
});
Object.entries(totals).forEach(([cal, hours]) => {
const h = Math.round(hours * 100) / 100;
const d = hours/8;
rows.push([group.key, cal, h, d ]);
});
});
return rows;
}
// Il floglio chiama la fuzione FLYCAL_HOURS passando tra argomenti non un options... riportati nella firma del metodo e definiti al suo interno
function FLYCAL_HOURS(calendar, to, keys) {
const options = flycalPlanningDefaultOptions();
if (calendar) options.calendar = calendar;
if (to) options.to = to;
if (keys) options.keys = keys;
const cache = CacheService.getScriptCache();
//const cacheKey = `CacheFlycalHours-${options.to}-${options.keys}`;
const raw = [options.from, options.to, options.calendar, options.keys].join('§');
const cacheKey = 'flycal-' + Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, raw));
const cached = cache.get(cacheKey);
if (cached) {
console.log("cache HIT", cached)
return JSON.parse(cached);
}
const data = getEventHoursByKeys(options)
const rows = parseGroupsHours_(data)
cache.put(cacheKey, JSON.stringify(rows), 300); // 300 sec = 5 min
console.log(rows)
return rows;
}
function testFlycal() {
const rows = FLYCAL_HOURS(
'incode - antonio|incode - andrea|incode - matteo|incode - luca',
'2026-12-31',
'placeholder|rui|composer|solver|sanlorenzo|fraghe|comoonboat|vitaloni|hangerstore|danesi.revamp'
);
console.log(JSON.stringify(rows, null, 2));
}
function testFormat(val) {
return 1;
}
const CONFIG_WORK = {
CODE_CELL: 'a11', // cella da cui leggere il numero "code"
CODE_SHEET: null, // null = foglio corrente; oppure 'NomeFoglio' se il code sta altrove
GROUP_COLUMN: 4, // indice di una colonna DENTRO il gruppo da espandere (1 = A, 5 = E...)
GROUP_DEPTH: 1, // profondità del gruppo (1 = gruppo di primo livello)
TRIGGER_CELL: 'D1' // cella con la casella di controllo (opzionale, vedi onEdit)
};
/**
* Valori da inserire nella nuova riga.
* Il primo elemento viene sostituito con il code letto dalla cella.
*/
function buildRow(code) {
return [
code,
'work',
'aperto', //state
'', //handle
1000,
'', //future income?
'client', //client
'', //rif cliente / po
'direct', //ownership
'website', //tags
'none', //data access?
'no', //admin_access?
'none', // dpa_status
2026, //year
];
}
/**
* Estrae il numero da valori come 123, "123", "next: 123", "next:123".
* Restituisce null se non trova un numero.
*/
function parseCode(value) {
if (typeof value === 'number') return value;
// Versione senza regexp: prende quello che c'è dopo l'ultimo ":"
const part = String(value).split(':').pop().trim();
const n = Number(part);
if (part !== '' && !isNaN(n)) return n;
// Fallback con regexp: primo gruppo di cifre ovunque nel testo
const m = String(value).match(/\d+/);
return m ? Number(m[0]) : null;
}
/**
* Funzione principale: da assegnare al pulsante.
*/
function newWork() {
const ui = SpreadsheetApp.getUi();
const answer = ui.alert('Conferma', 'Confirm new row?', ui.ButtonSet.YES_NO);
if (answer !== ui.Button.YES) return;
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
// 1. Legge il code
const codeSheet = CONFIG_WORK.CODE_SHEET ? ss.getSheetByName(CONFIG_WORK.CODE_SHEET) : sheet;
const code = parseCode(codeSheet.getRange(CONFIG_WORK.CODE_CELL).getValue());
if (code === '' || isNaN(Number(code))) {
SpreadsheetApp.getUi().alert('Nella cella ' + CONFIG_WORK.CODE_CELL + ' non c\'è un numero valido.');
return;
}
// 2. Inserisce la riga in fondo al foglio
sheet.appendRow(buildRow(Number(code)));
const lastRow = sheet.getLastRow();
// 3. Espande il gruppo di colonne (se esiste)
try {
const group = sheet.getColumnGroup(CONFIG_WORK.GROUP_COLUMN, CONFIG_WORK.GROUP_DEPTH);
if (group && group.isCollapsed()) {
group.expand();
}
} catch (e) {
// Nessun gruppo su quella colonna: ignora
}
// 4. Scrolla alla nuova riga selezionandola
SpreadsheetApp.flush();
sheet.getRange(lastRow, 1).activate();
}
/**
* Alternativa al pulsante: casella di controllo in CONFIG_WORK.TRIGGER_CELL.
* Quando viene spuntata esegue newWork e si rimette a falso.
*/
function onEdit(e) {
if (!e || !e.range) return;
if (e.range.getA1Notation() !== CONFIG_WORK.TRIGGER_CELL) return;
if (e.value !== 'TRUE') return;
newWork();
e.range.setValue(false);
}
参与讨论