{"files":[{"id":"b4550fd9-45cd-472b-ba40-ed7db72f6c14","name":"appsscript","type":"json","source":"{\n  \"timeZone\": \"Asia/Yangon\",\n  \"dependencies\": {\n  },\n  \"exceptionLogging\": \"STACKDRIVER\",\n  \"runtimeVersion\": \"V8\"\n}"},{"id":"20c2cc70-9ea8-43df-b1dc-9cc0b57a6f74","name":"Code","type":"server_js","source":"// \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d CONFIGURATION \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n// --- UPDATE THESE VALUES WITH YOUR DETAILS ---\n\n// 1. Google Drive Folder ID where the weekly sheets will be saved.\nconst FOLDER_ID \u003d \u00271KNwNKApqaaKWj9LUjY7nwikQ3A9nXt6z\u0027;\n\n// 2. MySQL Database Connection Details\nconst DB_HOST \u003d \u0027db-distribution-accounting-do-user-2718740-0.h.db.ondigitalocean.com\u0027;\nconst DB_PORT \u003d 25060;\nconst DB_NAME \u003d \u0027das-db\u0027;\nconst DB_USER \u003d \u0027thansoeaung\u0027;\nconst DB_PASSWORD \u003d \u0027cW8w3EQxFFWZfKBZeWjNbQ\u003d\u003d\u0027;\n\n// 3. Your Full SQL Queries and Target Sheet (Tab) Names\nconst QUERIES_TO_RUN \u003d [\n  {\n    sheetName: \u0027data\u0027,\n    query: `\n    select \n    p.is_active as Active,\n    p.id,\n\n    case \n        when p.is_expiry_date_tracking \u003d true and (p.shelf_life_days \u003c 30 or p.shelf_life_days is null) then \u0027Confirm shelf life\u0027\n        when p.brand_name is null or trim(p.brand_name) \u003d \u0027\u0027 then \u0027Confirm brand name\u0027\n        when p.is_batch_tracking \u003d false \n             and parent_c.name !\u003d \u0027Containers\u0027 \n             and not (bom.main_product_id is not null and bom.bom_type \u003d \u0027Kit\u0027) then \u0027No batch tracking\u0027\n        when p.is_expiry_date_tracking \u003d false and c.name \u003d \u0027Perishable\u0027 then \u0027No expiry date tracking\u0027\n        else \u0027OK\u0027\n    end as MasterdataAlert,\n    \n    p.sku,\n    p.name,\n    p.weight,\n    p.brand_name,\n    \n    coalesce(grandparent_c.name, \u0027NA\u0027) as CategoryL1,\n    coalesce(parent_c.name, \u0027NA\u0027) as CategoryL2,\n    c.name as CategoryL3,\n    p.description,\n    p.is_purchasable,\n    p.purchased_sku,\n    \n    -- Updated Source logic prioritizing Kits\n    case \n        when bom.main_product_id is not null and bom.bom_type \u003d \u0027Kit\u0027 then \u0027Kit\u0027\n        else \u0027Supplier\u0027\n    end as Source,\n\n    -- Replaced IDs with Account Names\n    pa.name as purchase_account_name,\n    p.is_sellable,\n    sa.name as sales_account_name,\n    p.is_batch_tracking,\n    p.is_expiry_date_tracking,\n    p.shelf_life_days,\n    ia.name as inventory_account_name\n\nfrom products as p\n\n-- Category Joins\nleft join product_categories as c \n    on p.category_id \u003d c.id\n\nleft join product_categories as parent_c \n    on c.parent_category_id \u003d parent_c.id\n\nleft join product_categories as grandparent_c \n    on parent_c.parent_category_id \u003d grandparent_c.id\n\n-- Account Joins\nleft join accounts as pa \n    on p.purchase_account_id \u003d pa.id\n\nleft join accounts as sa \n    on p.sales_account_id \u003d sa.id\n\nleft join accounts as ia \n    on p.inventory_account_id \u003d ia.id\n\n-- Bill of Materials Join\nleft join bill_of_materials as bom\n    on p.id \u003d bom.main_product_id\n\n\n\nwhere (p.is_sellable \u003d true or p.is_purchasable \u003d true)\n    and p.is_active \u003d true\n    and p.nature \u003d \u0027G\u0027\n    and p.business_id \u003d \u00277ef7c6c0-b59d-4be9-960c-7fa573038642\u0027\n\nORDER BY CategoryL3 ASC, Weight ASC, Name ASC;\n    `\n  }\n];\n\n\n// 4. Email Notification Configuration\n\nconst SPREADSHEET_ID \u003d \"17BxuaTjxvYqDytbMU7VRceNIqVYUqo0CD_e9LgUhz_o\"; \nconst TAB_ID \u003d 0; // 0 is typically the very first tab in a sheet, so this is fine!\nconst ss \u003d SpreadsheetApp.openById(SPREADSHEET_ID);\nvar EMAIL_RECIPIENTS \u003d \"\";\n\n// Fixed: changed ss.getSheet() to ss.getSheets()\nconst email_sheet \u003d ss.getSheets().find(s \u003d\u003e s.getSheetId() \u003d\u003d\u003d TAB_ID);\n\nif (email_sheet) {\n  EMAIL_RECIPIENTS \u003d email_sheet.getRange(\"D2\").getValue();\n  Logger.log(EMAIL_RECIPIENTS);\n} else {\n  Logger.log(\"Error: Could not find a tab with ID \" + TAB_ID);\n}\n\n\n// \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d END OF CONFIGURATION \u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\u003d\n\n\n/**\n * The main function to be scheduled weekly.\n */\nfunction mainWeeklyDataPull() {\n  try {\n    // --- 1. Calculate Dates and Filename ---\n    const today \u003d new Date();\n    // To get the date of the most recent Sunday for the filename.\n    const lastSunday \u003d new Date(today.setDate(today.getDate() - today.getDay()));\n    const timezone \u003d Session.getScriptTimeZone();\n    const baseName \u003d Utilities.formatDate(lastSunday, timezone, \"\u0027W\u0027ww/yy \u0027M-Trading Master Data Snapshot\u0027\");\n\n    // --- 2. Create the new Spreadsheet ---\n    const parentFolder \u003d DriveApp.getFolderById(FOLDER_ID);\n    const newSheet \u003d SpreadsheetApp.create(baseName);\n    const tempFile \u003d DriveApp.getFileById(newSheet.getId());\n    parentFolder.addFile(tempFile);\n    DriveApp.getRootFolder().removeFile(tempFile);\n    \n    Logger.log(`Successfully created new sheet: \"${baseName}\" (ID: ${newSheet.getId()})`);\n\n    // --- 3. Establish Database Connection ---\n    const dbUrl \u003d \"jdbc:mysql://\" + DB_HOST + \":\" + DB_PORT + \"/\" + DB_NAME;\n    const conn \u003d Jdbc.getConnection(dbUrl, DB_USER, DB_PASSWORD);\n    Logger.log(\"Successfully connected to the database.\");\n\n    // --- 4. Loop Through Each Query and Populate Sheets ---\n    QUERIES_TO_RUN.forEach(item \u003d\u003e {\n      Logger.log(`Fetching data for sheet: ${item.sheetName}`);\n      \n      const data \u003d fetchData(conn, item.query);\n      \n      if (data \u0026\u0026 data.length \u003e 1) { \n        Logger.log(`Retrieved ${data.length - 1} rows of data.`);\n        writeDataToSheet(newSheet, item.sheetName, data);\n      } else {\n        Logger.log(`No data found for sheet: ${item.sheetName}.`);\n        writeDataToSheet(newSheet, item.sheetName, [[\"No data found for the provided query\"]]);\n      }\n    });\n\n    // --- 5. Clean up Default Tabs ---\n    const defaultSheet \u003d newSheet.getSheetByName(\u0027Sheet1\u0027);\n    if (defaultSheet) {\n      newSheet.deleteSheet(defaultSheet);\n    }\n    conn.close();\n    Logger.log(\"Process complete. Database connection closed.\");\n\n    // --- 6. Export Target Tab to CSV and Send Email ---\n    Logger.log(\"Preparing email notification...\");\n    sendCsvEmail(newSheet, QUERIES_TO_RUN[0].sheetName, baseName);\n    \n  } catch (e) {\n    Logger.log(`An error occurred: ${e.message} \\nStack: ${e.stack}`);\n  }\n}\n\n/**\n * Fetches data from the database using a given query.\n */\nfunction fetchData(conn, query) {\n  const stmt \u003d conn.createStatement();\n  const rs \u003d stmt.executeQuery(query);\n  const numCols \u003d rs.getMetaData().getColumnCount();\n  const headers \u003d [];\n  for (let col \u003d 1; col \u003c\u003d numCols; col++) {\n    headers.push(rs.getMetaData().getColumnLabel(col));\n  }\n  \n  const data \u003d [headers];\n  \n  while (rs.next()) {\n    const row \u003d [];\n    for (let col \u003d 1; col \u003c\u003d numCols; col++) {\n      row.push(rs.getString(col));\n    }\n    data.push(row);\n  }\n  \n  rs.close();\n  stmt.close();\n  return data;\n}\n\n/**\n * Writes a 2D array of data to a specified sheet.\n */\nfunction writeDataToSheet(ss, sheetName, data) {\n  let sheet \u003d ss.getSheetByName(sheetName);\n  if (!sheet) {\n    sheet \u003d ss.insertSheet(sheetName);\n  }\n  sheet.clear();\n  \n  if (data \u0026\u0026 data.length \u003e 0) {\n    const range \u003d sheet.getRange(1, 1, data.length, data[0].length);\n    range.setValues(data);\n    sheet.setFrozenRows(1);\n    Logger.log(`Wrote data to sheet: \"${sheetName}\"`);\n  }\n}\n\n/**\n * Converts a specific sheet tab to a CSV blob and emails it to the team.\n * @param {Spreadsheet} ss The spreadsheet object.\n * @param {string} targetSheetName The specific tab name to convert.\n * @param {string} baseName The base filename used for subject and attachment.\n */\nfunction sendCsvEmail(ss, targetSheetName, baseName) {\n  const targetSheet \u003d ss.getSheetByName(targetSheetName);\n  if (!targetSheet) {\n    Logger.log(`Error: Could not find tab \"${targetSheetName}\" to export to CSV.`);\n    return;\n  }\n\n  // Get data from the sheet and convert to a CSV string format\n  const data \u003d targetSheet.getDataRange().getValues();\n  let csvString \u003d \"\";\n  \n  for (let i \u003d 0; i \u003c data.length; i++) {\n    const row \u003d data[i].map(cell \u003d\u003e {\n      // Escape inner quotes and wrap cells containing commas/newlines in quotes\n      let cellStr \u003d cell.toString();\n      if (cellStr.includes(\",\") || cellStr.includes(\"\\n\") || cellStr.includes(\u0027\"\u0027)) {\n        cellStr \u003d \u0027\"\u0027 + cellStr.replace(/\"/g, \u0027\"\"\u0027) + \u0027\"\u0027;\n      }\n      return cellStr;\n    });\n    csvString +\u003d row.join(\",\") + \"\\r\\n\";\n  }\n\n  // Convert string to an email attachment blob\n  const attachmentFilename \u003d `${baseName}.csv`;\n  const csvBlob \u003d Utilities.newBlob(csvString, \u0027text/csv\u0027, attachmentFilename);\n\n  // Draft and send email\n  const subject \u003d `Weekly Data Update: ${baseName}`;\n  const body \u003d `Hello Team,\\n\\nPlease find attached the weekly automated CSV data snapshot for: ${baseName}.\\n\\nThis is an automated system email.`;\n\n  MailApp.sendEmail({\n    to: EMAIL_RECIPIENTS,\n    subject: subject,\n    body: body,\n    attachments: [csvBlob]\n  });\n\n  Logger.log(`Notification email successfully sent to: ${EMAIL_RECIPIENTS}`);\n}\n\n/**\n * ONE-TIME SETUP: Run this function manually ONCE to create the weekly trigger.\n */\nfunction createWeeklyTrigger() {\n  const allTriggers \u003d ScriptApp.getProjectTriggers();\n  for (const trigger of allTriggers) {\n    if (trigger.getHandlerFunction() \u003d\u003d\u003d \u0027mainWeeklyDataPull\u0027) {\n      ScriptApp.deleteTrigger(trigger);\n    }\n  }\n  \n  ScriptApp.newTrigger(\u0027mainWeeklyDataPull\u0027)\n    .timeBased()\n    .onWeekDay(ScriptApp.WeekDay.MONDAY)\n    .atHour(4) // Triggers between 4:00 AM and 5:00 AM every Monday\n    .create();\n    \n  Logger.log(\u0027Successfully created the weekly trigger for Monday mornings.\u0027);\n}"}]}