Appointments

Save appointments into Excel file

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Appointments
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Save appointments into Excel file
// @author       Dorosh
// @match      https://app.zenmaid.com/appointments/list*
// @grant         GM_xmlhttpRequest
// ==/UserScript==

const dropDownMenu = document.querySelector('ul[class="dropdown-menu"]');
dropDownMenu.innerHTML += '<li><button id="toExcelBtn" class="to-aside" style="background:none;border:none;">&nbsp &nbsp &nbsp &nbsp Export Excel</button></li>';
const toExcelBtn = dropDownMenu.querySelector('button[id="toExcelBtn"]');
toExcelBtn.addEventListener('click', exportAppointments);

async function exportAppointments()
{
    console.log('==> Export appointments <==');

    let result = '';

    const appointmentListObject = document.querySelector('div[id="appointments-list"]');
    const appointmentsPerDay = groupAppointmentsListObject(appointmentListObject);

    console.log(`Appointments days amount is ${appointmentsPerDay.length}`);
    console.log('Start reading...');

    const sidebarDiv = document.createElement( 'div' );
    sidebarDiv.id = 'response';
    sidebarDiv.style.display = 'none';

    for (let dayIndex = 0; dayIndex < appointmentsPerDay.length; dayIndex++) {
        const appointmentsGroup = appointmentsPerDay[dayIndex];
        const day = appointmentsGroup.head.querySelector('div[class="col-xs-12"]').querySelector('h3').textContent.trim();

        console.log(`Day ${dayIndex}`);
        console.log(`Date: ${day}`);
        console.log(`Appointments: ${appointmentsGroup.appointments.length}`);

        result += `${day}\n`;
        result += 'Day;Time;Client;Price;Address;Phone;Recurrence;Assigned to;Working Hours;Check;Credit Card;Cash;Invoice;Zelle;Notes\n';

        for (let appointmentIndex = 0; appointmentIndex < appointmentsGroup.appointments.length; appointmentIndex++) {
            const appointmentInfo = appointmentsGroup.appointments[appointmentIndex];

            const timeMap = appointmentInfo.querySelector('h4[class="appointment-time-list"]')
                                        .textContent.trim().split('\n');
            const time = timeMap[0].trim();
            const workingHours = timeMap[1].trim();

            const clientInfo = appointmentInfo.querySelector('div[class="col-xs-12 col-sm-8 col-lg-4 client_name"]');
            const clientName = clientInfo.querySelector('h3[class="mt-s"]').textContent;
            const address = clientInfo.querySelector('p').textContent.trim();
            const phones = clientInfo.querySelectorAll('a');
            const phoneNumbers = Array.from(phones)
                                    .filter(a => a.href.includes('tel:'))
                                    .map(function(a) { return a.href.replace('tel:', '');});
            const customerPhone = phoneNumbers.length > 0 ? phoneNumbers.join() : 'No phone';

            const price = parseInt(appointmentInfo.querySelector('div[class="charges-container"]')
                                        .querySelector('span').textContent.trim().replace('$', ''));
            
            result += `${day};${time};${clientName};${price};${address};${customerPhone};`;

            const appointmentId = appointmentInfo.querySelector('a[class="appointment-entry"]').getAttribute("data-appointment");

            const sidebarResponse = await makeGetRequest(`https://app.zenmaid.com/appointments/${appointmentId}/sidebar`);
            const sidebarJson = JSON.parse(sidebarResponse.response);
            sidebarDiv.innerHTML = sidebarJson.html;

            const recurrence = sidebarDiv.querySelector('div[class="sidebar-element border-b-partial recurrence-properties"]')
                                        .querySelector('div[class="sidebar-element-body"]')
                                        .childNodes[1].textContent.trim();

            const assignedToWidget = sidebarDiv.querySelector('div[class="assignment-widget"]');
            const assingmentData = JSON.parse(assignedToWidget.getAttribute('data-assignment-widget-data'));
            const assignments = assingmentData.assignments.map(function(a) { return a.name;});
            const assignedTo = assignments.length > 0 ? assignments.join() : 'No Assigned Cleaners';

            result += `${recurrence};${assignedTo};${workingHours};`;

            const customFields = sidebarDiv.querySelector('div[class="sidebar-section custom-fields"]')
                                            .querySelectorAll('div[class="sidebar-labeled-value-label"]');
            const customFieldsMap = {};
            customFields.forEach(function(field) {
                let name = field.innerText.trim();
                let value = field.nextElementSibling.innerText.trim();
                customFieldsMap[name] = value;
            });

            let check = '';
            if(customFieldsMap['Check'] && customFieldsMap['Check'] === "Yes") { check = price; }

            let creditCard = '';
            if(customFieldsMap['Credit Card'] && customFieldsMap['Credit Card'] === "Yes") { creditCard = price; }

            let cash = '';
            if(customFieldsMap['Cash'] && customFieldsMap['Cash'] === "Yes") { cash = price; }

            let invoice = '';
            if(customFieldsMap['Invoice'] && customFieldsMap['Invoice'] === "Yes") { invoice = price; }

            let zelle = '';
            if(customFieldsMap['Zelle'] && customFieldsMap['Zelle'] === "Yes") { zelle = price; }

            result += `${check};${creditCard};${cash};${invoice};${zelle};`;

            const notesResponse = await makeGetRequest(`https://app.zenmaid.com/appointments/${appointmentId}/notes.json`);
            const notesJson = JSON.parse(notesResponse.response);
            const notesArray = Array.from(notesJson).map(function(note) {
                return note.body.replace(/<br\s*[\/]?>/gi, ' -- ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(';', '\\').trim();
            });

            const notes = notesArray.length > 0 ? notesArray.join(' || ') : 'No notes recorded';

            result += `${notes};\n`;
        }
    }
    
    download(result);
}

function groupAppointmentsListObject(target) {
    console.log('== Parse appointments table object ==');

    let result = [];

    for(let childIndex in target.children) {
        const child = target.children[childIndex];

        if(child.tagName != 'DIV') { continue; }

        if(child.className == 'row appointment-date-list') {
            let newDayObject = {};
            newDayObject.head = child;
            newDayObject.appointments = []
            result.push(newDayObject);
        } else if(child.className == 'row') {
            result[result.length - 1].appointments.push(child);
        }
    }

    return result;
}

function download(stringResult) {
    console.log(stringResult);

    const currentDate = new Date();

    let a = document.createElement('a');
    a.href = `data:attachment/csv,${encodeURIComponent(stringResult)}`; //content
    a.target = '_blank';
    a.download = `${currentDate.toUTCString()}.csv`; //file name
    a.click();
}

function getHoursInterval(input) {
    let time = {};

    time.full = input.split(' ').join('');

    const startPart = time.full.substring(0, time.full.indexOf('-'));
    const endPart = time.full.substring(time.full.indexOf('-') + 1, time.full.length);

    time.begin = to24HoursFormat(startPart);
    time.end = to24HoursFormat(endPart);

    const dateBegin = new Date(2000, 1, 1, time.begin.hours, time.begin.minutes);
    const dateEnd = new Date(2000, 1, 1, time.end.hours, time.end.minutes);

    const hoursDiff = (dateEnd.getTime() - dateBegin.getTime()) / 3600000;

    return hoursDiff;
}

function to24HoursFormat(input) {
    let result = {};

    const dayPart = input.substring(5, 7);
    const hours = parseInt(input.substring(0, 2));

    result.hours = (dayPart === 'PM' && hours !== 12)
        ? (hours + 12)
        : hours;

    result.minutes = parseInt(input.substring(3, 5));

    return result;
}

function makeGetRequest(url) {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: "GET",
      url: url,
      onload: function(response) {
        resolve(response);
      },
      onerror: function(error) {
        reject(error);
      }
    });
  });
}