Skip to content

Commit

Permalink
Alerts (#12)
Browse files Browse the repository at this point in the history
* merge

* refactor alert to accordion, add cypress test

* revert change to business store for slim

* fix lint

* couple more fixes

* fix merge error add padding to open alert

* remove outdated component test for Alerts

* fix lint

* some tweaks from pr feedback, and add cypress screenshots to gitignore
  • Loading branch information
BrandonSharratt authored Jul 9, 2024
1 parent eec45a3 commit 2602e86
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ logs

# local ide stuff
.vscode

#Cypress screenshots
cypress/screenshots
26 changes: 26 additions & 0 deletions cypress/e2e/components/alerts.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
context('Business dashboard -> Alerts main component', () => {
it('Alerts are rendered when business has alerts', () => {
cy.visitBusinessDashFor('businessInfo/ben/active-frozen-not_in_good_standing.json')

// the alerts exist
cy.get('[data-cy="alerts-display"]').should('exist')
cy.get('[data-cy="alert-display"]').should('have.length', 4)

// accordion headers exist
cy.get('[data-cy="alert-display"]').eq(0).contains('This business is frozen').should('be.visible')
cy.get('[data-cy="alert-display"]').eq(2).contains('This business is not in good standing').should('be.visible')

// expand the second item
cy.get('[data-cy="alert-display"]').eq(2).find('[data-cy="alert-icon"]').click()

cy.get('[data-cy="alert-description"]').eq(0).should('not.be.visible')
cy.get('[data-cy="alert-description"]').eq(1).contains('Email').should('be.visible')
})

it('Shouldn\'t show alerts when business has no alerts', () => {
cy.visitBusinessDash('BC0871427', 'SP')

cy.get('[data-cy="alerts-display"]').should('not.exist')
cy.get('[data-cy="alert-display"]').should('not.exist')
})
})
26 changes: 26 additions & 0 deletions src/components/bcros/alert/List.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<div data-cy="alerts-display">
<UAccordion
color="black"
variant="outline"
size="sm"
:items="alerts"
>
<template #default="{ item, open }">
<BcrosAlert
:alert="item"
:open="open"
:show-description="false"
:contact="contact"
/>
</template>
<template #item="{ item, open }">
<BcrosAlert :open="open" :alert="item" :show-header="false" :contact="contact" />
</template>
</UAccordion>
</div>
</template>

<script setup lang="ts">
defineProps<{ alerts: Array<Partial<AlertI>>, contact: boolean }>()
</script>
102 changes: 102 additions & 0 deletions src/components/bcros/alert/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<template>
<div data-cy="alert-display" class="px-3 py-3">
<UIcon
v-if="showHeader"
:class="`${iconColour} mr-2 font-semibold`"
:name="iconName"
data-cy="alert-icon"
/>
<span v-if="showHeader" class="font-semibold flex-auto">{{ alertHeader }}</span>
<UButton
v-if="showHeader"
color="primary"
:icon="actualExpanded ? 'i-mdi-chevron-up' : 'i-mdi-chevron-down'"
:label="actualExpanded ? 'Hide Details' : 'View Details'"
trailing
variant="ghost"
class="float-right"
:ui="{ icon: { base: 'transition-all' } }"
@click="toggleExpanded()"
/>
<div v-if="actualExpanded && showDescription" data-cy="alert-description">
<p>{{ $t(alertDescription) }}</p>
<p v-if="contact" class="mt-3">
{{ contact }}:
</p>
<p v-if="contact" class="mt-3">
<bcros-contact-info class="font-normal font-16 mt-4" :contacts="getContactInfo('registries')" />
</p>
</div>
</div>
</template>

<script setup lang="ts">
import { getAlertIcon, getAlertHeader, getAlertColour } from '~/utils/alert'
interface Props {
alert: Partial<AlertI>,
contact: boolean,
showHeader?: boolean,
showDescription?: boolean,
open?: boolean,
}
const props = withDefaults(defineProps<Props>(), {
showHeader: true,
showDescription: true
})
const expanded = props.showHeader ? ref(false) : ref(true)
const actualExpanded = computed((): boolean => {
return expanded.value || props.open
})
const toggleExpanded = () => {
if (typeof props.open === 'boolean') {
return
}
expanded.value = !expanded.value
}
const t = useNuxtApp().$i18n.t
const iconName = computed((): string => {
return getAlertIcon(props.alert)
})
const iconColour = computed((): string => {
return getAlertColour(props.alert)
})
const alertHeader = computed((): string => {
return getAlertHeader(props.alert)
})
const alertDescription = computed((): string => {
const date: string = props.alert?.date || 'unknown'
const description = t(props.alert.alertType
? 'alerts.descriptions.' + props.alert.alertType
: props.alert.description)
description.replaceAll('[date]', date)
return description
})
const contact = computed((): string => {
// 1 - assistance
// 2 - questions
// 3 - must contact
// 4 - action
if (props.alert.alertType === AlertTypesE.AMALGAMATION) {
return t('alerts.contact2')
}
if (props.alert.alertType === AlertTypesE.COMPLIANCE) {
return t('alerts.contact3')
}
if ((props.alert.alertType === AlertTypesE.MISSINGINFO) || (props.alert.alertType === AlertTypesE.STANDING)) {
return t('alerts.contact4')
}
return t('alerts.contact')
})
</script>
7 changes: 7 additions & 0 deletions src/enums/alert-severity-e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** Alert Severities. */
export enum AlertSeverityE {
SUCCESS = 'success',
ERROR = 'error',
INFO = 'info',
WARNING = 'warning'
}
11 changes: 11 additions & 0 deletions src/enums/alert-types-e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** Alert Types. */
export enum AlertTypesE {
FROZEN = 'frozen',
DISABLED = 'disabled',
STANDING = 'standing',
DISSOLUTION = 'dissolution',
COMPLIANCE = 'compliance',
MISSINGINFO = 'missinginfo',
NONENTITY = 'nonentity',
AMALGAMATION = 'amalgamation',
}
9 changes: 9 additions & 0 deletions src/interfaces/alert-i.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { AlertTypesE, AlertSeverityE } from '#imports'

export interface AlertI {
severity: AlertSeverityE | undefined
alertType: AlertTypesE | undefined
text: string | undefined
description: string | undefined
date: any | undefined
}
27 changes: 27 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
},
"section": {
"toDo": "To Do",
"alert": "Alerts",
"filingHistory": "Recent Filing History",
"businessAddresses": "Business Addresses",
"officeAddresses": "Office Addresses",
Expand Down Expand Up @@ -202,5 +203,31 @@
"isPendingDissolution": "You cannot view or change business information while the business is pending dissolution."
}
}
},
"alerts": {
"headers": {
"frozen": "This business is frozen",
"standing": "This business is not in good standing",
"disabled": "This business is disabled",
"dissolution": "Urgent - this business is in the process of being dissolved",
"compliance": "This business is not in compliance",
"missinginfo": "Missing Information",
"nonentity": "This corporation is not an entity",
"amalgamation": "This corporation is part of an amalgamation and is scheduled to become historical on [date]"
},
"descriptions": {
"frozen": "This business is frozen and therefore no filings can be completed at this time.",
"standing": "The most common reason a business is not in good standing is an overdue annual report. Any outstanding annual reports must filed to bring the business back into good standing.",
"disabled": "This business is disabled",
"dissolution": "This means that the business will be struck from the Corporate Registry in [date] days due to overdue annual reports. Please file the annual reports immediately to bring this business back into good standing or request a delay of dissolution if more time is needed.",
"compliance": "",
"missinginfo": "BC Registries is missing information about your business (e.g., business start date, nature of business, business address, etc.). Please contact BC Registries to input any missing business information. Missing information must be entered before you can file changes or dissolve this business.",
"nonentity": "This corporation is not an entity",
"amalgamation": ""
},
"contact": "For assitance, please contact BC Registries staff",
"contact2": "If you have any questions, please contact BC Registries staff",
"contact3": "To resolve this issue, you MUST contact BC Registries staff",
"contact4": "If further action is required, please contact BC Registries staff"
}
}
56 changes: 56 additions & 0 deletions src/pages/dashboard.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<template>
<div class="mt-8 mb-16 flex flex-wrap" data-cy="business-dashboard">
<div class="w-full md:w-9/12">
<BcrosSection v-if="alerts && alerts.length>0" class="pb-5" name="alerts">
<template #header>
{{ $t('title.section.alert') }}({{ alerts.length }})
</template>
<BcrosAlertList :alerts="alerts" :contact="true" />
</BcrosSection>
<BcrosSection name="todo">
<template #header>
{{ $t('title.section.toDo') }}
Expand Down Expand Up @@ -173,4 +179,54 @@ const loadBusinessInfo = () => {
onBeforeMount(() => {
loadBusinessInfo()
})
const alerts = computed((): Array<Partial<AlertI>> => {
const allWarnings = currentBusiness.value?.warnings || []
const alertList: Array<Partial<AlertI>> = []
if (currentBusiness.value?.adminFreeze) {
alertList.push({ alertType: AlertTypesE.FROZEN })
}
if ((currentBusiness.value?.goodStanding === false) ||
(allWarnings.some(item => item.warningType === WarningTypes.NOT_IN_GOOD_STANDING))) {
alertList.push({ alertType: AlertTypesE.STANDING })
}
if ((allWarnings.some(item => item.warningType === WarningTypes.INVOLUNTARY_DISSOLUTION)) ||
(currentBusiness.value?.inDissolution)) {
let days = null
const warning = allWarnings.find(item =>
item.warningType?.includes(WarningTypes.INVOLUNTARY_DISSOLUTION)
)
const targetDissolutionDate = warning?.data?.targetDissolutionDate
const daysDifference = daysBetweenTwoDates(
new Date(), new Date(targetDissolutionDate)
)
if (daysDifference) {
days = daysDifference
}
alertList.push({ alertType: AlertTypesE.DISSOLUTION, date: days })
}
if (allWarnings.some(item => item.warningType === WarningTypes.COMPLIANCE)) {
alertList.push({ alertType: AlertTypesE.COMPLIANCE })
}
if (currentBusiness.value?.state !== 'ACTIVE') {
alertList.push({ alertType: AlertTypesE.DISABLED })
}
if (allWarnings.some(item => item.warningType === WarningTypes.FUTURE_EFFECTIVE_AMALGAMATION)) {
const warning = allWarnings.find(item =>
item.warningType?.includes(WarningTypes.FUTURE_EFFECTIVE_AMALGAMATION)
)
const amalDate = warning?.data?.amalgamationDate as string
alertList.push({ alertType: AlertTypesE.AMALGAMATION, date: amalDate })
}
if (allWarnings.some(item => item.warningType === WarningTypes.MISSING_REQUIRED_BUSINESS_INFO)) {
alertList.push({ alertType: AlertTypesE.MISSINGINFO })
}
return alertList
})
</script>
42 changes: 42 additions & 0 deletions src/utils/alert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export const getAlertHeader = function(alert: Partial<AlertI>): string {
const t = useNuxtApp().$i18n.t
return t(alert.alertType ? 'alerts.headers.' + alert.alertType : alert.text)
}

export const getAlertIcon = function(alert: Partial<AlertI>): string {
if (alert.alertType) {
return 'i-mdi-alert'
}

switch (alert.severity?.toLowerCase()) {
case 'error':
return 'i-mdi-alert-circle'
case 'warning':
return 'i-mdi-alert'
case 'info':
return 'i-mdi-information'
case 'success':
return 'i-mdi-check'
default:
return 'i-mdi-alert-circle'
}
}

export const getAlertColour = function(alert: Partial<AlertI>): string {
if (alert.alertType) {
return 'text-yellow-500'
}

switch (alert.severity?.toLowerCase()) {
case 'error':
return 'text-red-500'
case 'warning':
return 'text-yellow-500'
case 'info':
return 'text-blue-500'
case 'success':
return 'text-green-500'
default:
return 'text-yellow-500'
}
}
24 changes: 24 additions & 0 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import moment from 'moment'

const MS_IN_A_DAY = (1000 * 60 * 60 * 24)

/** Return the date string as a date
* @param dateString expected dateString format: YYYY-MM-DD
*/
Expand Down Expand Up @@ -42,3 +44,25 @@ export function datetimeStringToDateString (datetimeString: string) {
}

export const todayIsoDateString = () => dateToString(new Date(), 'YYYY-MM-DD')

export function daysBetweenTwoDates (initialDate: Date, d: Date) {
// safety check
if (initialDate !== new Date(initialDate)) {
return NaN
}
if (d !== new Date(d)) {
return NaN
}

// set "date" to 12:00 am Pacific
d.setHours(0, 0, 0, 0)

// compute "initialDate" at 12:00 am Pacific
initialDate.setHours(0, 0, 0, 0)

// calculate difference between "date" and "initialDate"
// (result should be a whole number)
const diff = (d.valueOf() - initialDate.valueOf()) / MS_IN_A_DAY

return Math.round(diff)
}

0 comments on commit 2602e86

Please sign in to comment.