HEX
Server: Microsoft-IIS/10.0
System: Windows NT ITPWINWEBSVR22 10.0 build 20348 (Windows Server 2022) AMD64
User: www.conferencesearch.co.uk (0)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: D:/web/matomo.vdk/plugins/ScheduledReports/vue/src/AddReport/AddReport.vue
<!--
  Matomo - free/libre analytics platform

  @link    https://matomo.org
  @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
-->

<template>
  <ContentBlock
    class="entityAddContainer"
    :content-title="contentTitle"
  >
    <p>{{ translate('ScheduledReports_CreateTooltip') }}</p>
    <div class="clear" />
    <form
      id="addEditReport"
      @submit="$emit('submit')"
      v-form
    >
      <div>
        <Field
          uicontrol="text"
          name="website"
          :title="translate('General_Website')"
          :disabled="true"
          :model-value="siteName"
        >
        </Field>
      </div>
      <div>
        <Field
          uicontrol="textarea"
          name="report_description"
          :title="translate('General_Description')"
          :model-value="report.description"
          @update:model-value="$emit('change', { prop: 'description', value: $event })"
          :inline-help="translate('ScheduledReports_DescriptionOnFirstPageScheduledReport')"
        >
        </Field>
      </div>
      <div v-if="segmentEditorActivated">
        <Field
          uicontrol="select"
          name="report_segment"
          :title="translate('SegmentEditor_ChooseASegment')"
          :model-value="report.idsegment"
          @update:model-value="$emit('change', { prop: 'idsegment', value: $event })"
          :options="savedSegmentsById"
        >
          <template v-slot:inline-help>
            <div
              id="reportSegmentInlineHelp"
              class="inline-help-node"
              v-if="segmentEditorActivated"
              v-html="$sanitize(reportSegmentInlineHelp)"
            />
          </template>
        </Field>
      </div>
      <div>
        <Field
          uicontrol="select"
          name="report_schedule"
          :model-value="report.period"
          @update:model-value="$emit('change', { prop: 'period', value: $event });
            $emit('change', {
              prop: 'periodParam',
              value: report.period === 'never' ? null : report.period,
            })"
          :title="translate('ScheduledReports_ReportSchedule')"
          :options="periods"
        >
          <template v-slot:inline-help>
            <div
              id="emailScheduleInlineHelp"
              class="inline-help-node"
            >
              {{ translate('ScheduledReports_WeeklyScheduleHelp') }}
              <br />
              {{ translate('ScheduledReports_MonthlyScheduleHelp') }}
            </div>
          </template>
        </Field>
      </div>
      <div>
        <Field
          uicontrol="select"
          name="report_period"
          :model-value="report.periodParam"
          @update:model-value="$emit('change', { prop: 'periodParam', value: $event })"
          :options="paramPeriods"
          :title="translate('ScheduledReports_ReportPeriod')"
        >
          <template v-slot:inline-help>
            <div
              id="emailReportPeriodInlineHelp"
              class="inline-help-node"
            >
              {{ translate('ScheduledReports_ScheduleReportPeriodHelp') }}
              <br /><br />
              {{ translate('ScheduledReports_ScheduleReportPeriodHelp2') }}
            </div>
          </template>
        </Field>
      </div>
      <div>
        <Field
          uicontrol="select"
          name="report_hour"
          :model-value="report.hour"
          @update:model-value="$emit('change', { prop: 'hour', value: $event })"
          :title="translate('ScheduledReports_ReportHourLocal')"
          :options="reportHours"
        >
          <template v-slot:inline-help>
            <div
              id="reportHourHelpText"
              class="inline-help-node"
              v-if="timezoneOffset !== 0 && timezoneOffset !== '0'"
            >
              <span v-text="reportHourUtcHelpText" />
            </div>
          </template>
        </Field>
      </div>
      <div>
        <Field
          uicontrol="select"
          name="report_type"
          :disabled="reportTypes.length === 1"
          :model-value="report.type"
          @update:model-value="$emit('change', { prop: 'type', value: $event })"
          :title="translate('ScheduledReports_ReportType')"
          :options="reportTypeOptions"
        >
        <template v-slot:inline-help>
          <div id="deliveryMediumnInlineHelp" class="inline-help-node">
          <span
            v-html="$sanitize(getDeliveryMediumInlineTooltip)"
          >
          </span>
          </div>
        </template>
        </Field>
      </div>
      <div ref="reportParameters">
        <slot name="report-parameters"></slot>
      </div>
      <div
        v-for="(reportFormats, reportType) in reportFormatsByReportTypeOptions"
        :key="reportType"
      >
        <Field
          uicontrol="select"
          name="report_format"
          :title="translate('ScheduledReports_ReportFormat')"
          :class="reportType"
          v-show="report.type === reportType"
          :model-value="report[`format${reportType}`]"
          @update:model-value="$emit('change', { prop: `format${reportType}`, value: $event })"
          :options="reportFormats"
        >
        </Field>
      </div>
      <div
        v-show="report[`format${report.type}`] === 'pdf' ||
                report[`format${report.type}`] === 'html'
               "
      >
        <div :class="report.type">
          <Field
            uicontrol="select"
            name="display_format"
            :model-value="report.displayFormat"
            @update:model-value="$emit('change', { prop: 'displayFormat', value: $event })"
            :options="displayFormats"
            :introduction="translate('ScheduledReports_AggregateReportsFormat')"
          >
          </Field>
        </div>
        <div class="report_evolution_graph">
          <Field
            uicontrol="checkbox"
            name="report_evolution_graph"
            :title="translate('ScheduledReports_EvolutionGraph', 5)"
            v-show="[2, '2', 3, '3'].indexOf(report.displayFormat) !== -1"
            :model-value="report.evolutionGraph"
            @update:model-value="$emit('change', { prop: 'evolutionGraph', value: $event })"
          >
          </Field>
        </div>
        <div
          class="row evolution-graph-period"
          v-show="[1, '1', 2, '2', 3, '3'].indexOf(report.displayFormat) !== -1"
        >
          <div class="col s12">
            <label for="report_evolution_period_for_each">
              <input
                id="report_evolution_period_for_each"
                name="report_evolution_period_for"
                type="radio"
                value="each"
                :checked="report.evolutionPeriodFor === 'each'"
                @change="$emit(
                  'change',
                  { prop: 'evolutionPeriodFor', value: $event.target.value },
                )"
              />
              <span v-html="$sanitize(evolutionGraphsShowForEachInPeriod)"></span>
            </label>
          </div>
          <div class="col s12">
            <label for="report_evolution_period_for_prev">
              <input
                id="report_evolution_period_for_prev"
                name="report_evolution_period_for"
                type="radio"
                value="prev"
                :checked="report.evolutionPeriodFor === 'prev'"
                @change="$emit(
                  'change',
                  { prop: 'evolutionPeriodFor', value: $event.target.value },
                )"
              />
              <span>{{ translate(
                'ScheduledReports_EvolutionGraphsShowForPreviousN',
                frequencyPeriodPlural,
              ) }}:
                <input
                  type="number"
                  name="report_evolution_period_n"
                  :value="report.evolutionPeriodN"
                  @keydown="onEvolutionPeriodN($event)"
                  @change="onEvolutionPeriodN($event)"
                />
              </span>
            </label>
          </div>
        </div>
      </div>
      <div class="row">
        <h3 class="col s12">{{ translate('ScheduledReports_ReportsIncluded') }}</h3>
      </div>
      <div
        name="reportsList"
        :class="`row ${reportType}`"
        v-show="report.type === reportType"
        v-for="(reportColumns, reportType) in reportsByCategoryByReportTypeInColumns"
        :key="reportType"
      >
        <div class="col s12 m6" v-for="(reportsByCategory, index) in reportColumns" :key="index">
          <div v-for="(reports, category) in reportsByCategory" :key="category">
            <h3 class="reportCategory">{{ category }}</h3>
            <ul class="listReports">
              <li v-for="report in reports" :key="report.uniqueId">
                <label>
                  <input
                    :name="`${reportType}Reports`"
                    :type="allowMultipleReportsByReportType[reportType] ? 'checkbox' : 'radio'"
                    :id="`${reportType}${report.uniqueId}`"
                    :checked="selectedReports[reportType]?.[report.uniqueId]"
                    @change="$emit('toggleSelectedReport', {
                      reportType,
                      uniqueId: report.uniqueId,
                    })"
                  />
                  <span>{{ decode(report.name) }}</span>
                  <div class="entityInlineHelp" v-if="report.uniqueId === 'MultiSites_getAll'">
                    {{ translate('ScheduledReports_ReportIncludeNWebsites', countWebsites) }}
                  </div>
                </label>
              </li>
            </ul>
            <br />
          </div>
        </div>
      </div>
      <SelectedReportsList
        :reports="selectedReportsForCurrentType"
        :enabled="allowMultipleReportsByReportType[report.type]"
        @reorder="onSelectedReportsReorder"
      />
      <SaveButton
        :value="saveButtonTitle"
        @confirm="$emit('submit')"
      />
      <div
        class="entityCancel"
        v-html="$sanitize(entityCancelText)"
      >
      </div>
    </form>
  </ContentBlock>
</template>

<script lang="ts">
import {
  defineComponent,
  onMounted,
  ref,
  watch,
} from 'vue';
import {
  ContentBlock,
  Matomo,
  translate,
  debounce,
  externalLink,
} from 'CoreHome';
import { Field, Form, SaveButton } from 'CorePluginsAdmin';
import { adjustHourToTimezone } from '../utilities';
import SelectedReportsList from './SelectedReportsList.vue';

interface Option {
  key: string;
  value: string;
}

interface ReportMetadata {
  uniqueId: string;
  name: string;
}

interface ReportsByType {
  [reportType: string]: Record<string, ReportMetadata[]>;
}

interface ReportsLookupByType {
  [reportType: string]: Record<string, ReportMetadata>;
}

export default defineComponent({
  props: {
    report: {
      type: Object,
      required: true,
    },
    selectedReports: Object,
    selectedReportsOrder: {
      type: Object,
      default: () => ({}),
    },
    paramPeriods: {
      type: Object,
      required: true,
    },
    reportTypeOptions: {
      type: Object,
      required: true,
    },
    reportFormatsByReportTypeOptions: {
      type: Object,
      required: true,
    },
    displayFormats: {
      type: Object,
      required: true,
    },
    reportsByCategoryByReportType: {
      type: Object,
      required: true,
    },
    allowMultipleReportsByReportType: {
      type: Object,
      required: true,
    },
    countWebsites: {
      type: Number,
      required: true,
    },
    siteName: {
      type: String,
      required: true,
    },
    reportTypes: {
      type: Object,
      required: true,
    },
    segmentEditorActivated: Boolean,
    savedSegmentsById: Object,
    periods: {
      type: Object,
      required: true,
    },
  },
  emits: ['submit', 'change', 'toggleSelectedReport', 'reorderSelectedReports'],
  components: {
    ContentBlock,
    Field,
    SaveButton,
    SelectedReportsList,
  },
  directives: {
    Form,
  },
  created() {
    this.onEvolutionPeriodN = debounce(this.onEvolutionPeriodN, 50);
  },
  methods: {
    onEvolutionPeriodN(event: Event) {
      this.$emit('change', {
        prop: 'evolutionPeriodN',
        value: (event.target as HTMLInputElement).value,
      });
    },
    decode(s: string) {
      // report names can be encoded (mainly goals)
      return Matomo.helper.htmlDecode(s);
    },
    onSelectedReportsReorder(order: string[]) {
      if (!this.report || !this.report.type) {
        return;
      }

      this.$emit('reorderSelectedReports', {
        reportType: this.report.type,
        order,
      });
    },
  },
  setup(props, ctx) {
    const reportParameters = ref<HTMLElement|null>(null);

    watch(() => props.report, (newValue) => {
      const reportParametersElement = reportParameters.value as HTMLElement;
      reportParametersElement.querySelectorAll('[vue-entry]').forEach((node) => {
        // eslint-disable-next-line no-underscore-dangle
        $(node).data('vueAppInstance').report_ = newValue;
      });
    });

    onMounted(() => {
      const reportParametersElement = reportParameters.value as HTMLElement;
      Matomo.helper.compileVueEntryComponents(reportParametersElement, {
        report: props.report,
        onChange(prop: string, value: unknown) {
          ctx.emit('change', { prop, value });
        },
      });
    });

    return {
      reportParameters,
    };
  },
  beforeUnmount() {
    const reportParameters = this.$refs.reportParameters as HTMLElement;
    Matomo.helper.destroyVueComponent(reportParameters);
  },
  computed: {
    enforceSelectedReportOrder(): boolean {
      const parameters = (this.report?.parameters || {}) as { enforceOrder?: boolean };
      if (typeof parameters.enforceOrder !== 'undefined') {
        return !!parameters.enforceOrder;
      }

      return false;
    },

    /**
     * Ensures each report type has a flattened order array where every selected report
     * appears exactly once (ordered first, then any remaining selections).
     */
    selectedReportsOrderNormalized(): Record<string, string[]> {
      const normalized: Record<string, string[]> = {};
      const allSelectedReports = this.selectedReports || {};
      Object.keys(allSelectedReports).forEach((reportType) => {
        const selectedForType = allSelectedReports[reportType] || {};
        const ordered = ((this.selectedReportsOrder || {})[reportType] || [])
          .filter((uniqueId: string) => selectedForType[uniqueId]);
        const remaining = Object.keys(selectedForType).filter(
          (uniqueId) => selectedForType[uniqueId] && ordered.indexOf(uniqueId) === -1,
        );
        normalized[reportType] = ordered.concat(remaining);
      });
      return normalized;
    },

    /**
     * Flattens the nested report metadata into a two-level lookup so we can access any report
     * by its type and unique id without re-iterating the category structure.
     */
    reportsLookup(): ReportsLookupByType {
      const reportsByType = this.reportsByCategoryByReportType as ReportsByType;
      const lookup: ReportsLookupByType = {};

      Object.entries(reportsByType).forEach(([reportType, reportsByCategory]) => {
        lookup[reportType] = lookup[reportType] || {};
        Object.values(reportsByCategory).forEach((reports) => {
          reports.forEach((report) => {
            lookup[reportType][report.uniqueId] = report;
          });
        });
      });

      return lookup;
    },
    selectedReportsForCurrentType(): ReportMetadata[] {
      const type = this.report?.type as string;
      if (!type) {
        return [];
      }

      const selectedForType = (this.selectedReports || {})[type] || {};
      let order: string[] = [];
      if (this.enforceSelectedReportOrder) {
        order = this.selectedReportsOrderNormalized[type] || [];
      } else {
        const reportsByCategory = (this.reportsByCategoryByReportType as ReportsByType)[type] || {};
        const ordered: string[] = [];
        Object.values(reportsByCategory).forEach((reports) => {
          reports.forEach((report) => {
            if (selectedForType[report.uniqueId]) {
              ordered.push(report.uniqueId);
            }
          });
        });
        order = ordered;
      }

      if (!order.length) {
        order = Object.keys(selectedForType).filter((uniqueId) => selectedForType[uniqueId]);
      }

      if (!order.length) {
        return [];
      }

      const lookup = this.reportsLookup[type] || {};

      return order
        .map((uniqueId) => lookup[uniqueId])
        .filter((report): report is ReportMetadata => !!report);
    },
    reportsByCategoryByReportTypeInColumns() {
      const reportsByCategoryByReportType = this.reportsByCategoryByReportType as
        Record<string, Record<string, unknown[]>>;

      const inColumns = Object.entries(reportsByCategoryByReportType).map(
        ([key, reportsByCategory]) => {
          const newColumnAfter = Math.floor((Object.keys(reportsByCategory).length + 1) / 2);

          const column1: Record<string, unknown[]> = {};
          const column2: Record<string, unknown[]> = {};

          let currentColumn = column1;
          Object.entries(reportsByCategory).forEach(([category, reports]) => {
            currentColumn[category] = reports;

            if (Object.keys(currentColumn).length >= newColumnAfter) {
              currentColumn = column2;
            }
          });

          return [key, [column1, column2]];
        },
      );

      return Object.fromEntries(inColumns);
    },
    entityCancelText() {
      return translate(
        'General_OrCancel',
        '<a class="entityCancelLink">',
        '</a>',
      );
    },
    frequencyPeriodSingle() {
      if (!this.report || !this.report.period) {
        return '';
      }

      const { ReportPlugin } = window;

      let translation = ReportPlugin.periodTranslations[this.report.period];
      if (!translation) {
        translation = ReportPlugin.periodTranslations.day;
      }
      return translation.single;
    },
    frequencyPeriodPlural() {
      if (!this.report || !this.report.period) {
        return '';
      }

      const { ReportPlugin } = window;

      let translation = ReportPlugin.periodTranslations[this.report.period];
      if (!translation) {
        translation = ReportPlugin.periodTranslations.day;
      }
      return translation.plural;
    },
    evolutionGraphsShowForEachInPeriod() {
      return translate(
        'ScheduledReports_EvolutionGraphsShowForEachInPeriod',
        '<strong>',
        '</strong>',
        this.frequencyPeriodSingle,
      );
    },
    reportSegmentInlineHelp() {
      return translate(
        'ScheduledReports_Segment_HelpScheduledReport',
        '<a href="./" rel="noreferrer noopener" target="_blank">',
        '</a>',
        translate('SegmentEditor_DefaultAllVisits'),
        translate('SegmentEditor_AddNewSegment'),
      );
    },
    timezoneOffset() {
      return Matomo.timezoneOffset;
    },
    timeZoneDifferenceInHours() {
      return Matomo.timezoneOffset / 3600;
    },
    reportHours() {
      const hours: Option[] = [];
      const fractionalOffset = ((this.timeZoneDifferenceInHours % 1) + 1) % 1;
      const minutePart = Math.round(fractionalOffset * 60);
      const minuteLabel = `${minutePart}`.padStart(2, '0');

      for (let i = 0; i < 24; i += 1) {
        const paddedHour = `${i}`.padStart(2, '0');
        const key = fractionalOffset === 0 ? `${i}` : `${i + fractionalOffset}`;
        const value = fractionalOffset === 0 ? `${paddedHour}:00` : `${paddedHour}:${minuteLabel}`;
        hours.push({
          key,
          value,
        });
      }
      return hours;
    },
    reportHourUtc() {
      const reportHour = adjustHourToTimezone(
        this.report.hour as string,
        -this.timeZoneDifferenceInHours,
      );
      const normalized = ((parseFloat(reportHour) % 24) + 24) % 24;
      const roundedHour = Math.round(normalized) % 24;
      return `${roundedHour}`.padStart(2, '0');
    },
    reportHourUtcLabel() {
      return translate('ScheduledReports_ReportHourWithUtcOnly', [`${this.reportHourUtc}:00`]);
    },
    reportHourUtcHelpText() {
      return `${translate('ScheduledReports_ReportWillBeSentAt')} `
        + `${translate('ScheduledReports_ReportHourEqualsUtc', [this.reportHourUtcLabel])} `
        + `${translate('ScheduledReports_NoteDeliveryTime')}`;
    },
    saveButtonTitle() {
      const { ReportPlugin } = window;

      const isEditing = this.report.idreport > 0;
      return isEditing ? ReportPlugin.updateReportString : ReportPlugin.createReportString;
    },
    contentTitle() {
      const { ReportPlugin } = window;

      const isEditing = this.report.idreport > 0;
      return isEditing ? ReportPlugin.updateReportString : translate('ScheduledReports_CreateAndScheduleReport');
    },
    getDeliveryMediumInlineTooltip(): string {
      const link = translate(
        'CoreHome_LearnMoreFullStop',
        externalLink('https://matomo.org/faq/general/create-and-schedule-a-report/'),
        '</a>',
      );
      return `${translate('ScheduledReports_CreateTooltip')} ${link}`;
    },
  },
});
</script>