tüit Logo Direkt zum Hauptinhalt

Clientskript (Client Script)

Einleitung

Diese Code-Erweiterungen können dazu genutzt werden über die Browser-Sitzung die Oberfläche von ERPNext zu manipulieren. Sie können in ganz unterschiedlicher Weise eingesetzt werden.

https://frappeframework.com/docs/v13/user/en/desk/scripting/client-script

Beispiele

Timesheet Button auf Projekt Listenansicht
// Doctype: Project
// View: List

const FILTER_BY_OWNER = true;          // only consider your own timesheets
const FALLBACK_EMPLOYEE = '';          // set if Employee is mandatory in your setup
const PARENT_SCAN_LIMIT = 25;          // how many recent Timesheets to scan

async function find_running_timer_for_project(project_name) {
  // 1) Get recent parent Timesheets
  const tsListRes = await frappe.call({
    method: 'frappe.client.get_list',
    args: {
      doctype: 'Timesheet',
      fields: ['name', 'owner', 'modified'],
      filters: [
        ['Timesheet', 'docstatus', '<', 2],
        ...(FILTER_BY_OWNER ? [['Timesheet', 'owner', '=', frappe.session.user]] : [])
      ],
      order_by: 'modified desc',
      limit_page_length: PARENT_SCAN_LIMIT
    }
  });

  const parents = tsListRes?.message || [];
  if (!parents.length) return null;

  // 2) Load each parent and inspect time_logs on the client
  for (const p of parents) {
    const tsRes = await frappe.call({
      method: 'frappe.client.get',
      args: { doctype: 'Timesheet', name: p.name }
    });
    const ts = tsRes?.message;
    if (!ts || !Array.isArray(ts.time_logs)) continue;

    const runningRow = ts.time_logs.find(
      tl => tl.project === project_name && !tl.to_time // running == no to_time
    );

    if (runningRow) {
      return { parent: ts.name, child: runningRow.name };
    }
  }

  return null;
}

frappe.listview_settings['Project'] = {
  button: {
    show() { return true; },
    get_label() { return __('Timer'); },
    get_description(doc) {
      return __('Start or manage a running Timesheet timer for Project {0}', [doc.name]);
    },
    action(doc) {
      (async () => {
        try {
          frappe.dom.freeze(__('Checking timers...'));
          const running = await find_running_timer_for_project(doc.name);
          frappe.dom.unfreeze();

          if (running) {
            // Running timer exists -> Open/Stop dialog
            const dlg = new frappe.ui.Dialog({
              title: __('Running Timer'),
              fields: [{ fieldtype: 'HTML', fieldname: 'info' }],
              primary_action_label: __('Stop Timer'),
              primary_action: async () => {
                try {
                  dlg.disable_primary_action();
                  dlg.set_message(__('Stopping timer...'));

                  const tsGet = await frappe.call({
                    method: 'frappe.client.get',
                    args: { doctype: 'Timesheet', name: running.parent }
                  });
                  const tsDoc = tsGet.message;

                  const row = (tsDoc.time_logs || []).find(r => r.name === running.child && !r.to_time);
                  if (!row) throw new Error('Running row not found');

                  row.to_time = frappe.datetime.now_datetime();

                  await frappe.call({
                    method: 'frappe.client.save',
                    args: { doc: tsDoc }
                  });

                  dlg.hide();
                  frappe.show_alert(
                    { message: __('Timer stopped. <a href="#Form/Timesheet/{0}">Open Timesheet</a>', [running.parent]), indicator: 'green' },
                    8
                  );
                } catch (e) {
                  console.error(e);
                  frappe.msgprint({ title: __('Error'), message: __('Could not stop the timer.'), indicator: 'red' });
                }
              }
            });

            dlg.get_field('info').$wrapper.html(`
              <div>
                ${__('A timer is already running for this project.')}
                <div class="mt-2">
                  <a class="btn btn-sm btn-secondary" href="#Form/Timesheet/${frappe.utils.escape_html(running.parent)}">
                    ${__('Open Timesheet')}
                  </a>
                </div>
              </div>
            `);
            dlg.show();
            return;
          }

          // No running timer -> Start flow
          const d = new frappe.ui.Dialog({
            title: __('Start Timer'),
            fields: [
              { fieldtype: 'Link', label: __('Project'), fieldname: 'project', options: 'Project', read_only: 1, reqd: 1 },
              { fieldtype: 'Link', label: __('Task'), fieldname: 'task', options: 'Task',
                get_query: () => ({ filters: { project: doc.name } })
              },
              { fieldtype: 'Link', label: __('Activity Type'), fieldname: 'activity_type', options: 'Activity Type', reqd: 1 },
              { fieldtype: 'Small Text', label: __('Notes'), fieldname: 'notes' }
            ],
            primary_action_label: __('Start'),
            primary_action: async () => {
              const v = d.get_values();
              if (!v) return;

              try {
                frappe.dom.freeze(__('Starting timer...'));
                const ts_doc = {
                  doctype: 'Timesheet',
                  ...(FALLBACK_EMPLOYEE ? { employee: FALLBACK_EMPLOYEE } : {}),
                  time_logs: [
                    {
                      project: v.project,
                      task: v.task || undefined,
                      activity_type: v.activity_type,
                      description: v.notes || undefined,
                      from_time: frappe.datetime.now_datetime() // running
                    }
                  ]
                };

                const ins = await frappe.call({
                  method: 'frappe.client.insert',
                  args: { doc: ts_doc }
                });

                frappe.dom.unfreeze();
                d.hide();

                const ts_name = ins?.message?.name;
                frappe.show_alert(
                  {
                    message: __(
                      'Timer started for {0}. <a href="#Form/Timesheet/{1}">Open Timesheet</a>',
                      [frappe.utils.escape_html(v.project), ts_name]
                    ),
                    indicator: 'green'
                  },
                  8
                );
              } catch (e) {
                console.error(e);
                frappe.dom.unfreeze();
                d.hide();
                frappe.msgprint({
                  title: __('Error'),
                  message: __('Could not start timer. Check mandatory fields or permissions.'),
                  indicator: 'red'
                });
              }
            }
          });

          d.set_value('project', doc.name);
          d.show();

        } catch (err) {
          console.error(err);
          frappe.dom.unfreeze();
          frappe.msgprint({ title: __('Error'), message: __('Unexpected error occurred.'), indicator: 'red' });
        }
      })();
    }
  }
};