<commit_before>import { gql } from "@apollo/client"
import "@blueprintjs/core/lib/css/blueprint.css"
import { DateRangeInput } from "@blueprintjs/datetime"
import "@blueprintjs/datetime/lib/css/blueprint-datetime.css"
import { IconNames } from "@blueprintjs/icons"
import { DEFAULT_PAGE_PROPS, DEFAULT_SEARCH_PROPS } from "actions"
import API from "api"
import AppContext from "components/AppContext"
import "components/BlueprintOverrides.css"
import ButtonToggleGroup from "components/ButtonToggleGroup"
import DailyRollupChart from "components/DailyRollupChart"
import * as FieldHelper from "components/FieldHelper"
import Fieldset from "components/Fieldset"
import Messages from "components/Messages"
import MosaicLayout from "components/MosaicLayout"
import {
  mapPageDispatchersToProps,
  PageDispatchersPropType,
  useBoilerplate
} from "components/Page"
import ReportCollection, {
  FORMAT_CALENDAR,
  FORMAT_MAP,
  FORMAT_STATISTICS,
  FORMAT_SUMMARY,
  FORMAT_TABLE
} from "components/ReportCollection"
import { getSearchQuery, SearchQueryPropType } from "components/SearchFilters"
import { Field, Form, Formik } from "formik"
import { Organization, Report } from "models"
import moment from "moment"
import pluralize from "pluralize"
import PropTypes from "prop-types"
import React, { useContext, useMemo, useState } from "react"
import { Button, FormText, Modal } from "react-bootstrap"
import ContainerDimensions from "react-container-dimensions"
import { connect } from "react-redux"
import { useHistory, useLocation } from "react-router-dom"
import Settings from "settings"
import utils from "utils"

const GQL_ROLLUP_GRAPH = gql`
  query(
    $startDate: Instant!
    $endDate: Instant!
    $principalOrganizationUuid: String
    $advisorOrganizationUuid: String
    $orgType: OrganizationType
  ) {
    rollupGraph(
      startDate: $startDate
      endDate: $endDate
      principalOrganizationUuid: $principalOrganizationUuid
      advisorOrganizationUuid: $advisorOrganizationUuid
      orgType: $orgType
    ) {
      org {
        uuid
        shortName
      }
      published
      cancelled
    }
  }
`
const GQL_SHOW_ROLLUP_EMAIL = gql`
  query(
    $startDate: Instant!
    $endDate: Instant!
    $principalOrganizationUuid: String
    $advisorOrganizationUuid: String
    $orgType: OrganizationType
  ) {
    showRollupEmail(
      startDate: $startDate
      endDate: $endDate
      principalOrganizationUuid: $principalOrganizationUuid
      advisorOrganizationUuid: $advisorOrganizationUuid
      orgType: $orgType
    )
  }
`
const GQL_EMAIL_ROLLUP = gql`
  mutation(
    $startDate: Instant!
    $endDate: Instant!
    $email: AnetEmailInput!
    $principalOrganizationUuid: String
    $advisorOrganizationUuid: String
    $orgType: OrganizationType
  ) {
    emailRollup(
      startDate: $startDate
      endDate: $endDate
      email: $email
      principalOrganizationUuid: $principalOrganizationUuid
      advisorOrganizationUuid: $advisorOrganizationUuid
      orgType: $orgType
    )
  }
`

const Chart = ({
  pageDispatchers,
  rollupStart,
  rollupEnd,
  focusedOrg,
  setFocusedOrg,
  orgType
}) => {
  const variables = getVariables()
  const { loading, error, data } = API.useApiQuery(GQL_ROLLUP_GRAPH, variables)
  const { done, result } = useBoilerplate({
    loading,
    error,
    pageDispatchers
  })
  const graphData = useMemo(() => {
    if (!data) {
      return []
    }
    const pinnedOrgs = Settings.pinned_ORGs
    return data.rollupGraph
      .map(d => {
        d.org = d.org || { uuid: "-1", shortName: "Other" }
        return d
      })
      .sort((a, b) => {
        const aIndex = pinnedOrgs.indexOf(a.org.shortName)
        const bIndex = pinnedOrgs.indexOf(b.org.shortName)
        if (aIndex < 0) {
          const nameOrder = a.org.shortName.localeCompare(b.org.shortName)
          return bIndex < 0
            ? nameOrder === 0
              ? a.org.uuid - b.org.uuid
              : nameOrder
            : 1
        } else {
          return bIndex < 0 ? -1 : aIndex - bIndex
        }
      })
  }, [data])
  if (done) {
    return result
  }

  const CHART_ID = "reports_by_organization"
  const barColors = {
    cancelled: "#ec971f",
    published: "#75eb75"
  }
  const legendCss = {
    width: "14px",
    height: "14px",
    display: "inline-block"
  }

  return (

      <ContainerDimensions>
        {({ width }) => (
          <DailyRollupChart
            width={width}
            chartId={CHART_ID}
            data={graphData}
            onBarClick={setFocusedOrg}
            tooltip={d => `

${d.org.shortName}

Published: ${d.published}

Cancelled: ${d.cancelled}

Click to view details

            `}
            barColors={barColors}
          />
        )}
      </ContainerDimensions>

{" "}
        Published reports:

          {graphData.reduce((acc, org) => acc + org.published, 0)}

{" "}
        Cancelled engagements:

          {graphData.reduce((acc, org) => acc + org.cancelled, 0)}

  )

  function getVariables() {
    const variables = {
      startDate: rollupStart.valueOf(),
      endDate: rollupEnd.valueOf()
    }
    if (focusedOrg) {
      if (orgType === Organization.TYPE.PRINCIPAL_ORG) {
        variables.principalOrganizationUuid = focusedOrg.uuid
      } else {
        variables.advisorOrganizationUuid = focusedOrg.uuid
      }
    } else if (orgType) {
      variables.orgType = orgType
    }
    return variables
  }
}

Chart.propTypes = {
  pageDispatchers: PageDispatchersPropType,
  rollupStart: PropTypes.object,
  rollupEnd: PropTypes.object,
  focusedOrg: PropTypes.object,
  setFocusedOrg: PropTypes.func,
  orgType: PropTypes.string
}

const Collection = ({ queryParams }) => (

    <ReportCollection
      paginationKey="r_rollup"
      queryParams={queryParams}
      viewFormats={[
        FORMAT_SUMMARY,
        FORMAT_TABLE,
        FORMAT_STATISTICS,
        FORMAT_CALENDAR
      ]}
    />

)

Collection.propTypes = {
  queryParams: PropTypes.object
}

const Map = ({ queryParams }) => (

    <ContainerDimensions>
      {({ width, height }) => (
        <ReportCollection
          queryParams={queryParams}
          width={width}
          height={height}
          marginBottom={0}
          viewFormats={[FORMAT_MAP]}
        />
      )}
    </ContainerDimensions>

)

Map.propTypes = {
  queryParams: PropTypes.object
}

const RollupShow = ({ pageDispatchers, searchQuery }) => {
  const { appSettings } = useContext(AppContext)
  const history = useHistory()
  const routerLocation = useLocation()
  const { startDate, endDate } = getDateRangeFromQS(routerLocation.search)
  const [showEmailModal, setShowEmailModal] = useState(false)
  const [orgType, setOrgType] = useState(Organization.TYPE.ADVISOR_ORG)
  const [focusedOrg, setFocusedOrg] = useState(null)
  const [saveSuccess, setSaveSuccess] = useState(null)
  const [saveError, setSaveError] = useState(null)
  const previewPlaceholderUrl = "/help"
  useBoilerplate({
    pageProps: DEFAULT_PAGE_PROPS,
    searchProps: DEFAULT_SEARCH_PROPS,
    pageDispatchers
  })

  const VISUALIZATIONS = [
    {
      id: "rbdow-chart",
      icons: [IconNames.GROUPED_BAR_CHART],
      title: "Chart by organization",
      renderer: renderChart
    },
    {
      id: "rbdow-collection",
      icons: [IconNames.PANEL_TABLE],
      title: "Reports by organization",
      renderer: renderReportCollection
    },
    {
      id: "rbdow-map",
      icons: [IconNames.MAP],
      title: "Map by organization",
      renderer: renderReportMap
    }
  ]
  const INITIAL_LAYOUT = VISUALIZATIONS[1].id
  const DESCRIPTION = "Number of reports released per organization."
  const flexStyle = {
    display: "flex",
    flexDirection: "column",
    flex: "1 1 auto",
    height: "100%",
    overflow: "auto"
  }
  const fieldsetStyle = {
    height: "100%",
    overflow: "auto",
    display: "flex",
    flexDirection: "column"
  }
  const mosaicLayoutStyle = {
    display: "flex",
    flex: "1 1 auto",
    height: "100%"
  }
  const inputFormat = Settings.dateFormats.forms.input.date[0]
  const style = { width: "7em", fontSize: "1em" }

  return (

      <Messages error={saveError} success={saveSuccess} />

      <Fieldset
        title={

                Rollup
                {focusedOrg && ` for ${focusedOrg.shortName}`}

              <DateRangeInput
                className="rollupDateRange"
                startInputProps={{ style }}
                endInputProps={{ style }}
                value={[startDate.toDate(), endDate.toDate()]}
                onChange={changeRollupDate}
                formatDate={date => moment(date).format(inputFormat)}
                parseDate={str =>
                  moment(
                    str,
                    Settings.dateFormats.forms.input.date,
                    true
                  ).toDate()
                }
                placeholder={inputFormat}
                maxDate={moment().toDate()}
                allowSingleDayRange
                closeOnSelection={false}
                contiguousCalendarMonths={false}
                shortcuts
              />

            {focusedOrg ? (
              <Button
                onClick={() => setFocusedOrg(null)}
                variant="outline-secondary"
              >
                All organizations
              </Button>
            ) : (
              <ButtonToggleGroup value={orgType} onChange={setOrgType}>
                <Button
                  value={Organization.TYPE.ADVISOR_ORG}
                  variant="outline-secondary"
                >
                  {pluralize(Settings.fields.advisor.org.name)}
                </Button>
                <Button
                  value={Organization.TYPE.PRINCIPAL_ORG}
                  variant="outline-secondary"
                >
                  {pluralize(Settings.fields.principal.org.name)}
                </Button>
              </ButtonToggleGroup>
            )}

        }
        action={

            <Button
              id="print-rollup"
              href={previewPlaceholderUrl}
              target="rollup"
              onClick={printPreview}
              variant="outline-secondary"
            >
              Print
            </Button>
            <Button
              id="email-rollup"
              onClick={toggleEmailModal}
              variant="primary"
            >
              Email rollup
            </Button>

        }
        style={fieldsetStyle}
      >
        <MosaicLayout
          style={mosaicLayoutStyle}
          visualizations={VISUALIZATIONS}
          initialNode={INITIAL_LAYOUT}
          description={DESCRIPTION}
        />
      </Fieldset>
      <Formik
        enableReinitialize
        onSubmit={onSubmitEmailRollup}
        initialValues={{ to: "", comment: "" }}
      >
        {formikProps => renderEmailModal(formikProps)}
      </Formik>

  )

  function renderChart(id) {
    return (
      <Chart
        pageDispatchers={pageDispatchers}
        rollupStart={getRollupStart()}
        rollupEnd={getRollupEnd()}
        focusedOrg={focusedOrg}
        setFocusedOrg={setFocusedOrg}
        orgType={orgType}
      />
    )
  }

  function renderReportCollection(id) {
    return <Collection queryParams={getQueryParams()} />
  }

  function renderReportMap(id) {
    return <Map queryParams={getQueryParams()} />
  }

  function getQueryParams() {
    const sqParams = getSearchQuery(searchQuery)
    const maxReportAge =
      1 + (parseInt(appSettings.DAILY_ROLLUP_MAX_REPORT_AGE_DAYS, 10) || 14)
    const reportsQueryParams = {
      state: [Report.STATE.PUBLISHED], // Specifically excluding cancelled engagements.
      releasedAtStart: getRollupStart().valueOf(),
      releasedAtEnd: getRollupEnd().valueOf(),
      engagementDateStart: moment(getRollupStart())
        .subtract(maxReportAge, "days")
        .valueOf(),
      sortBy: "ENGAGEMENT_DATE",
      sortOrder: "DESC",
      ...sqParams
    }
    if (focusedOrg) {
      if (orgType === Organization.TYPE.PRINCIPAL_ORG) {
        reportsQueryParams.principalOrgUuid = focusedOrg.uuid
        reportsQueryParams.includePrincipalOrgChildren = true
      } else {
        reportsQueryParams.advisorOrgUuid = focusedOrg.uuid
        reportsQueryParams.includeAdvisorOrgChildren = true
      }
    }
    return reportsQueryParams
  }

  function getDateStr() {
    if (startDate.isSame(endDate, "day")) {
      return `for ${startDate.format(
        Settings.dateFormats.forms.displayShort.date
      )}`
    } else {
      return `from ${startDate.format(
        Settings.dateFormats.forms.displayShort.date
      )} to ${endDate.format(Settings.dateFormats.forms.displayShort.date)}`
    }
  }

  function getRollupStart() {
    return moment(startDate).startOf("day")
  }

  function getRollupEnd() {
    return moment(endDate).endOf("day")
  }

  function getDateOrDefault(qsDate) {
    return qsDate ? moment(+qsDate) : moment().subtract(1, "day") // default to yesterday
  }

  function getDateRangeFromQS(search) {
    // Having a qs with ?date=… overrides startDate and endDate (for backwards compatibility)
    const qs = utils.parseQueryString(search)
    return {
      startDate: getDateOrDefault(qs.date || qs.startDate),
      endDate: getDateOrDefault(qs.date || qs.endDate)
    }
  }

  function changeRollupDate(dateRange) {
    const startDate = dateRange[0] && dateRange[0].valueOf()
    const endDate = dateRange[1] && dateRange[1].valueOf()
    if (!startDate || !endDate) {
      return
    }
    history.replace({
      pathname: "rollup",
      search: utils.formatQueryString({
        startDate,
        endDate
      })
    })
  }

  function renderEmailModal(formikProps) {
    const { isSubmitting, submitForm } = formikProps
    return (
      <Modal centered show={showEmailModal} onHide={toggleEmailModal}>
        <Form>
          <Modal.Header closeButton>
            <Modal.Title>Email rollup - {getDateStr()}</Modal.Title>
          </Modal.Header>
          <Modal.Body>

              {focusedOrg
                ? `Reports for ${focusedOrg.shortName}`
                : `All reports by ${orgType.replace("_", " ").toLowerCase()}`}

            <Field
              name="to"
              component={FieldHelper.InputField}
              validate={email => handleEmailValidation(email)}
              vertical
            >
              <FormText>
                One or more email addresses, comma separated, e.g.:

                  jane@nowhere.invalid, John Doe <john@example.org>, "Mr.
                  X" <x@example.org>

              </FormText>
            </Field>
            <Field
              name="comment"
              component={FieldHelper.InputField}
              asA="textarea"
              vertical
            />
          </Modal.Body>
          <Modal.Footer>
            <Button
              id="preview-rollup-email"
              href={previewPlaceholderUrl}
              target="rollup"
              onClick={showPreview}
              variant="outline-secondary"
            >
              Preview
            </Button>
            <Button
              id="send-rollup-email"
              variant="primary"
              onClick={submitForm}
              disabled={isSubmitting}
            >
              Send email
            </Button>
          </Modal.Footer>
        </Form>
      </Modal>
    )
  }

  function handleEmailValidation(value) {
    const r = utils.parseEmailAddresses(value)
    return r.isValid ? null : r.message
  }

  function toggleEmailModal() {
    setShowEmailModal(!showEmailModal)
  }

  function printPreview() {
    showPreview(true)
  }

  function showPreview(print) {
    const variables = {
      startDate: getRollupStart().valueOf(),
      endDate: getRollupEnd().valueOf()
    }
    if (focusedOrg) {
      if (orgType === Organization.TYPE.PRINCIPAL_ORG) {
        variables.principalOrganizationUuid = focusedOrg.uuid
      } else {
        variables.advisorOrganizationUuid = focusedOrg.uuid
      }
    }
    if (orgType) {
      variables.orgType = orgType
    }
    return API.query(GQL_SHOW_ROLLUP_EMAIL, variables).then(data => {
      const rollupWindow = window.open("", "rollup")
      const doc = rollupWindow.document
      doc.clear()
      doc.open()
      doc.write(data.showRollupEmail)
      doc.close()
      if (print === true) {
        rollupWindow.print()
      }
    })
  }

  function onSubmitEmailRollup(values, form) {
    emailRollup(values, form)
      .then(response => onSubmitEmailRollupSuccess(response, values, form))
      .catch(error => {
        setSaveSuccess(null)
        setSaveError(error)
        setShowEmailModal(false)
        form.setSubmitting(false)
      })
  }

  function onSubmitEmailRollupSuccess(response, values, form) {
    setSaveSuccess("Email successfully sent")
    setSaveError(null)
    setShowEmailModal(false)
    form.resetForm() // Reset the email modal field values
  }

  function emailRollup(values, form) {
    const r = utils.parseEmailAddresses(values.to)
    if (!r.isValid) {
      return
    }
    const emailDelivery = {
      toAddresses: r.to,
      comment: values.comment
    }
    const variables = {
      startDate: getRollupStart().valueOf(),
      endDate: getRollupEnd().valueOf(),
      email: emailDelivery
    }
    if (focusedOrg) {
      if (orgType === Organization.TYPE.PRINCIPAL_ORG) {
        variables.principalOrganizationUuid = focusedOrg.uuid
      } else {
        variables.advisorOrganizationUuid = focusedOrg.uuid
      }
    }
    if (orgType) {
      variables.orgType = orgType
    }
    return API.mutation(GQL_EMAIL_ROLLUP, variables)
  }
}

RollupShow.propTypes = {
  searchQuery: SearchQueryPropType,
  pageDispatchers: PageDispatchersPropType
}

const mapStateToProps = (state, ownProps) => ({
  searchQuery: state.searchQuery
})

export default connect(mapStateToProps, mapPageDispatchersToProps)(RollupShow)
<commit_msg>AB#265: Remove chart from the rollup page<commit_after>import { gql } from "@apollo/client"
import "@blueprintjs/core/lib/css/blueprint.css"
import { DateRangeInput } from "@blueprintjs/datetime"
import "@blueprintjs/datetime/lib/css/blueprint-datetime.css"
import { IconNames } from "@blueprintjs/icons"
import { DEFAULT_PAGE_PROPS, DEFAULT_SEARCH_PROPS } from "actions"
import API from "api"
import AppContext from "components/AppContext"
import "components/BlueprintOverrides.css"
import ButtonToggleGroup from "components/ButtonToggleGroup"
import * as FieldHelper from "components/FieldHelper"
import Fieldset from "components/Fieldset"
import Messages from "components/Messages"
import MosaicLayout from "components/MosaicLayout"
import {
  mapPageDispatchersToProps,
  PageDispatchersPropType,
  useBoilerplate
} from "components/Page"
import ReportCollection, {
  FORMAT_CALENDAR,
  FORMAT_MAP,
  FORMAT_STATISTICS,
  FORMAT_SUMMARY,
  FORMAT_TABLE
} from "components/ReportCollection"
import { getSearchQuery, SearchQueryPropType } from "components/SearchFilters"
import { Field, Form, Formik } from "formik"
import { Organization, Report } from "models"
import moment from "moment"
import pluralize from "pluralize"
import PropTypes from "prop-types"
import React, { useContext, useState } from "react"
import { Button, FormText, Modal } from "react-bootstrap"
import ContainerDimensions from "react-container-dimensions"
import { connect } from "react-redux"
import { useHistory, useLocation } from "react-router-dom"
import Settings from "settings"
import utils from "utils"

const GQL_SHOW_ROLLUP_EMAIL = gql`
  query(
    $startDate: Instant!
    $endDate: Instant!
    $principalOrganizationUuid: String
    $advisorOrganizationUuid: String
    $orgType: OrganizationType
  ) {
    showRollupEmail(
      startDate: $startDate
      endDate: $endDate
      principalOrganizationUuid: $principalOrganizationUuid
      advisorOrganizationUuid: $advisorOrganizationUuid
      orgType: $orgType
    )
  }
`
const GQL_EMAIL_ROLLUP = gql`
  mutation(
    $startDate: Instant!
    $endDate: Instant!
    $email: AnetEmailInput!
    $principalOrganizationUuid: String
    $advisorOrganizationUuid: String
    $orgType: OrganizationType
  ) {
    emailRollup(
      startDate: $startDate
      endDate: $endDate
      email: $email
      principalOrganizationUuid: $principalOrganizationUuid
      advisorOrganizationUuid: $advisorOrganizationUuid
      orgType: $orgType
    )
  }
`

const Collection = ({ queryParams }) => (

    <ReportCollection
      paginationKey="r_rollup"
      queryParams={queryParams}
      viewFormats={[
        FORMAT_SUMMARY,
        FORMAT_TABLE,
        FORMAT_STATISTICS,
        FORMAT_CALENDAR
      ]}
    />

)

Collection.propTypes = {
  queryParams: PropTypes.object
}

const Map = ({ queryParams }) => (

    <ContainerDimensions>
      {({ width, height }) => (
        <ReportCollection
          queryParams={queryParams}
          width={width}
          height={height}
          marginBottom={0}
          viewFormats={[FORMAT_MAP]}
        />
      )}
    </ContainerDimensions>

)

Map.propTypes = {
  queryParams: PropTypes.object
}

const RollupShow = ({ pageDispatchers, searchQuery }) => {
  const { appSettings } = useContext(AppContext)
  const history = useHistory()
  const routerLocation = useLocation()
  const { startDate, endDate } = getDateRangeFromQS(routerLocation.search)
  const [showEmailModal, setShowEmailModal] = useState(false)
  const [orgType, setOrgType] = useState(Organization.TYPE.ADVISOR_ORG)
  const [focusedOrg, setFocusedOrg] = useState(null)
  const [saveSuccess, setSaveSuccess] = useState(null)
  const [saveError, setSaveError] = useState(null)
  const previewPlaceholderUrl = "/help"
  useBoilerplate({
    pageProps: DEFAULT_PAGE_PROPS,
    searchProps: DEFAULT_SEARCH_PROPS,
    pageDispatchers
  })

  const VISUALIZATIONS = [
    {
      id: "rbdow-collection",
      icons: [IconNames.PANEL_TABLE],
      title: "Reports by organization",
      renderer: renderReportCollection
    },
    {
      id: "rbdow-map",
      icons: [IconNames.MAP],
      title: "Map by organization",
      renderer: renderReportMap
    }
  ]
  const INITIAL_LAYOUT = VISUALIZATIONS[0].id
  const DESCRIPTION = "Number of reports released per organization."
  const flexStyle = {
    display: "flex",
    flexDirection: "column",
    flex: "1 1 auto",
    height: "100%",
    overflow: "auto"
  }
  const fieldsetStyle = {
    height: "100%",
    overflow: "auto",
    display: "flex",
    flexDirection: "column"
  }
  const mosaicLayoutStyle = {
    display: "flex",
    flex: "1 1 auto",
    height: "100%"
  }
  const inputFormat = Settings.dateFormats.forms.input.date[0]
  const style = { width: "7em", fontSize: "1em" }

  return (

      <Messages error={saveError} success={saveSuccess} />

      <Fieldset
        title={

                Rollup
                {focusedOrg && ` for ${focusedOrg.shortName}`}

              <DateRangeInput
                className="rollupDateRange"
                startInputProps={{ style }}
                endInputProps={{ style }}
                value={[startDate.toDate(), endDate.toDate()]}
                onChange={changeRollupDate}
                formatDate={date => moment(date).format(inputFormat)}
                parseDate={str =>
                  moment(
                    str,
                    Settings.dateFormats.forms.input.date,
                    true
                  ).toDate()
                }
                placeholder={inputFormat}
                maxDate={moment().toDate()}
                allowSingleDayRange
                closeOnSelection={false}
                contiguousCalendarMonths={false}
                shortcuts
              />

            {focusedOrg ? (
              <Button
                onClick={() => setFocusedOrg(null)}
                variant="outline-secondary"
              >
                All organizations
              </Button>
            ) : (
              <ButtonToggleGroup value={orgType} onChange={setOrgType}>
                <Button
                  value={Organization.TYPE.ADVISOR_ORG}
                  variant="outline-secondary"
                >
                  {pluralize(Settings.fields.advisor.org.name)}
                </Button>
                <Button
                  value={Organization.TYPE.PRINCIPAL_ORG}
                  variant="outline-secondary"
                >
                  {pluralize(Settings.fields.principal.org.name)}
                </Button>
              </ButtonToggleGroup>
            )}

        }
        action={

            <Button
              id="print-rollup"
              href={previewPlaceholderUrl}
              target="rollup"
              onClick={printPreview}
              variant="outline-secondary"
            >
              Print
            </Button>
            <Button
              id="email-rollup"
              onClick={toggleEmailModal}
              variant="primary"
            >
              Email rollup
            </Button>

        }
        style={fieldsetStyle}
      >
        <MosaicLayout
          style={mosaicLayoutStyle}
          visualizations={VISUALIZATIONS}
          initialNode={INITIAL_LAYOUT}
          description={DESCRIPTION}
        />
      </Fieldset>
      <Formik
        enableReinitialize
        onSubmit={onSubmitEmailRollup}
        initialValues={{ to: "", comment: "" }}
      >
        {formikProps => renderEmailModal(formikProps)}
      </Formik>

  )

  function renderReportCollection(id) {
    return <Collection queryParams={getQueryParams()} />
  }

  function renderReportMap(id) {
    return <Map queryParams={getQueryParams()} />
  }

  function getQueryParams() {
    const sqParams = getSearchQuery(searchQuery)
    const maxReportAge =
      1 + (parseInt(appSettings.DAILY_ROLLUP_MAX_REPORT_AGE_DAYS, 10) || 14)
    const reportsQueryParams = {
      state: [Report.STATE.PUBLISHED], // Specifically excluding cancelled engagements.
      releasedAtStart: getRollupStart().valueOf(),
      releasedAtEnd: getRollupEnd().valueOf(),
      engagementDateStart: moment(getRollupStart())
        .subtract(maxReportAge, "days")
        .valueOf(),
      sortBy: "ENGAGEMENT_DATE",
      sortOrder: "DESC",
      ...sqParams
    }
    if (focusedOrg) {
      if (orgType === Organization.TYPE.PRINCIPAL_ORG) {
        reportsQueryParams.principalOrgUuid = focusedOrg.uuid
        reportsQueryParams.includePrincipalOrgChildren = true
      } else {
        reportsQueryParams.advisorOrgUuid = focusedOrg.uuid
        reportsQueryParams.includeAdvisorOrgChildren = true
      }
    }
    return reportsQueryParams
  }

  function getDateStr() {
    if (startDate.isSame(endDate, "day")) {
      return `for ${startDate.format(
        Settings.dateFormats.forms.displayShort.date
      )}`
    } else {
      return `from ${startDate.format(
        Settings.dateFormats.forms.displayShort.date
      )} to ${endDate.format(Settings.dateFormats.forms.displayShort.date)}`
    }
  }

  function getRollupStart() {
    return moment(startDate).startOf("day")
  }

  function getRollupEnd() {
    return moment(endDate).endOf("day")
  }

  function getDateOrDefault(qsDate) {
    return qsDate ? moment(+qsDate) : moment().subtract(1, "day") // default to yesterday
  }

  function getDateRangeFromQS(search) {
    // Having a qs with ?date=… overrides startDate and endDate (for backwards compatibility)
    const qs = utils.parseQueryString(search)
    return {
      startDate: getDateOrDefault(qs.date || qs.startDate),
      endDate: getDateOrDefault(qs.date || qs.endDate)
    }
  }

  function changeRollupDate(dateRange) {
    const startDate = dateRange[0] && dateRange[0].valueOf()
    const endDate = dateRange[1] && dateRange[1].valueOf()
    if (!startDate || !endDate) {
      return
    }
    history.replace({
      pathname: "rollup",
      search: utils.formatQueryString({
        startDate,
        endDate
      })
    })
  }

  function renderEmailModal(formikProps) {
    const { isSubmitting, submitForm } = formikProps
    return (
      <Modal centered show={showEmailModal} onHide={toggleEmailModal}>
        <Form>
          <Modal.Header closeButton>
            <Modal.Title>Email rollup - {getDateStr()}</Modal.Title>
          </Modal.Header>
          <Modal.Body>

              {focusedOrg
                ? `Reports for ${focusedOrg.shortName}`
                : `All reports by ${orgType.replace("_", " ").toLowerCase()}`}

            <Field
              name="to"
              component={FieldHelper.InputField}
              validate={email => handleEmailValidation(email)}
              vertical
            >
              <FormText>
                One or more email addresses, comma separated, e.g.:

                  jane@nowhere.invalid, John Doe <john@example.org>, "Mr.
                  X" <x@example.org>

              </FormText>
            </Field>
            <Field
              name="comment"
              component={FieldHelper.InputField}
              asA="textarea"
              vertical
            />
          </Modal.Body>
          <Modal.Footer>
            <Button
              id="preview-rollup-email"
              href={previewPlaceholderUrl}
              target="rollup"
              onClick={showPreview}
              variant="outline-secondary"
            >
              Preview
            </Button>
            <Button
              id="send-rollup-email"
              variant="primary"
              onClick={submitForm}
              disabled={isSubmitting}
            >
              Send email
            </Button>
          </Modal.Footer>
        </Form>
      </Modal>
    )
  }

  function handleEmailValidation(value) {
    const r = utils.parseEmailAddresses(value)
    return r.isValid ? null : r.message
  }

  function toggleEmailModal() {
    setShowEmailModal(!showEmailModal)
  }

  function printPreview() {
    showPreview(true)
  }

  function showPreview(print) {
    const variables = {
      startDate: getRollupStart().valueOf(),
      endDate: getRollupEnd().valueOf()
    }
    if (focusedOrg) {
      if (orgType === Organization.TYPE.PRINCIPAL_ORG) {
        variables.principalOrganizationUuid = focusedOrg.uuid
      } else {
        variables.advisorOrganizationUuid = focusedOrg.uuid
      }
    }
    if (orgType) {
      variables.orgType = orgType
    }
    return API.query(GQL_SHOW_ROLLUP_EMAIL, variables).then(data => {
      const rollupWindow = window.open("", "rollup")
      const doc = rollupWindow.document
      doc.clear()
      doc.open()
      doc.write(data.showRollupEmail)
      doc.close()
      if (print === true) {
        rollupWindow.print()
      }
    })
  }

  function onSubmitEmailRollup(values, form) {
    emailRollup(values, form)
      .then(response => onSubmitEmailRollupSuccess(response, values, form))
      .catch(error => {
        setSaveSuccess(null)
        setSaveError(error)
        setShowEmailModal(false)
        form.setSubmitting(false)
      })
  }

  function onSubmitEmailRollupSuccess(response, values, form) {
    setSaveSuccess("Email successfully sent")
    setSaveError(null)
    setShowEmailModal(false)
    form.resetForm() // Reset the email modal field values
  }

  function emailRollup(values, form) {
    const r = utils.parseEmailAddresses(values.to)
    if (!r.isValid) {
      return
    }
    const emailDelivery = {
      toAddresses: r.to,
      comment: values.comment
    }
    const variables = {
      startDate: getRollupStart().valueOf(),
      endDate: getRollupEnd().valueOf(),
      email: emailDelivery
    }
    if (focusedOrg) {
      if (orgType === Organization.TYPE.PRINCIPAL_ORG) {
        variables.principalOrganizationUuid = focusedOrg.uuid
      } else {
        variables.advisorOrganizationUuid = focusedOrg.uuid
      }
    }
    if (orgType) {
      variables.orgType = orgType
    }
    return API.mutation(GQL_EMAIL_ROLLUP, variables)
  }
}

RollupShow.propTypes = {
  searchQuery: SearchQueryPropType,
  pageDispatchers: PageDispatchersPropType
}

const mapStateToProps = (state, ownProps) => ({
  searchQuery: state.searchQuery
})

export default connect(mapStateToProps, mapPageDispatchersToProps)(RollupShow)
<|endoftext|>


<commit_before>/*!
    SlickNav Responsive Mobile Menu v1.0.0
    (c) 2014 Josh Cope
    licensed under MIT
*/
;(function(e,t,n){function o(t,n){this.element=t;this.settings=e.extend({},r,n);this._defaults=r;this._name=i;this.init()}var r={label:"MENU",duplicate:true,duration:200,easingOpen:"swing",easingClose:"swing",closedSymbol:"►",openedSymbol:"▼",prependTo:"body",parentTag:"a",closeOnClick:false,allowParentLinks:false,nestedParentLinks:true,showChildren:false,init:function(){},open:function(){},close:function(){}},i="slicknav",s="slicknav";o.prototype.init=function(){var n=this;var r=e(this.element);var i=this.settings;if(i.duplicate){n.mobileNav=r.clone();n.mobileNav.removeAttr("id");n.mobileNav.find("*").each(function(t,n){e(n).removeAttr("id")})}else n.mobileNav=r;var o=s+"_icon";if(i.label===""){o+=" "+s+"_no-text"}if(i.parentTag=="a"){i.parentTag='a href="#"'}n.mobileNav.attr("class",s+"_nav");var u=e('

');n.btn=e("<"+i.parentTag+' aria-haspopup="true" tabindex="0" class="'+s+"_btn "+s+'_collapsed">'+i.label+'');e(u).append(n.btn);e(i.prependTo).prepend(u);u.append(n.mobileNav);var a=n.mobileNav.find("li");e(a).each(function(){var t=e(this);var r={};r.children=t.children("ul").attr("role","menu");t.data("menu",r);if(r.children.length>0){var o=t.contents();var u=false;var a=[];e(o).each(function(){if(!e(this).is("ul")){a.push(this)}else{return false}if(e(this).is("a")){u=true}});var f=e("<"+i.parentTag+' role="menuitem" aria-haspopup="true" tabindex="-1" class="'+s+'_item"/>');if(!i.allowParentLinks||i.nestedParentLinks||!u){var l=e(a).wrapAll(f).parent();l.addClass(s+"_row")}else e(a).wrapAll('').parent();t.addClass(s+"_collapsed");t.addClass(s+"_parent");var c=e(''+i.closedSymbol+"");if(i.allowParentLinks&&!i.nestedParentLinks&&u)c=c.wrap(f).parent();e(a).last().after(c)}else if(t.children().length===0){t.addClass(s+"_txtnode")}t.children("a").attr("role","menuitem").click(function(t){if(i.closeOnClick&&!e(t.target).parent().closest("li").hasClass(s+"_parent"))e(n.btn).click()});if(i.closeOnClick&&i.allowParentLinks){t.children("a").children("a").click(function(t){e(n.btn).click()});t.find("."+s+"_parent-link a:not(."+s+"_item)").click(function(t){e(n.btn).click()})}});e(a).each(function(){var t=e(this).data("menu");if(!i.showChildren){n._visibilityToggle(t.children,null,false,null,true)}});n._visibilityToggle(n.mobileNav,null,false,"init",true);n.mobileNav.attr("role","menu");e(t).mousedown(function(){n._outlines(false)});e(t).keyup(function(){n._outlines(true)});e(n.btn).click(function(e){e.preventDefault();n._menuToggle()});n.mobileNav.on("click","."+s+"_item",function(t){t.preventDefault();n._itemClick(e(this))});e(n.btn).keydown(function(e){var t=e||event;if(t.keyCode==13){e.preventDefault();n._menuToggle()}});n.mobileNav.on("keydown","."+s+"_item",function(t){var r=t||event;if(r.keyCode==13){t.preventDefault();n._itemClick(e(t.target))}});if(i.allowParentLinks&&i.nestedParentLinks){e("."+s+"_item a").click(function(e){e.stopImmediatePropagation()})}};o.prototype._menuToggle=function(e){var t=this;var n=t.btn;var r=t.mobileNav;if(n.hasClass(s+"_collapsed")){n.removeClass(s+"_collapsed");n.addClass(s+"_open")}else{n.removeClass(s+"_open");n.addClass(s+"_collapsed")}n.addClass(s+"_animating");t._visibilityToggle(r,n.parent(),true,n)};o.prototype._itemClick=function(e){var t=this;var n=t.settings;var r=e.data("menu");if(!r){r={};r.arrow=e.children("."+s+"_arrow");r.ul=e.next("ul");r.parent=e.parent();if(r.parent.hasClass(s+"_parent-link")){r.parent=e.parent().parent();r.ul=e.parent().next("ul")}e.data("menu",r)}if(r.parent.hasClass(s+"_collapsed")){r.arrow.html(n.openedSymbol);r.parent.removeClass(s+"_collapsed");r.parent.addClass(s+"_open");r.parent.addClass(s+"_animating");t._visibilityToggle(r.ul,r.parent,true,e)}else{r.arrow.html(n.closedSymbol);r.parent.addClass(s+"_collapsed");r.parent.removeClass(s+"_open");r.parent.addClass(s+"_animating");t._visibilityToggle(r.ul,r.parent,true,e)}};o.prototype._visibilityToggle=function(t,n,r,i,o){var u=this;var a=u.settings;var f=u._getActionItems(t);var l=0;if(r)l=a.duration;if(t.hasClass(s+"_hidden")){t.removeClass(s+"_hidden");t.slideDown(l,a.easingOpen,function(){e(i).removeClass(s+"_animating");e(n).removeClass(s+"_animating");if(!o){a.open(i)}});t.attr("aria-hidden","false");f.attr("tabindex","0");u._setVisAttr(t,false)}else{t.addClass(s+"_hidden");t.slideUp(l,this.settings.easingClose,function(){t.attr("aria-hidden","true");f.attr("tabindex","-1");u._setVisAttr(t,true);t.hide();e(i).removeClass(s+"_animating");e(n).removeClass(s+"_animating");if(!o)a.close(i);else if(i=="init")a.init()})}};o.prototype._setVisAttr=function(t,n){var r=this;var i=t.children("li").children("ul").not("."+s+"_hidden");if(!n){i.each(function(){var t=e(this);t.attr("aria-hidden","false");var i=r._getActionItems(t);i.attr("tabindex","0");r._setVisAttr(t,n)})}else{i.each(function(){var t=e(this);t.attr("aria-hidden","true");var i=r._getActionItems(t);i.attr("tabindex","-1");r._setVisAttr(t,n)})}};o.prototype._getActionItems=function(e){var t=e.data("menu");if(!t){t={};var n=e.children("li");var r=n.find("a");t.links=r.add(n.find("."+s+"_item"));e.data("menu",t)}return t.links};o.prototype._outlines=function(t){if(!t){e("."+s+"_item, ."+s+"_btn").css("outline","none")}else{e("."+s+"_item, ."+s+"_btn").css("outline","")}};o.prototype.toggle=function(){var e=this;e._menuToggle()};o.prototype.open=function(){var e=this;if(e.btn.hasClass(s+"_collapsed")){e._menuToggle()}};o.prototype.close=function(){var e=this;if(e.btn.hasClass(s+"_open")){e._menuToggle()}};e.fn[i]=function(t){var n=arguments;if(t===undefined||typeof t==="object"){return this.each(function(){if(!e.data(this,"plugin_"+i)){e.data(this,"plugin_"+i,new o(this,t))}})}else if(typeof t==="string"&&t[0]!=="_"&&t!=="init"){var r;this.each(function(){var s=e.data(this,"plugin_"+i);if(s instanceof o&&typeof s[t]==="function"){r=s[t].apply(s,Array.prototype.slice.call(n,1))}});return r!==undefined?r:this}}})(jQuery,document,window)<commit_msg>New minified file<commit_after>/*!
    SlickNav Responsive Mobile Menu v1.0.0
    (c) 2014 Josh Cope
    licensed under MIT
*/
;(function(e,t,n){function o(t,n){this.element=t;this.settings=e.extend({},r,n);this._defaults=r;this._name=i;this.init()}var r={label:"MENU",duplicate:true,duration:200,easingOpen:"swing",easingClose:"swing",closedSymbol:"►",openedSymbol:"▼",prependTo:"body",parentTag:"a",closeOnClick:false,allowParentLinks:false,nestedParentLinks:true,showChildren:false,init:function(){},open:function(){},close:function(){}},i="slicknav",s="slicknav";o.prototype.init=function(){var n=this;var r=e(this.element);var i=this.settings;if(i.duplicate){n.mobileNav=r.clone();n.mobileNav.removeAttr("id");n.mobileNav.find("*").each(function(t,n){e(n).removeAttr("id")})}else n.mobileNav=r;var o=s+"_icon";if(i.label===""){o+=" "+s+"_no-text"}if(i.parentTag=="a"){i.parentTag='a href="#"'}n.mobileNav.attr("class",s+"_nav");var u=e('

');n.btn=e(["<"+i.parentTag+' aria-haspopup="true" tabindex="0" class="'+s+"_btn "+s+'_collapsed">',''+i.label+"",'','','','',"","</"+i.parentTag+">"].join(""));e(u).append(n.btn);e(i.prependTo).prepend(u);u.append(n.mobileNav);var a=n.mobileNav.find("li");e(a).each(function(){var t=e(this);var r={};r.children=t.children("ul").attr("role","menu");t.data("menu",r);if(r.children.length>0){var o=t.contents();var u=false;var a=[];e(o).each(function(){if(!e(this).is("ul")){a.push(this)}else{return false}if(e(this).is("a")){u=true}});var f=e("<"+i.parentTag+' role="menuitem" aria-haspopup="true" tabindex="-1" class="'+s+'_item"/>');if(!i.allowParentLinks||i.nestedParentLinks||!u){var l=e(a).wrapAll(f).parent();l.addClass(s+"_row")}else e(a).wrapAll('').parent();t.addClass(s+"_collapsed");t.addClass(s+"_parent");var c=e(''+i.closedSymbol+"");if(i.allowParentLinks&&!i.nestedParentLinks&&u)c=c.wrap(f).parent();e(a).last().after(c)}else if(t.children().length===0){t.addClass(s+"_txtnode")}t.children("a").attr("role","menuitem").click(function(t){if(i.closeOnClick&&!e(t.target).parent().closest("li").hasClass(s+"_parent"))e(n.btn).click()});if(i.closeOnClick&&i.allowParentLinks){t.children("a").children("a").click(function(t){e(n.btn).click()});t.find("."+s+"_parent-link a:not(."+s+"_item)").click(function(t){e(n.btn).click()})}});e(a).each(function(){var t=e(this).data("menu");if(!i.showChildren){n._visibilityToggle(t.children,null,false,null,true)}});n._visibilityToggle(n.mobileNav,null,false,"init",true);n.mobileNav.attr("role","menu");e(t).mousedown(function(){n._outlines(false)});e(t).keyup(function(){n._outlines(true)});e(n.btn).click(function(e){e.preventDefault();n._menuToggle()});n.mobileNav.on("click","."+s+"_item",function(t){t.preventDefault();n._itemClick(e(this))});e(n.btn).keydown(function(e){var t=e||event;if(t.keyCode==13){e.preventDefault();n._menuToggle()}});n.mobileNav.on("keydown","."+s+"_item",function(t){var r=t||event;if(r.keyCode==13){t.preventDefault();n._itemClick(e(t.target))}});if(i.allowParentLinks&&i.nestedParentLinks){e("."+s+"_item a").click(function(e){e.stopImmediatePropagation()})}};o.prototype._menuToggle=function(e){var t=this;var n=t.btn;var r=t.mobileNav;if(n.hasClass(s+"_collapsed")){n.removeClass(s+"_collapsed");n.addClass(s+"_open")}else{n.removeClass(s+"_open");n.addClass(s+"_collapsed")}n.addClass(s+"_animating");t._visibilityToggle(r,n.parent(),true,n)};o.prototype._itemClick=function(e){var t=this;var n=t.settings;var r=e.data("menu");if(!r){r={};r.arrow=e.children("."+s+"_arrow");r.ul=e.next("ul");r.parent=e.parent();if(r.parent.hasClass(s+"_parent-link")){r.parent=e.parent().parent();r.ul=e.parent().next("ul")}e.data("menu",r)}if(r.parent.hasClass(s+"_collapsed")){r.arrow.html(n.openedSymbol);r.parent.removeClass(s+"_collapsed");r.parent.addClass(s+"_open");r.parent.addClass(s+"_animating");t._visibilityToggle(r.ul,r.parent,true,e)}else{r.arrow.html(n.closedSymbol);r.parent.addClass(s+"_collapsed");r.parent.removeClass(s+"_open");r.parent.addClass(s+"_animating");t._visibilityToggle(r.ul,r.parent,true,e)}};o.prototype._visibilityToggle=function(t,n,r,i,o){var u=this;var a=u.settings;var f=u._getActionItems(t);var l=0;if(r)l=a.duration;if(t.hasClass(s+"_hidden")){t.removeClass(s+"_hidden");t.slideDown(l,a.easingOpen,function(){e(i).removeClass(s+"_animating");e(n).removeClass(s+"_animating");if(!o){a.open(i)}});t.attr("aria-hidden","false");f.attr("tabindex","0");u._setVisAttr(t,false)}else{t.addClass(s+"_hidden");t.slideUp(l,this.settings.easingClose,function(){t.attr("aria-hidden","true");f.attr("tabindex","-1");u._setVisAttr(t,true);t.hide();e(i).removeClass(s+"_animating");e(n).removeClass(s+"_animating");if(!o)a.close(i);else if(i=="init")a.init()})}};o.prototype._setVisAttr=function(t,n){var r=this;var i=t.children("li").children("ul").not("."+s+"_hidden");if(!n){i.each(function(){var t=e(this);t.attr("aria-hidden","false");var i=r._getActionItems(t);i.attr("tabindex","0");r._setVisAttr(t,n)})}else{i.each(function(){var t=e(this);t.attr("aria-hidden","true");var i=r._getActionItems(t);i.attr("tabindex","-1");r._setVisAttr(t,n)})}};o.prototype._getActionItems=function(e){var t=e.data("menu");if(!t){t={};var n=e.children("li");var r=n.find("a");t.links=r.add(n.find("."+s+"_item"));e.data("menu",t)}return t.links};o.prototype._outlines=function(t){if(!t){e("."+s+"_item, ."+s+"_btn").css("outline","none")}else{e("."+s+"_item, ."+s+"_btn").css("outline","")}};o.prototype.toggle=function(){var e=this;e._menuToggle()};o.prototype.open=function(){var e=this;if(e.btn.hasClass(s+"_collapsed")){e._menuToggle()}};o.prototype.close=function(){var e=this;if(e.btn.hasClass(s+"_open")){e._menuToggle()}};e.fn[i]=function(t){var n=arguments;if(t===undefined||typeof t==="object"){return this.each(function(){if(!e.data(this,"plugin_"+i)){e.data(this,"plugin_"+i,new o(this,t))}})}else if(typeof t==="string"&&t[0]!=="_"&&t!=="init"){var r;this.each(function(){var s=e.data(this,"plugin_"+i);if(s instanceof o&&typeof s[t]==="function"){r=s[t].apply(s,Array.prototype.slice.call(n,1))}});return r!==undefined?r:this}}})(jQuery,document,window)<|endoftext|>


<commit_before>/* Engine.js
 * This file provides the game loop functionality (update entities and render),
 * draws the initial game board on the screen, and then calls the update and
 * render methods on your player and enemy objects (defined in your app.js).
 *
 * A game engine works by drawing the entire game screen over and over, kind of
 * like a flipbook you may have created as a kid. When your player moves across
 * the screen, it may look like just that image/character is moving or being
 * drawn but that is not the case. What's really happening is the entire "scene"
 * is being drawn over and over, presenting the illusion of animation.
 *
 * This engine is available globally via the Engine variable and it also makes
 * the canvas' context (ctx) object globally available to make writing app.js
 * a little simpler to work with.
 */

var Engine = (function(global) {
    /* Predefine the variables we'll be using within this scope,
     * create the canvas element, grab the 2D context for that canvas
     * set the canvas elements height/width and add it to the DOM.
     */
    var doc = global.document,
        win = global.window,
        canvas = doc.createElement('canvas'),
        ctx = canvas.getContext('2d'),
        lastTime;

    canvas.width = 505;
    canvas.height = 606;
    doc.body.appendChild(canvas);

    /* This function serves as the kickoff point for the game loop itself
     * and handles properly calling the update and render methods.
     */
    function main() {
        /* Get our time delta information which is required if your game
         * requires smooth animation. Because everyone's computer processes
         * instructions at different speeds we need a constant value that
         * would be the same for everyone (regardless of how fast their
         * computer is) - hurray time!
         */
        var now = Date.now(),
            dt = (now - lastTime) / 1000.0;

        /* Call our update/render functions, pass along the time delta to
         * our update function since it may be used for smooth animation.
         */
        update(dt);
        render();

        /* Set our lastTime variable which is used to determine the time delta
         * for the next time this function is called.
         */
        lastTime = now;

        /* Use the browser's requestAnimationFrame function to call this
         * function again as soon as the browser is able to draw another frame.
         */
        win.requestAnimationFrame(main);
    }

    /* This function does some initial setup that should only occur once,
     * particularly setting the lastTime variable that is required for the
     * game loop.
     */
    function init() {
        reset();
        lastTime = Date.now();
        main();
    }

    /* This function is called by main (our game loop) and itself calls all
     * of the functions which may need to update entity's data. Based on how
     * you implement your collision detection (when two entities occupy the
     * same space, for instance when your character should die), you may find
     * the need to add an additional function call here. For now, we've left
     * it commented out - you may or may not want to implement this
     * functionality this way (you could just implement collision detection
     * on the entities themselves within your app.js file).
     */
    function update(dt) {
        updateEntities(dt);
        // checkCollisions();
    }

    /* This is called by the update function  and loops through all of the
     * objects within your allEnemies array as defined in app.js and calls
     * their update() methods. It will then call the update function for your
     * player object. These update methods should focus purely on updating
     * the data/properties related to  the object. Do your drawing in your
     * render methods.
     */
    function updateEntities(dt) {
        allEnemies.forEach(function(enemy) {
            enemy.update(dt);
        });
        player.update();
    }

    /* This function initially draws the "game level", it will then call
     * the renderEntities function. Remember, this function is called every
     * game tick (or loop of the game engine) because that's how games work -
     * they are flipbooks creating the illusion of animation but in reality
     * they are just drawing the entire screen over and over.
     */
    function render() {
        /* This array holds the relative URL to the image used
         * for that particular row of the game level.
         */
        var rowImages = [
                'images/water-block.png',   // Top row is water
                'images/stone-block.png',   // Row 1 of 3 of stone
                'images/stone-block.png',   // Row 2 of 3 of stone
                'images/stone-block.png',   // Row 3 of 3 of stone
                'images/grass-block.png',   // Row 1 of 2 of grass
                'images/grass-block.png'    // Row 2 of 2 of grass
            ],
            numRows = 6,
            numCols = 5,
            row, col;

        /* Loop through the number of rows and columns we've defined above
         * and, using the rowImages array, draw the correct image for that
         * portion of the "grid"
         */
        for (row = 0; row < numRows; row++) {
            for (col = 0; col < numCols; col++) {
                /* The drawImage function of the canvas' context element
                 * requires 3 parameters: the image to draw, the x coordinate
                 * to start drawing and the y coordinate to start drawing.
                 * We're using our Resources helpers to refer to our images
                 * so that we get the benefits of caching these images, since
                 * we're using them over and over.
                 */
                ctx.drawImage(Resources.get(rowImages[row]), col * 101, row * 83);
            }
        }

        renderEntities();
    }

    /* This function is called by the render function and is called on each game
     * tick. It's purpose is to then call the render functions you have defined
     * on your enemy and player entities within app.js
     */
    function renderEntities() {
        /* Loop through all of the objects within the allEnemies array and call
         * the render function you have defined.
         */
        allEnemies.forEach(function(enemy) {
            enemy.render();
        });

        player.render();
    }

    /* This function does nothing but it could have been a good place to
     * handle game reset states - maybe a new game menu or a game over screen
     * those sorts of things. It's only called once by the init() method.
     */
    function reset() {
        // noop
    }

    /* Go ahead and load all of the images we know we're going to need to
     * draw our game level. Then set init as the callback method, so that when
     * all of these images are properly loaded our game will start.
     */
    Resources.load([
        'images/stone-block.png',
        'images/water-block.png',
        'images/grass-block.png',
        'images/enemy-bug.png',
        'images/char-boy.png'
    ]);
    Resources.onReady(init);

    /* Assign the canvas' context object to the global variable (the window
     * object when run in a browser) so that developer's can use it more easily
     * from within their app.js files.
     */
    global.ctx = ctx;
})(this);
<commit_msg>Spelling: It's -> Its, developer's -> developers; Extra spaces; Extra empty line<commit_after>/* Engine.js
 * This file provides the game loop functionality (update entities and render),
 * draws the initial game board on the screen, and then calls the update and
 * render methods on your player and enemy objects (defined in your app.js).
 *
 * A game engine works by drawing the entire game screen over and over, kind of
 * like a flipbook you may have created as a kid. When your player moves across
 * the screen, it may look like just that image/character is moving or being
 * drawn but that is not the case. What's really happening is the entire "scene"
 * is being drawn over and over, presenting the illusion of animation.
 *
 * This engine is available globally via the Engine variable and it also makes
 * the canvas' context (ctx) object globally available to make writing app.js
 * a little simpler to work with.
 */

var Engine = (function(global) {
    /* Predefine the variables we'll be using within this scope,
     * create the canvas element, grab the 2D context for that canvas
     * set the canvas elements height/width and add it to the DOM.
     */
    var doc = global.document,
        win = global.window,
        canvas = doc.createElement('canvas'),
        ctx = canvas.getContext('2d'),
        lastTime;

    canvas.width = 505;
    canvas.height = 606;
    doc.body.appendChild(canvas);

    /* This function serves as the kickoff point for the game loop itself
     * and handles properly calling the update and render methods.
     */
    function main() {
        /* Get our time delta information which is required if your game
         * requires smooth animation. Because everyone's computer processes
         * instructions at different speeds we need a constant value that
         * would be the same for everyone (regardless of how fast their
         * computer is) - hurray time!
         */
        var now = Date.now(),
            dt = (now - lastTime) / 1000.0;

        /* Call our update/render functions, pass along the time delta to
         * our update function since it may be used for smooth animation.
         */
        update(dt);
        render();

        /* Set our lastTime variable which is used to determine the time delta
         * for the next time this function is called.
         */
        lastTime = now;

        /* Use the browser's requestAnimationFrame function to call this
         * function again as soon as the browser is able to draw another frame.
         */
        win.requestAnimationFrame(main);
    }

    /* This function does some initial setup that should only occur once,
     * particularly setting the lastTime variable that is required for the
     * game loop.
     */
    function init() {
        reset();
        lastTime = Date.now();
        main();
    }

    /* This function is called by main (our game loop) and itself calls all
     * of the functions which may need to update entity's data. Based on how
     * you implement your collision detection (when two entities occupy the
     * same space, for instance when your character should die), you may find
     * the need to add an additional function call here. For now, we've left
     * it commented out - you may or may not want to implement this
     * functionality this way (you could just implement collision detection
     * on the entities themselves within your app.js file).
     */
    function update(dt) {
        updateEntities(dt);
        // checkCollisions();
    }

    /* This is called by the update function and loops through all of the
     * objects within your allEnemies array as defined in app.js and calls
     * their update() methods. It will then call the update function for your
     * player object. These update methods should focus purely on updating
     * the data/properties related to the object. Do your drawing in your
     * render methods.
     */
    function updateEntities(dt) {
        allEnemies.forEach(function(enemy) {
            enemy.update(dt);
        });
        player.update();
    }

    /* This function initially draws the "game level", it will then call
     * the renderEntities function. Remember, this function is called every
     * game tick (or loop of the game engine) because that's how games work -
     * they are flipbooks creating the illusion of animation but in reality
     * they are just drawing the entire screen over and over.
     */
    function render() {
        /* This array holds the relative URL to the image used
         * for that particular row of the game level.
         */
        var rowImages = [
                'images/water-block.png',   // Top row is water
                'images/stone-block.png',   // Row 1 of 3 of stone
                'images/stone-block.png',   // Row 2 of 3 of stone
                'images/stone-block.png',   // Row 3 of 3 of stone
                'images/grass-block.png',   // Row 1 of 2 of grass
                'images/grass-block.png'    // Row 2 of 2 of grass
            ],
            numRows = 6,
            numCols = 5,
            row, col;

        /* Loop through the number of rows and columns we've defined above
         * and, using the rowImages array, draw the correct image for that
         * portion of the "grid"
         */
        for (row = 0; row < numRows; row++) {
            for (col = 0; col < numCols; col++) {
                /* The drawImage function of the canvas' context element
                 * requires 3 parameters: the image to draw, the x coordinate
                 * to start drawing and the y coordinate to start drawing.
                 * We're using our Resources helpers to refer to our images
                 * so that we get the benefits of caching these images, since
                 * we're using them over and over.
                 */
                ctx.drawImage(Resources.get(rowImages[row]), col * 101, row * 83);
            }
        }

        renderEntities();
    }

    /* This function is called by the render function and is called on each game
     * tick. Its purpose is to then call the render functions you have defined
     * on your enemy and player entities within app.js
     */
    function renderEntities() {
        /* Loop through all of the objects within the allEnemies array and call
         * the render function you have defined.
         */
        allEnemies.forEach(function(enemy) {
            enemy.render();
        });

        player.render();
    }

    /* This function does nothing but it could have been a good place to
     * handle game reset states - maybe a new game menu or a game over screen
     * those sorts of things. It's only called once by the init() method.
     */
    function reset() {
        // noop
    }

    /* Go ahead and load all of the images we know we're going to need to
     * draw our game level. Then set init as the callback method, so that when
     * all of these images are properly loaded our game will start.
     */
    Resources.load([
        'images/stone-block.png',
        'images/water-block.png',
        'images/grass-block.png',
        'images/enemy-bug.png',
        'images/char-boy.png'
    ]);
    Resources.onReady(init);

    /* Assign the canvas' context object to the global variable (the window
     * object when run in a browser) so that developers can use it more easily
     * from within their app.js files.
     */
    global.ctx = ctx;
})(this);
<|endoftext|>


<commit_before>#!/usr/bin/env node

const fs = require('fs');
const shell = require("shelljs");

const Main = (() => {
  try {
    return require("../output/Main");
  } catch (e) {
    return require("./bundle");
  }
})();

const makeSource = require("stream-json");
const Assembler  = require("stream-json/utils/Assembler");
const commandLineArgs = require('command-line-args')
const getUsage = require('command-line-usage')
const fetch = require('node-fetch');

const optionDefinitions = [
  {
    name: 'src',
    type: String,
    multiple: true,
    defaultOption: true,
    typeLabel: '[underline]{file|url}',
    description: 'The JSON file or url to type.'
  },
  {
    name: 'output',
    alias: 'o',
    type: String,
    typeLabel: `[underline]{output}`,
    description: 'The output file.'
  },
  {
    name: 'srcLang',
    type: String,
    defaultValue: 'json',
    typeLabel: '[underline]{json|json-schema}',
    description: 'The source language.'
  },
  {
    name: 'lang',
    alias: 'l',
    type: String,
    typeLabel: `[underline]{${Main.renderers.map((r) => r.extension).join("|")}}`,
    description: 'The target language.'
  },
  {
    name: 'help',
    alias: 'h',
    type: Boolean,
    description: 'Get some help.'
  }
];

const sections = [
  {
    header: 'quicktype',
    content: 'Quickly generate types from data'
  },
  {
    header: 'Options',
    optionList: optionDefinitions
  },
  {
    header: 'Examples',
    content: [
      '$ quicktype [bold]{-o} LatestBlock.cs [underline]{https://blockchain.info/latestblock}'
    ]
  }
];

const options = commandLineArgs(optionDefinitions);

function getRenderer() {
  let lang = options.lang || "cs";
  let renderer = Main.renderers.find((r) => r.extension === lang);

  if (!renderer) {
    console.error(`'${lang}' is not yet supported as an output language.`);
    process.exit(1);
  }

  return renderer;
}

function fromRight(either) {
  let { constructor: { name }, value0: result } = either;
  if (name == "Left") {
    console.error(result);
    console.exit(1);
  } else {
    return result;
  }
}

function renderFromJson(json) {
    let pipeline = {
      "json": Main.renderFromJson,
      "json-schema": Main.renderFromJsonSchema
    }[options.srcLang];

    if (!pipeline) {
      console.error(`Input language '${options.srcLang}' is not supported.`);
      process.exit(1);
    }

    let input = {
      input: json,
      renderer: getRenderer(),
      topLevelName: "TopLevel"
    };

    return fromRight(pipeline(input));
}

function work(json) {
  let output = renderFromJson(json);
  if (options.output) {
    fs.writeFileSync(options.output, output);
  } else {
    process.stdout.write(output);
  }
}

function workFromStream(stream) {
  let source = makeSource();
  let assembler = new Assembler();

  source.output.on("data", function (chunk) {
    assembler[chunk.name] && assembler[chunk.name](chunk.value);
  });
  source.output.on("end", function () {
    work(assembler.current);
  });

  stream.setEncoding('utf8');
  stream.pipe(source.input);
  stream.resume();
}

function usage() {
  console.log(getUsage(sections));
}

function parseFileOrUrl(fileOrUrl) {
  if (fs.existsSync(fileOrUrl)) {
    workFromStream(fs.createReadStream(fileOrUrl));
  } else {
    fetch(fileOrUrl).then(res => workFromStream(res.body));
  }
}

// Output file extension determines the language if language is undefined
if (options.output && !options.lang) {
  if (options.output.indexOf(".") < 0) {
    console.error("Please specify a language (--lang) or an output file extension.");
    process.exit(1);
  }
  options.lang = options.output.split(".").pop();
}

if (options.help) {
  usage();
} else if (!options.src || options.src.length === 0) {
  workFromStream(process.stdin);
} else if (options.src.length == 1) {
  parseFileOrUrl(options.src[0]);
} else {
  usage();
  process.exit(1);
}
<commit_msg>Top level name option in CLI<commit_after>#!/usr/bin/env node

const fs = require('fs');
const shell = require("shelljs");

const Main = (() => {
  try {
    return require("../output/Main");
  } catch (e) {
    return require("./bundle");
  }
})();

const makeSource = require("stream-json");
const Assembler  = require("stream-json/utils/Assembler");
const commandLineArgs = require('command-line-args')
const getUsage = require('command-line-usage')
const fetch = require('node-fetch');

const optionDefinitions = [
  {
    name: 'src',
    type: String,
    multiple: true,
    defaultOption: true,
    typeLabel: '[underline]{file|url}',
    description: 'The JSON file or url to type.'
  },
  {
    name: 'output',
    alias: 'o',
    type: String,
    typeLabel: `[underline]{output}`,
    description: 'The output file.'
  },
  {
    name: 'srcLang',
    type: String,
    defaultValue: 'json',
    typeLabel: '[underline]{json|json-schema}',
    description: 'The source language.'
  },
  {
    name: 'lang',
    alias: 'l',
    type: String,
    typeLabel: `[underline]{${Main.renderers.map((r) => r.extension).join("|")}}`,
    description: 'The target language.'
  },
  {
    name: 'topLevel',
    type: String,
    defaultValue: 'TopLevel',
    typeLabel: '[underline]{type name}',
    description: 'The name for the top level type.'
  },
  {
    name: 'help',
    alias: 'h',
    type: Boolean,
    description: 'Get some help.'
  }
];

const sections = [
  {
    header: 'quicktype',
    content: 'Quickly generate types from data'
  },
  {
    header: 'Options',
    optionList: optionDefinitions
  },
  {
    header: 'Examples',
    content: [
      '$ quicktype [bold]{-o} LatestBlock.cs [underline]{https://blockchain.info/latestblock}'
    ]
  }
];

const options = commandLineArgs(optionDefinitions);

function getRenderer() {
  let lang = options.lang || "cs";
  let renderer = Main.renderers.find((r) => r.extension === lang);

  if (!renderer) {
    console.error(`'${lang}' is not yet supported as an output language.`);
    process.exit(1);
  }

  return renderer;
}

function fromRight(either) {
  let { constructor: { name }, value0: result } = either;
  if (name == "Left") {
    console.error(result);
    console.exit(1);
  } else {
    return result;
  }
}

function renderFromJson(json) {
    let pipeline = {
      "json": Main.renderFromJson,
      "json-schema": Main.renderFromJsonSchema
    }[options.srcLang];

    if (!pipeline) {
      console.error(`Input language '${options.srcLang}' is not supported.`);
      process.exit(1);
    }

    let input = {
      input: json,
      renderer: getRenderer(),
      topLevelName: options.topLevel
    };

    return fromRight(pipeline(input));
}

function work(json) {
  let output = renderFromJson(json);
  if (options.output) {
    fs.writeFileSync(options.output, output);
  } else {
    process.stdout.write(output);
  }
}

function workFromStream(stream) {
  let source = makeSource();
  let assembler = new Assembler();

  source.output.on("data", function (chunk) {
    assembler[chunk.name] && assembler[chunk.name](chunk.value);
  });
  source.output.on("end", function () {
    work(assembler.current);
  });

  stream.setEncoding('utf8');
  stream.pipe(source.input);
  stream.resume();
}

function usage() {
  console.log(getUsage(sections));
}

function parseFileOrUrl(fileOrUrl) {
  if (fs.existsSync(fileOrUrl)) {
    workFromStream(fs.createReadStream(fileOrUrl));
  } else {
    fetch(fileOrUrl).then(res => workFromStream(res.body));
  }
}

// Output file extension determines the language if language is undefined
if (options.output && !options.lang) {
  if (options.output.indexOf(".") < 0) {
    console.error("Please specify a language (--lang) or an output file extension.");
    process.exit(1);
  }
  options.lang = options.output.split(".").pop();
}

if (options.help) {
  usage();
} else if (!options.src || options.src.length === 0) {
  workFromStream(process.stdin);
} else if (options.src.length == 1) {
  parseFileOrUrl(options.src[0]);
} else {
  usage();
  process.exit(1);
}
<|endoftext|>


<commit_before>#!/usr/bin/env node

'use strict';

/**
 * @author: Predrag Stojadinovic <predrag@stojadinovic.net>
 *
 * Usage:
 * -l, --pull        Execute git pull recursively in all subfolders
 * -c, --commit      Execute git commit recursively in all subfolders
 * -p, --push        Execute git commit and push recursively in all subfolders
 *
 * <no params>       Same as using -l:
 *                   Execute git pull recursively in all subfolders
 */

var fs = require('fs');
var _ = require('lodash');
var cmd = require('node-cmd');
var moment = require('moment');
var Promise = require('bluebird');
var commander = require('commander');
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var execSync = require('child_process').execSync;

commander
	.version('0.0.1')
	.option('', '')
	.option('-l, --pull', 'Execute git pull recursively in all subfolders', false)
	.option('-c, --commit', 'Execute git commit recursively in all subfolders', false)
	.option('-p, --push', 'Execute git commit and push recursively in all subfolders', false)
	.option('', '')
	.option('<no params>', 'Same as using -l:')
	.option('', 'Execute git pull recursively in all subfolders')
	.parse(process.argv);

commander.pull = true === commander.pull;
commander.commit = true === commander.commit;
commander.push = true === commander.push;

if (!commander.commit && !commander.push) {
	commander.pull = true;
}

// commander debug:
// console.log('commander.pull: ' + commander.pull);
// console.log('commander.commit: ' + commander.commit);
// console.log('commander.push: ' + commander.push);

var gitterjs = {

	isGit: function (path) {
		try {
			this.goto(path);
			var git = execSync('git rev-parse --is-inside-work-tree', {encoding: 'utf8'});
		} catch (e) {
		}
		return !!git;
	},

	execute: function (command) {
		try {
			console.log(execSync(command, {encoding: 'utf8'}).trim());
		} catch (e) {
		}
	},

	find: function (path) {

		var ret = [];
		if (this.isGit(path)) {
			ret.push(path);
		}

		var done = !_.isEmpty(ret);
		if (!done) {
			var id, childPath, child, children = fs.readdirSync(path);
			for (id in children) {
				if (children.hasOwnProperty(id)) {
					childPath = path + '/' + children[id];
					child = fs.lstatSync(childPath);
					if (child.isDirectory()) {
						if (this.isGit(childPath)) {
							ret.push(childPath);
						}
						ret = ret.concat(this.find(childPath));
					}
				}
			}
		}
		return ret;
	},

	goto: function (dir, print) {
		print && console.log('\nIn ' + dir);
		process.chdir(dir);
	},

	all: function () {
		var self = this;

		// var gits = _.uniq(this.find(process.cwd()));
		var gits = _.uniq(this.find('D:/Bitbucket'));

		var commands = ['git remote prune origin'];
		if (commander.pull) {
			commands.push('git pull');
		}
		if (commander.commit) {
			commands.push('git add .');
			commands.push('git commit -a -m "gitterjs autocommit ' + moment().format("YYYY-MM-DD HH:mm:ss") + '"');
		}
		if (commander.push) {
			if (commander.commit) {
				commands.push('git push');
			} else {
				commands.push('git add .');
				commands.push('git commit -a -m "gitterjs autocommit ' + moment().format("YYYY-MM-DD HH:mm:ss") + '"');
				commands.push('git push');
			}
		}

		_.forEach(gits, function (dir) {
			self.goto(dir, true);
			_.forEach(commands, function (command) {
				self.execute(command);
			});
		});
	}
};

gitterjs.all();
<commit_msg>initial<commit_after>#!/usr/bin/env node

'use strict';

/**
 * @author: Predrag Stojadinovic <predrag@stojadinovic.net>
 *
 * Usage:
 * -l, --pull        Execute git pull recursively in all subfolders
 * -c, --commit      Execute git commit recursively in all subfolders
 * -p, --push        Execute git commit and push recursively in all subfolders
 *
 * <no params>       Same as using -l:
 *                   Execute git pull recursively in all subfolders
 */

var fs = require('fs');
var _ = require('lodash');
var cmd = require('node-cmd');
var moment = require('moment');
var Promise = require('bluebird');
var commander = require('commander');
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var execSync = require('child_process').execSync;

commander
	.version('0.0.1')
	.option('', '')
	.option('-l, --pull', 'Execute git pull recursively in all subfolders', false)
	.option('-c, --commit', 'Execute git commit recursively in all subfolders', false)
	.option('-p, --push', 'Execute git commit and push recursively in all subfolders', false)
	.option('', '')
	.option('<no params>', 'Same as using -l:')
	.option('', 'Execute git pull recursively in all subfolders')
	.parse(process.argv);

commander.pull = true === commander.pull;
commander.commit = true === commander.commit;
commander.push = true === commander.push;

if (!commander.commit && !commander.push) {
	commander.pull = true;
}

// commander debug:
// console.log('commander.pull: ' + commander.pull);
// console.log('commander.commit: ' + commander.commit);
// console.log('commander.push: ' + commander.push);

var gitterjs = {

	isGit: function (path) {
		try {
			this.goto(path);
			var git = execSync('git rev-parse --is-inside-work-tree', {encoding: 'utf8'});
		} catch (e) {
		}
		return !!git;
	},

	execute: function (command) {
		try {
			console.log(execSync(command, {encoding: 'utf8'}).trim());
		} catch (e) {
		}
	},

	find: function (path) {

		var ret = [];
		if (this.isGit(path)) {
			ret.push(path);
		}

		var done = !_.isEmpty(ret);
		if (!done) {
			var id, childPath, child, children = fs.readdirSync(path);
			for (id in children) {
				if (children.hasOwnProperty(id)) {
					childPath = path + '/' + children[id];
					child = fs.lstatSync(childPath);
					if (child.isDirectory()) {
						if (this.isGit(childPath)) {
							ret.push(childPath);
						}
						ret = ret.concat(this.find(childPath));
					}
				}
			}
		}
		return ret;
	},

	goto: function (dir, print) {
		print && console.log('\nIn ' + dir);
		process.chdir(dir);
	},

	all: function () {
		var self = this;

		var gits = _.uniq(this.find(process.cwd()));

		var commands = ['git remote prune origin'];
		if (commander.pull) {
			commands.push('git pull');
		}
		if (commander.commit) {
			commands.push('git add .');
			commands.push('git commit -a -m "gitterjs autocommit ' + moment().format("YYYY-MM-DD HH:mm:ss") + '"');
		}
		if (commander.push) {
			if (commander.commit) {
				commands.push('git push');
			} else {
				commands.push('git add .');
				commands.push('git commit -a -m "gitterjs autocommit ' + moment().format("YYYY-MM-DD HH:mm:ss") + '"');
				commands.push('git push');
			}
		}

		_.forEach(gits, function (dir) {
			self.goto(dir, true);
			_.forEach(commands, function (command) {
				self.execute(command);
			});
		});
	}
};

gitterjs.all();
<|endoftext|>


<commit_before>/*
*  AngularJs Fullcalendar Wrapper for the JQuery FullCalendar
*  API @ http://arshaw.com/fullcalendar/
*
*  Angular Calendar Directive that takes in the [eventSources] nested array object as the ng-model and watches it deeply changes.
*       Can also take in multiple event urls as a source object(s) and feed the events per view.
*       The calendar will watch any eventSource array and update itself when a change is made.
*
*/

angular.module('ui.calendar', [])
  .constant('uiCalendarConfig', {calendars: {}})
  .controller('uiCalendarCtrl', ['$scope',
                                 '$locale', function(
                                  $scope,
                                  $locale){

      var sources = $scope.eventSources,
          extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,

          wrapFunctionWithScopeApply = function(functionToWrap){
              return function(){
                  // This may happen outside of angular context, so create one if outside.

                  if ($scope.$root.$$phase) {
                      return functionToWrap.apply(this, arguments);
                  } else {
                      var args = arguments;
                      var self = this;
                      return $scope.$root.$apply(function(){
                          return functionToWrap.apply(self, args);
                      });
                  }
              };
          };

      var eventSerialId = 1;
      // @return {String} fingerprint of the event object and its properties
      this.eventFingerprint = function(e) {
        if (!e._id) {
          e._id = eventSerialId++;
        }
        // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3
        return "" + e._id + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') +
          (e.allDay || '') + (e.className || '') + extraEventSignature({event: e}) || '';
      };

      var sourceSerialId = 1, sourceEventsSerialId = 1;
      // @return {String} fingerprint of the source object and its events array
      this.sourceFingerprint = function(source) {
          var fp = '' + (source.__id || (source.__id = sourceSerialId++)),
              events = angular.isObject(source) && source.events;
          if (events) {
              fp = fp + '-' + (events.__id || (events.__id = sourceEventsSerialId++));
          }
          return fp;
      };

      // @return {Array} all events from all sources
      this.allEvents = function() {
        // do sources.map(&:events).flatten(), but we don't have flatten
        var arraySources = [];
        for (var i = 0, srcLen = sources.length; i < srcLen; i++) {
          var source = sources[i];
          if (angular.isArray(source)) {
            // event source as array
            arraySources.push(source);
          } else if(angular.isObject(source) && angular.isArray(source.events)){
            // event source as object, ie extended form
            var extEvent = {};
            for(var key in source){
              if(key !== '_id' && key !== 'events'){
                 extEvent[key] = source[key];
              }
            }
            for(var eI = 0;eI < source.events.length;eI++){
              angular.extend(source.events[eI],extEvent);
            }
            arraySources.push(source.events);
          }
        }
        return Array.prototype.concat.apply([], arraySources);
      };

      // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens
      // @param {Array|Function} arraySource array of objects to watch
      // @param tokenFn {Function} that returns the token for a given object
      // @return {Object}
      //  subscribe: function(scope, function(newTokens, oldTokens))
      //    called when source has changed. return false to prevent individual callbacks from firing
      //  onAdded/Removed/Changed:
      //    when set to a callback, called each item where a respective change is detected
      this.changeWatcher = function(arraySource, tokenFn) {
        var self;
        var getTokens = function() {
          var array = angular.isFunction(arraySource) ? arraySource() : arraySource;
          var result = [], token, el;
          for (var i = 0, n = array.length; i < n; i++) {
            el = array[i];
            token = tokenFn(el);
            map[token] = el;
            result.push(token);
          }
          return result;
        };

        // @param {Array} a
        // @param {Array} b
        // @return {Array} elements in that are in a but not in b
        // @example
        //  subtractAsSets([6, 100, 4, 5], [4, 5, 7]) // [6, 100]
        var subtractAsSets = function(a, b) {
          var result = [], inB = {}, i, n;
          for (i = 0, n = b.length; i < n; i++) {
            inB[b[i]] = true;
          }
          for (i = 0, n = a.length; i < n; i++) {
            if (!inB[a[i]]) {
              result.push(a[i]);
            }
          }
          return result;
        };

        // Map objects to tokens and vice-versa
        var map = {};

        // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively.
        var applyChanges = function(newTokens, oldTokens) {
          var i, n, el, token;
          var replacedTokens = {};
          var removedTokens = subtractAsSets(oldTokens, newTokens);
          for (i = 0, n = removedTokens.length; i < n; i++) {
            var removedToken = removedTokens[i];
            el = map[removedToken];
            delete map[removedToken];
            var newToken = tokenFn(el);
            // if the element wasn't removed but simply got a new token, its old token will be different from the current one
            if (newToken === removedToken) {
              self.onRemoved(el);
            } else {
              replacedTokens[newToken] = removedToken;
              self.onChanged(el);
            }
          }

          var addedTokens = subtractAsSets(newTokens, oldTokens);
          for (i = 0, n = addedTokens.length; i < n; i++) {
            token = addedTokens[i];
            el = map[token];
            if (!replacedTokens[token]) {
              self.onAdded(el);
            }
          }
        };
        return self = {
          subscribe: function(scope, onArrayChanged) {
            scope.$watch(getTokens, function(newTokens, oldTokens) {
              var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false);
              if (notify) {
                applyChanges(newTokens, oldTokens);
              }
            }, true);
          },
          onAdded: angular.noop,
          onChanged: angular.noop,
          onRemoved: angular.noop
        };
      };

      this.getFullCalendarConfig = function(calendarSettings, uiCalendarConfig){
          var config = {};

          angular.extend(config, uiCalendarConfig);
          angular.extend(config, calendarSettings);

          angular.forEach(config, function(value,key){
            if (typeof value === 'function'){
              config[key] = wrapFunctionWithScopeApply(config[key]);
            }
          });

          return config;
      };

    this.getLocaleConfig = function(fullCalendarConfig) {
      if (!fullCalendarConfig.lang || fullCalendarConfig.useNgLocale) {
        // Configure to use locale names by default
        var tValues = function(data) {
          // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...]
          var r, k;
          r = [];
          for (k in data) {
            r[k] = data[k];
          }
          return r;
        };
        var dtf = $locale.DATETIME_FORMATS;
        return {
          monthNames: tValues(dtf.MONTH),
          monthNamesShort: tValues(dtf.SHORTMONTH),
          dayNames: tValues(dtf.DAY),
          dayNamesShort: tValues(dtf.SHORTDAY)
        };
      }
      return {};
    };
  }])
  .directive('uiCalendar', ['uiCalendarConfig', function(uiCalendarConfig) {
    return {
      restrict: 'A',
      scope: {eventSources:'=ngModel',calendarWatchEvent: '&'},
      controller: 'uiCalendarCtrl',
      link: function(scope, elm, attrs, controller) {

        var sources = scope.eventSources,
            sourcesChanged = false,
            calendar,
            eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint),
            eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint),
            options = null;

        function getOptions(){
          var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {},
              fullCalendarConfig;

          fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig);

          var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig);
          angular.extend(localeFullCalendarConfig, fullCalendarConfig);
          options = { eventSources: sources };
          angular.extend(options, localeFullCalendarConfig);
          //remove calendars from options
          options.calendars = null;

          var options2 = {};
          for(var o in options){
            if(o !== 'eventSources'){
              options2[o] = options[o];
            }
          }
          return JSON.stringify(options2);
        }

        scope.destroy = function(){
          if(calendar && calendar.fullCalendar){
            calendar.fullCalendar('destroy');
          }
          if(attrs.calendar) {
            calendar = uiCalendarConfig.calendars[attrs.calendar] = $(elm).html('');
          } else {
            calendar = $(elm).html('');
          }
        };

        scope.init = function(){
          calendar.fullCalendar(options);
          if(attrs.calendar) {
            uiCalendarConfig.calendars[attrs.calendar] = calendar;
          }
        };

        eventSourcesWatcher.onAdded = function(source) {
          calendar.fullCalendar('addEventSource', source);
          sourcesChanged = true;
        };

        eventSourcesWatcher.onRemoved = function(source) {
          calendar.fullCalendar('removeEventSource', source);
          sourcesChanged = true;
        };

        eventSourcesWatcher.onChanged = function(source) {
          calendar.fullCalendar('refetchEvents');
          sourcesChanged = true;
        };

        eventsWatcher.onAdded = function(event) {
          calendar.fullCalendar('renderEvent', event, (event, true));
        };

        eventsWatcher.onRemoved = function(event) {
          calendar.fullCalendar('removeEvents', event._id);
        };

        eventsWatcher.onChanged = function(event) {
          var clientEvents = calendar.fullCalendar('clientEvents', event._id);
          for (var i = 0; i < clientEvents.length; i++) {
            var clientEvent = clientEvents[i];
            clientEvent = angular.extend(clientEvent, event);
            calendar.fullCalendar('updateEvent', clientEvent);
          }
        };

        eventSourcesWatcher.subscribe(scope);
        eventsWatcher.subscribe(scope, function() {
          if (sourcesChanged === true) {
            sourcesChanged = false;
            // return false to prevent onAdded/Removed/Changed handlers from firing in this case
            return false;
          }
        });

        scope.$watch(getOptions, function(newO,oldO){
            scope.destroy();
            scope.init();
        });
      }
    };
}]);
<commit_msg>calendar fix<commit_after>/*
*  AngularJs Fullcalendar Wrapper for the JQuery FullCalendar
*  API @ http://arshaw.com/fullcalendar/
*
*  Angular Calendar Directive that takes in the [eventSources] nested array object as the ng-model and watches it deeply changes.
*       Can also take in multiple event urls as a source object(s) and feed the events per view.
*       The calendar will watch any eventSource array and update itself when a change is made.
*
*/

angular.module('ui.calendar', [])
  .constant('uiCalendarConfig', {calendars: {}})
  .controller('uiCalendarCtrl', ['$scope',
                                 '$locale', function(
                                  $scope,
                                  $locale){

      var sources = $scope.eventSources,
          extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,

          wrapFunctionWithScopeApply = function(functionToWrap){
              return function(){
                  // This may happen outside of angular context, so create one if outside.

                  if ($scope.$root.$$phase) {
                      return functionToWrap.apply(this, arguments);
                  } else {
                      var args = arguments;
                      var self = this;
                      return $scope.$root.$apply(function(){
                          return functionToWrap.apply(self, args);
                      });
                  }
              };
          };

      var eventSerialId = 1;
      // @return {String} fingerprint of the event object and its properties
      this.eventFingerprint = function(e) {
        if (!e._id) {
          e._id = eventSerialId++;
        }
        // This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3
        return "" + e._id + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') +
          (e.allDay || '') + (e.className || '') + extraEventSignature({event: e}) || '';
      };

      var sourceSerialId = 1, sourceEventsSerialId = 1;
      // @return {String} fingerprint of the source object and its events array
      this.sourceFingerprint = function(source) {
          var fp = '' + (source.__id || (source.__id = sourceSerialId++)),
              events = angular.isObject(source) && source.events;
          if (events) {
              fp = fp + '-' + (events.__id || (events.__id = sourceEventsSerialId++));
          }
          return fp;
      };

      // @return {Array} all events from all sources
      this.allEvents = function() {
        // do sources.map(&:events).flatten(), but we don't have flatten
        var arraySources = [];
        for (var i = 0, srcLen = sources.length; i < srcLen; i++) {
          var source = sources[i];
          if (angular.isArray(source)) {
            // event source as array
            arraySources.push(source);
          } else if(angular.isObject(source) && angular.isArray(source.events)){
            // event source as object, ie extended form
            var extEvent = {};
            for(var key in source){
              if(key !== '_id' && key !== 'events'){
                 extEvent[key] = source[key];
              }
            }
            for(var eI = 0;eI < source.events.length;eI++){
              angular.extend(source.events[eI],extEvent);
            }
            arraySources.push(source.events);
          }
        }
        return Array.prototype.concat.apply([], arraySources);
      };

      // Track changes in array of objects by assigning id tokens to each element and watching the scope for changes in the tokens
      // @param {Array|Function} arraySource array of objects to watch
      // @param tokenFn {Function} that returns the token for a given object
      // @return {Object}
      //  subscribe: function(scope, function(newTokens, oldTokens))
      //    called when source has changed. return false to prevent individual callbacks from firing
      //  onAdded/Removed/Changed:
      //    when set to a callback, called each item where a respective change is detected
      this.changeWatcher = function(arraySource, tokenFn) {
        var self;
        var getTokens = function() {
          var array = angular.isFunction(arraySource) ? arraySource() : arraySource;
          var result = [], token, el;
          for (var i = 0, n = array.length; i < n; i++) {
            el = array[i];
            token = tokenFn(el);
            map[token] = el;
            result.push(token);
          }
          return result;
        };

        // @param {Array} a
        // @param {Array} b
        // @return {Array} elements in that are in a but not in b
        // @example
        //  subtractAsSets([6, 100, 4, 5], [4, 5, 7]) // [6, 100]
        var subtractAsSets = function(a, b) {
          var result = [], inB = {}, i, n;
          for (i = 0, n = b.length; i < n; i++) {
            inB[b[i]] = true;
          }
          for (i = 0, n = a.length; i < n; i++) {
            if (!inB[a[i]]) {
              result.push(a[i]);
            }
          }
          return result;
        };

        // Map objects to tokens and vice-versa
        var map = {};

        // Compare newTokens to oldTokens and call onAdded, onRemoved, and onChanged handlers for each affected event respectively.
        var applyChanges = function(newTokens, oldTokens) {
          var i, n, el, token;
          var replacedTokens = {};
          var removedTokens = subtractAsSets(oldTokens, newTokens);
          for (i = 0, n = removedTokens.length; i < n; i++) {
            var removedToken = removedTokens[i];
            el = map[removedToken];
            delete map[removedToken];
            var newToken = tokenFn(el);
            // if the element wasn't removed but simply got a new token, its old token will be different from the current one
            if (newToken === removedToken) {
              self.onRemoved(el);
            } else {
              replacedTokens[newToken] = removedToken;
              self.onChanged(el);
            }
          }

          var addedTokens = subtractAsSets(newTokens, oldTokens);
          for (i = 0, n = addedTokens.length; i < n; i++) {
            token = addedTokens[i];
            el = map[token];
            if (!replacedTokens[token]) {
              self.onAdded(el);
            }
          }
        };
        return self = {
          subscribe: function(scope, onArrayChanged) {
            scope.$watch(getTokens, function(newTokens, oldTokens) {
              var notify = !(onArrayChanged && onArrayChanged(newTokens, oldTokens) === false);
              if (notify) {
                applyChanges(newTokens, oldTokens);
              }
            }, true);
          },
          onAdded: angular.noop,
          onChanged: angular.noop,
          onRemoved: angular.noop
        };
      };

      this.getFullCalendarConfig = function(calendarSettings, uiCalendarConfig){
          var config = {};

          angular.extend(config, uiCalendarConfig);
          angular.extend(config, calendarSettings);

          angular.forEach(config, function(value,key){
            if (typeof value === 'function'){
              config[key] = wrapFunctionWithScopeApply(config[key]);
            }
          });

          return config;
      };

    this.getLocaleConfig = function(fullCalendarConfig) {
      if (!fullCalendarConfig.lang || fullCalendarConfig.useNgLocale) {
        // Configure to use locale names by default
        var tValues = function(data) {
          // convert {0: "Jan", 1: "Feb", ...} to ["Jan", "Feb", ...]
          var r, k;
          r = [];
          for (k in data) {
            r[k] = data[k];
          }
          return r;
        };
        var dtf = $locale.DATETIME_FORMATS;
        return {
          monthNames: tValues(dtf.MONTH),
          monthNamesShort: tValues(dtf.SHORTMONTH),
          dayNames: tValues(dtf.DAY),
          dayNamesShort: tValues(dtf.SHORTDAY)
        };
      }
      return {};
    };
  }])
  .directive('uiCalendar', ['uiCalendarConfig', function(uiCalendarConfig) {
    return {
      restrict: 'A',
      scope: {eventSources:'=ngModel',calendarWatchEvent: '&'},
      controller: 'uiCalendarCtrl',
      link: function(scope, elm, attrs, controller) {

        var sources = scope.eventSources,
            sourcesChanged = false,
            calendar,
            eventSourcesWatcher = controller.changeWatcher(sources, controller.sourceFingerprint),
            eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventFingerprint),
            options = null;

        function getOptions(){
          var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : {},
              fullCalendarConfig;

          fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig);

          var localeFullCalendarConfig = controller.getLocaleConfig(fullCalendarConfig);
          angular.extend(localeFullCalendarConfig, fullCalendarConfig);
          options = { eventSources: sources };
          angular.extend(options, localeFullCalendarConfig);
          //remove calendars from options
          options.calendars = null;

          var options2 = {};
          for(var o in options){
            if(o !== 'eventSources'){
              options2[o] = options[o];
            }
          }
          return JSON.stringify(options2);
        }

        scope.destroy = function(){
          if(calendar && calendar.fullCalendar){
            calendar.fullCalendar('destroy');
          }
          if(attrs.calendar) {
            calendar = uiCalendarConfig.calendars[attrs.calendar] = $(elm).html('');
          } else {
            calendar = $(elm).html('');
          }
        };

        scope.init = function(){
          calendar.fullCalendar(options);
          if(attrs.calendar) {
            uiCalendarConfig.calendars[attrs.calendar] = calendar;
          }
        };

        eventSourcesWatcher.onAdded = function(source) {
          calendar.fullCalendar('addEventSource', source);
          sourcesChanged = true;
        };

        eventSourcesWatcher.onRemoved = function(source) {
          calendar.fullCalendar('removeEventSource', source);
          sourcesChanged = true;
        };

        eventSourcesWatcher.onChanged = function(source) {
          calendar.fullCalendar('refetchEvents');
          sourcesChanged = true;
        };

        eventsWatcher.onAdded = function(event) {
          calendar.fullCalendar('renderEvent', event, true);
        };

        eventsWatcher.onRemoved = function(event) {
          calendar.fullCalendar('removeEvents', event._id);
        };

        eventsWatcher.onChanged = function(event) {
          var clientEvents = calendar.fullCalendar('clientEvents', event._id);
          for (var i = 0; i < clientEvents.length; i++) {
            var clientEvent = clientEvents[i];
            clientEvent = angular.extend(clientEvent, event);
            calendar.fullCalendar('updateEvent', clientEvent);
          }
        };

        eventSourcesWatcher.subscribe(scope);
        eventsWatcher.subscribe(scope, function() {
          if (sourcesChanged === true) {
            sourcesChanged = false;
            // return false to prevent onAdded/Removed/Changed handlers from firing in this case
            return false;
          }
        });

        scope.$watch(getOptions, function(newO,oldO){
            scope.destroy();
            scope.init();
        });
      }
    };
}]);
<|endoftext|>