# Forms Generator Cookbook

# Start/End datetime validation

There can be different validation cases for start/end datetimes for different customers. This example case is to validate the dates as the following criteria:

  • Dates should not be in the future
  • Start date should be earlier than the end date
  • Difference between end date and start date should not be bigger than a value fetched from database.
import { computed } from '@vue/composition-api'
import { differenceInCalendarDays, differenceInHours, isFuture } from 'date-fns'
import { fireEvent, query, useParamsStore } from '@pitcher/core'
import { useFormGenerator } from '@pitcher/forms-generator'

export async function addDatetimesValidator() {
  const params = useParamsStore().state
  const { formState, addValidator } = useFormGenerator()
  const startAsDate = computed(() => new Date(formState.value.startDatetime))
  const endAsDate = computed(() => new Date(formState.value.endDatetime))

  const maxDiffDays = (await query(`
    SELECT 
      Submit_Days_Limit__c
    FROM tbl_crm_interaction_country_mappings
    WHERE Country__c = '${params.user?.Country__c}' 
    LIMIT 1`,
    'pitcher',
    false,
    'modal'
  )) || 14

  addValidator(() => {
    if (isFuture(endAsDate.value)) {
      fireEvent('showAlertBox', {
        title: $t('Dates are invalid'),
        message: $t('It is not possible to submit a call in the future'),
      })

      return false
    }

    if (differenceInHours(endAsDate.value, startAsDate.value) < 0) {
      fireEvent('showAlertBox', {
        title: $t('Dates are invalid'),
        message: $t('Start date should be earlier than the end date'),
      })

      return false
    }

    if (differenceInCalendarDays(endAsDate.value, startAsDate.value) > maxDiffDays) {
      fireEvent('showAlertBox', {
        title: $t('Dates are invalid'),
        message: $t('It is not possible to submit a call bigger than 2 weeks'),
      })

      return false
    }

    return true
  })
}

# Fields prefilled by certain keywords from session contents (Products Discussed)

Altough for most of the cases providing PForm components and correct options to upsertItem would be enough, there are some cases where we need to use the power of JavaScript to create correct behaviour. Especially when we need to do some async logic.

In this example we'll create a Product field that uses the presented content keywords to prefill its value.

// src/use/useProductsField.js
import { computed, reactive, watch } from '@vue/composition-api'
import { useFormRef } from '@pitcher/forms-generator'
import { useKeywords } from '@pitcher/forms'

// @pitcher/core async-store for products
import { useAllProducts } from '@/store/products'

export function useProductsField() {
  const { keywords, getKeywordValuesFor } = useKeywords()
  const selectedProducts = useFormRef('product', [])
  const { status: productsStatus, data: products } = toRefs(useAllProducts().state)

  watch([productsStatus, keywords], ([status, kwds]) => {
    if (status === 'success' && kwds.length > 0) {
      const keywordsForProducts = getKeywordValuesFor('Product', products.value)

      if (keywordsForProducts.length > 0) {
        selectedProducts.value = keywordsForProducts.map(k => k.Id)
      }
    }
  })

  return {
    id: 'product', // this field id is the same as `const selectedProducts = useFormRef('product', '')`
    component: 'PFormSelect',
    props: reactive({ // if props change, making them reactive would be a good idea
      title: $t('Product'),
      submissionParam: 'product',
      precallField: 'product',
      items: computed(() => products.value || []),
      returnObject: false,
      multiple: true
    }),
  }
}

// src/postcallFormConfig/initPostcall.js
import { useFormGenerator } from '@pitcher/forms-generator'

import { getPostcallItems } from './postcallItems'
import { useProductsField } from '@/use/useProductsField'

export function initPostcall() {
  const { onLoad, pluginItem, upsertItem } = useFormGenerator()

  // ...

  upsertItem(useProductsField())

  // ...
}

# Photo attachment

Even JavaScript wouldn't be enough for some cases, especially for the parts @pitcher/forms components don't cover. For those cases we can create custom vue components to use.

// src/components/PhotoAttachment
<template>
  <PLabelledFormItem title="Photo">
    <div>
      <VBtn @click="onAddPhotoClick">Add photo</VBtn>
      <div v-for="photo in photos" :key="photo.url" class="flex-column pa-4">
        <VImg :src="photo.url" height="100" width="100" />
        <VBtn class="mt-2" @click="removePhoto(photo)">Remove photo</VBtn>
      </div>
    </div>
  </PLabelledFormItem>
</template>

<script>
import { PLabelledFormItem, useWithForm } from '@pitcher/forms'
import { addPhoto, toDataURL } from '@/utils/photo'
import { defineComponent, ref } from '@vue/composition-api'

export default defineComponent({
  name: 'PhotoAttachment',
  components: { PLabelledFormItem },
  setup() {
    const photos = ref([])

    function onAddPhotoClick() {
      addPhoto(async (path) => {
        const url = path
        const dataURL = await toDataURL(path)

        photos.value.push({
          url,
          dataURL,
        })
      })
    }

    function removePhoto(photo) {
      photos.value = photos.value.filter((p) => p.url !== photo.url)
    }

    useWithForm(
      () => ({
        photos: photos.value.map((p) => p.dataURL),
      }),
      {
        id: 'photos',
        ref: photos,
      }
    )

    return {
      photos,
      onAddPhotoClick,
      removePhoto,
    }
  },
})
</script>
// src/utils/photo

import { fireEvent } from '@pitcher/core'

const openPhotoGallery = async () => {
  const name = `photo-${Date.now()}.jpg`

  await fireEvent('openPhotoGallery', {
    caller: 'html5',
    allowEditing: false,
    maxWidth: 1024,
    maxHeight: 1024,
    source: 'modal',
    targetPhoto: name,
  })
}

const getCameraPhoto = async () => {
  const name = `photo-${Date.now()}.jpg`

  await fireEvent('getCameraPhoto', {
    caller: 'html5',
    allowEditing: false,
    source: 'modal',
    maxWidth: 1024,
    maxHeight: 1024,
    targetPhoto: name,
    saveToPhotoGallery: true,
  })
}

export const addPhoto = async (callback) => {
  window.gotPhoto = (path, old) => {
    callback && callback(path, old)
  }

  if (!window.openPhotoGallery) {
    window.openPhotoGallery = () => {
      openPhotoGallery()
    }
  }

  if (!window.getCameraPhoto) {
    window.getCameraPhoto = () => {
      getCameraPhoto()
    }
  }

  await fireEvent('showConfirmBox', {
    title: '',
    message: $t('Please choose your data source'),
    confirm: 'getCameraPhoto',
    reject: 'openPhotoGallery',
    yesTitle: $t('Camera'),
    noTitle: $t('Library'),
  })
}

export function toDataURL(src, outputFormat = 'png') {
  return new Promise((resolve) => {
    const img = new Image()

    img.crossOrigin = 'Anonymous'
    img.onload = function() {
      const canvas = document.createElement('CANVAS')
      const ctx = canvas.getContext('2d')

      canvas.height = this.naturalHeight
      canvas.width = this.naturalWidth
      ctx.drawImage(this, 0, 0)
      const dataURL = canvas.toDataURL(outputFormat)

      resolve(dataURL)
    }
    img.src = src
    if (img.complete || img.complete === undefined) {
      img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
      img.src = src
    }
  })
}
// src/postcallFormConfig/initPostcall.js
import { useFormGenerator } from '@pitcher/forms-generator'

import { getPostcallItems } from './postcallItems'
import PhotoAttachment from '@/components/PhotoAttachment'

export function initPostcall() {
  const { onLoad, pluginItem, upsertItem } = useFormGenerator()

  // ...

  upsertItem({
    id: 'PhotoAttachment',
    weight: 600,
    component: PhotoAttachment,
  })
  // must be called as last
  setTimeout(() => {
    onLoad()
  }, 1000)
}

# Fields depending on each other

# Handling by itemsFetcher.query parameters

upsertItem({
  id: 'discussionItems',
  component: 'PFormSelect',
  itemsFetcher: {
    query: 'SELECT * FROM tbl_crm_ditems_PIT_v1',
  },
  defaultValue: '',
  props: {
    submissionParam: 'discussionItem',
    itemValue: 'Id',
    itemText: 'Name',
    title: $t('Discussion Items'),
    returnObject: false,
  },
})

upsertItem({
  id: 'callKeyMessages',
  component: 'PFormSelect',
  itemsFetcher: {
    query: 'SELECT * FROM tbl_crm_ccm_PIT_v1 WHERE Parent_Discussion_Item__c = {{ discussionItems }}', // Here the query for call key messages will be filtered by the selected value of `discussionItems`
  },
  visible: 'discussionItems', // this field will only be visible when `discussionItems` selected
  props: {
    submissionParam: 'callKeyMessage',
    precallField: 'callKeyMessage',
    itemValue: 'Id',
    itemText: 'Name',
    title: $t('Call Key Messages'),
    returnObject: false,
  },
})

# Handling it on the JS side

upsertItem({
  id: 'discussionItems',
  component: 'PFormSelect',
  itemsFetcher: {
    query: 'SELECT * FROM tbl_crm_ditems_PIT_v1',
  },
  defaultValue: '',
  props: {
    submissionParam: 'discussionItem',
    itemValue: 'Id',
    itemText: 'Name',
    title: $t('Discussion Items'),
    returnObject: false,
  },
})

function useCallKeyMessages() {
  const discussionItems = useFormRef('callKeyMessages', [])
  const callKeyMessages = useFormRef('callKeyMessages', [])
  const callKeyMessageItems = ref([]) // don't use useFormRef if that variable is not going to be sent with form

  watch(discussionItems, async () => {
    callKeyMessages.value = [] // lets say we want to clear the callKeyMessages selection whenever callKeyMessages changes

    callKeyMessageItems.value = await query(`SELECT * FROM tbl_crm_ccm_PIT_v1 WHERE Parent_Discussion_Item__c 
    IN ('${discussionItems.value.join("', '")}')`)
  })

  upsertItem({
    id: 'callKeyMessages',
    component: 'PFormSelect',
    visible: computed(() => discussionItems.value?.length > 0)
    props: reactive({
      items: callKeyMessageItems.value
      submissionParam: 'callKeyMessage',
      precallField: 'callKeyMessage',
      itemValue: 'Id',
      itemText: 'Name',
      title: $t('Call Key Messages'),
      returnObject: false,
    }),
  })
}