From ced6cf4685a6bb300ee9d1f4baf4a1ebfeb72043 Mon Sep 17 00:00:00 2001 From: deetz99 <73151365+deetz99@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:41:20 -0800 Subject: [PATCH] Host/PM - UI: Principal Residence step draft (#332) * initial principal residence step work * list types * remove set error * doc upload checkpoint --- strr-base-web/app/utils/sleep.ts | 8 + strr-base-web/tailwind.config.ts | 4 + strr-host-pm-web/.eslintrc | 4 +- .../app/components/document/list/Item.vue | 50 +++ .../app/components/document/list/index.vue | 11 + .../app/components/document/upload/Button.vue | 62 ++++ .../app/components/document/upload/Select.vue | 63 ++++ .../components/form/PrincipalResidence.vue | 311 ++++++++++++++++++ .../app/enums/document-upload-type.ts | 15 + .../app/enums/pr-exemption-other-provider.ts | 8 + .../app/enums/pr-exemption-reason.ts | 6 + .../app/interfaces/ui-document.ts | 8 + strr-host-pm-web/app/locales/en-CA.ts | 51 +++ strr-host-pm-web/app/pages/application.vue | 4 +- strr-host-pm-web/app/stores/document.ts | 163 +++++++++ strr-host-pm-web/package.json | 1 + strr-host-pm-web/pnpm-lock.yaml | 8 + 17 files changed, 774 insertions(+), 3 deletions(-) create mode 100644 strr-base-web/app/utils/sleep.ts create mode 100644 strr-host-pm-web/app/components/document/list/Item.vue create mode 100644 strr-host-pm-web/app/components/document/list/index.vue create mode 100644 strr-host-pm-web/app/components/document/upload/Button.vue create mode 100644 strr-host-pm-web/app/components/document/upload/Select.vue create mode 100644 strr-host-pm-web/app/components/form/PrincipalResidence.vue create mode 100644 strr-host-pm-web/app/enums/document-upload-type.ts create mode 100644 strr-host-pm-web/app/enums/pr-exemption-other-provider.ts create mode 100644 strr-host-pm-web/app/enums/pr-exemption-reason.ts create mode 100644 strr-host-pm-web/app/interfaces/ui-document.ts create mode 100644 strr-host-pm-web/app/stores/document.ts diff --git a/strr-base-web/app/utils/sleep.ts b/strr-base-web/app/utils/sleep.ts new file mode 100644 index 000000000..fbed9cf25 --- /dev/null +++ b/strr-base-web/app/utils/sleep.ts @@ -0,0 +1,8 @@ +/** + * Pauses for a specified number of milliseconds. + * @param {number} [ms=3000] - The pause duration, in milliseconds. Default of 3000ms. + * @returns {Promise} empty promise. + */ +export function sleep (ms: number = 3000): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/strr-base-web/tailwind.config.ts b/strr-base-web/tailwind.config.ts index a8b4e0a67..87463ea24 100644 --- a/strr-base-web/tailwind.config.ts +++ b/strr-base-web/tailwind.config.ts @@ -4,6 +4,10 @@ module.exports = { presets: [require('@daxiom/nuxt-core-layer-test/tailwind.config')], theme: { extend: { + listStyleType: { + alpha: 'lower-alpha', + upperAlpha: 'upper-alpha' + }, maxWidth: { bcGovInput: '600px' }, diff --git a/strr-host-pm-web/.eslintrc b/strr-host-pm-web/.eslintrc index 141577c5d..10b62c919 100644 --- a/strr-host-pm-web/.eslintrc +++ b/strr-host-pm-web/.eslintrc @@ -23,7 +23,9 @@ "app-inner-container", "connect-date-picker", "connect-date-picker__err", - "prose-bcGov" + "prose-bcGov", + "list-alpha", + "list-upperAlpha" ] }], "no-use-before-define": "off" diff --git a/strr-host-pm-web/app/components/document/list/Item.vue b/strr-host-pm-web/app/components/document/list/Item.vue new file mode 100644 index 000000000..a3b8ef875 --- /dev/null +++ b/strr-host-pm-web/app/components/document/list/Item.vue @@ -0,0 +1,50 @@ + + diff --git a/strr-host-pm-web/app/components/document/list/index.vue b/strr-host-pm-web/app/components/document/list/index.vue new file mode 100644 index 000000000..b5e88bbf3 --- /dev/null +++ b/strr-host-pm-web/app/components/document/list/index.vue @@ -0,0 +1,11 @@ + + diff --git a/strr-host-pm-web/app/components/document/upload/Button.vue b/strr-host-pm-web/app/components/document/upload/Button.vue new file mode 100644 index 000000000..34bca39be --- /dev/null +++ b/strr-host-pm-web/app/components/document/upload/Button.vue @@ -0,0 +1,62 @@ + + + diff --git a/strr-host-pm-web/app/components/document/upload/Select.vue b/strr-host-pm-web/app/components/document/upload/Select.vue new file mode 100644 index 000000000..b9a594291 --- /dev/null +++ b/strr-host-pm-web/app/components/document/upload/Select.vue @@ -0,0 +1,63 @@ + + diff --git a/strr-host-pm-web/app/components/form/PrincipalResidence.vue b/strr-host-pm-web/app/components/form/PrincipalResidence.vue new file mode 100644 index 000000000..9a3df5509 --- /dev/null +++ b/strr-host-pm-web/app/components/form/PrincipalResidence.vue @@ -0,0 +1,311 @@ + + + diff --git a/strr-host-pm-web/app/enums/document-upload-type.ts b/strr-host-pm-web/app/enums/document-upload-type.ts new file mode 100644 index 000000000..0ffd6d04a --- /dev/null +++ b/strr-host-pm-web/app/enums/document-upload-type.ts @@ -0,0 +1,15 @@ +export enum DocumentUploadType { + BC_DRIVERS_LICENSE = 'BC_DRIVERS_LICENSE', + PROPERTY_ASSESSMENT_NOTICE = 'PROPERTY_ASSESSMENT_NOTICE', + SPEC_TAX_CONFIRMATION = 'SPEC_TAX_CONFIRMATION', + HOG_DECLARATION = 'HOG_DECLARATION', + ICBC_CERTIFICATE_OF_INSURANCE = 'ICBC_CERTIFICATE_OF_INSURANCE', + HOME_INSURANCE_SUMMARY = 'HOME_INSURANCE_SUMMARY', + PROPERTY_TAX_NOTICE = 'PROPERTY_TAX_NOTICE', + UTILITY_BILL = 'UTILITY_BILL', + GOVT_OR_CROWN_CORP_OFFICIAL_NOTICE = 'GOVT_OR_CROWN_CORP_OFFICIAL_NOTICE', + TENANCY_AGREEMENT = 'TENANCY_AGREEMENT', + RENT_RECEIPT_OR_BANK_STATEMENT = 'RENT_RECEIPT_OR_BANK_STATEMENT', + LOCAL_GOVT_BUSINESS_LICENSE = 'LOCAL_GOVT_BUSINESS_LICENSE', + OTHERS = 'OTHERS' +} diff --git a/strr-host-pm-web/app/enums/pr-exemption-other-provider.ts b/strr-host-pm-web/app/enums/pr-exemption-other-provider.ts new file mode 100644 index 000000000..065aed534 --- /dev/null +++ b/strr-host-pm-web/app/enums/pr-exemption-other-provider.ts @@ -0,0 +1,8 @@ +export enum PrExemptionOtherProvider { + TIMESHARE = 'TIMESHARE', + FRACTIONAL_OWNERSHIP = 'FRACTIONAL_OWNERSHIP', + HOME_EXCHANGE = 'HOME_EXCHANGE', + LODGE_OPERATOR = 'LODGE_OPERATOR', + EDUCATIONAL_INSTITUTION = 'EDUCATIONAL_INSTITUTION', + STRATA_GUEST_SUITE = 'STRATA_GUEST_SUITE' +} diff --git a/strr-host-pm-web/app/enums/pr-exemption-reason.ts b/strr-host-pm-web/app/enums/pr-exemption-reason.ts new file mode 100644 index 000000000..e801800ff --- /dev/null +++ b/strr-host-pm-web/app/enums/pr-exemption-reason.ts @@ -0,0 +1,6 @@ +export enum PrExemptionReason { + EXEMPT_COMMUNITY = 'EXEMPT_COMMUNITY', + STRATA_HOTEL = 'STRATA_HOTEL', + FARM_LAND = 'FARM_LAND', + OTHER = 'OTHER' +} diff --git a/strr-host-pm-web/app/interfaces/ui-document.ts b/strr-host-pm-web/app/interfaces/ui-document.ts new file mode 100644 index 000000000..b0bf51e83 --- /dev/null +++ b/strr-host-pm-web/app/interfaces/ui-document.ts @@ -0,0 +1,8 @@ +export interface UiDocument { + file: File + apiDoc: ApiDocument + name: string + id: string + loading: boolean + type: DocumentUploadType +} diff --git a/strr-host-pm-web/app/locales/en-CA.ts b/strr-host-pm-web/app/locales/en-CA.ts index 1776fe8ba..23af6fa2c 100644 --- a/strr-host-pm-web/app/locales/en-CA.ts +++ b/strr-host-pm-web/app/locales/en-CA.ts @@ -6,6 +6,47 @@ export default { HOSTREG_2: 'STR Application Fee' } }, + form: { + pr: { + declaration: { + intro: 'As required by section 14 (2) of the {italicStart}Short-Term Accommodations Rental Act{italicEnd} (the Act), I declare the property host will comply with the principal residence restriction in the Act and provide the short-term rental accommodation services described in this registration in one or both of:', + list: { + a: 'the property host’s principal residence,', + b: "not more than one secondary suite or other accessory dwelling unit that is on the land parcel associated with the property host's principal residence." + }, + agreement: 'I understand that if the property host does not comply with the requirement to provide the short-term rental accommodation services in the principal residence, the property host may be subject to enforcement action under Part 4 of the Act, including being ordered to pay an administrative penalty.' + }, + exemptReason: { + EXEMPT_COMMUNITY: 'Located in exempt community', + STRATA_HOTEL: 'Eligible strata hotel or motel', + FARM_LAND: 'Farm land (BC Assessment Farm Class 9)', + OTHER: 'Other exempted accommodation service provider' + }, + exemptOtherProvider: { + TIMESHARE: 'Timeshare', + FRACTIONAL_OWNERSHIP: 'Fractional Ownership', + HOME_EXCHANGE: 'Home Exchange', + LODGE_OPERATOR: 'Lodge (operator of outdoor recreational activity)', + EDUCATIONAL_INSTITUTION: 'Educational institution accommodation (Student or Employee) (off campus)', + STRATA_GUEST_SUITE: 'Strata corporation guest suite' + }, + docType: { + BC_DRIVERS_LICENSE: "BC Driver's License", + PROPERTY_ASSESSMENT_NOTICE: 'Property Assessment Notice', + SPEC_TAX_CONFIRMATION: 'Speculation and Vacancy Tax Confirmation', + HOG_DECLARATION: 'Home Owner Grant declaration', + ICBC_CERTIFICATE_OF_INSURANCE: 'ICBC Certificate of Insurance', + HOME_INSURANCE_SUMMARY: 'Home Insurance Summary', + PROPERTY_TAX_NOTICE: 'Property Tax Notice', + UTILITY_BILL: 'Utility Bill', + GOVT_OR_CROWN_CORP_OFFICIAL_NOTICE: 'Government or Crown Corporation Official Notice', + TENANCY_AGREEMENT: 'Tenancy Agreement', + RENT_RECEIPT_OR_BANK_STATEMENT: 'Rent Receipt or Bank Statement', + LOCAL_GOVT_BUSINESS_LICENSE: 'Local Government Business License', + OTHERS: 'Other Proof Document (subject to review by registry staff)' + } + } + }, strr: { step: { stepperLabel: 'Short-Term Rental Application Step Navigation', @@ -114,6 +155,16 @@ export default { createAccount: { title: 'Error creating account', description: 'We could not create your account at this time. Please try again or if this issue persists, please contact us.' + }, + docUpload: { + fileSize: { + title: 'Error Uploading Document', + description: 'File size too large. Please only upload files less than 10mb.' + }, + generic: { + title: 'Error Uploading Document', + description: 'Something went wrong when uploading the file, only pdfs and files less than 10mb are accepted.' + } } }, label: { diff --git a/strr-host-pm-web/app/pages/application.vue b/strr-host-pm-web/app/pages/application.vue index 9a72cc4bf..3573a444c 100644 --- a/strr-host-pm-web/app/pages/application.vue +++ b/strr-host-pm-web/app/pages/application.vue @@ -283,8 +283,8 @@ setBreadcrumbs([
-
- +
+
{ + const { t } = useI18n() + const { $strrApi } = useNuxtApp() + const strrModal = useStrrModals() + + const storedDocuments = ref([]) + const apiDocuments = computed(() => storedDocuments.value.map(item => item.apiDoc)) + const selectedDocType = ref(undefined) + + const docTypeOptions = [ + { + label: t(`form.pr.docType.${DocumentUploadType.BC_DRIVERS_LICENSE}`), + value: DocumentUploadType.BC_DRIVERS_LICENSE + }, + { + label: t(`form.pr.docType.${DocumentUploadType.PROPERTY_ASSESSMENT_NOTICE}`), + value: DocumentUploadType.PROPERTY_ASSESSMENT_NOTICE + }, + { + label: t(`form.pr.docType.${DocumentUploadType.SPEC_TAX_CONFIRMATION}`), + value: DocumentUploadType.SPEC_TAX_CONFIRMATION + }, + { + label: t(`form.pr.docType.${DocumentUploadType.HOG_DECLARATION}`), + value: DocumentUploadType.HOG_DECLARATION + }, + { + label: t(`form.pr.docType.${DocumentUploadType.ICBC_CERTIFICATE_OF_INSURANCE}`), + value: DocumentUploadType.ICBC_CERTIFICATE_OF_INSURANCE + }, + { + label: t(`form.pr.docType.${DocumentUploadType.HOME_INSURANCE_SUMMARY}`), + value: DocumentUploadType.HOME_INSURANCE_SUMMARY + }, + { + label: t(`form.pr.docType.${DocumentUploadType.PROPERTY_TAX_NOTICE}`), + value: DocumentUploadType.PROPERTY_TAX_NOTICE + }, + { + label: t(`form.pr.docType.${DocumentUploadType.UTILITY_BILL}`), + value: DocumentUploadType.UTILITY_BILL + }, + { + label: t(`form.pr.docType.${DocumentUploadType.GOVT_OR_CROWN_CORP_OFFICIAL_NOTICE}`), + value: DocumentUploadType.GOVT_OR_CROWN_CORP_OFFICIAL_NOTICE + }, + { + label: t(`form.pr.docType.${DocumentUploadType.TENANCY_AGREEMENT}`), + value: DocumentUploadType.TENANCY_AGREEMENT + }, + { + label: t(`form.pr.docType.${DocumentUploadType.RENT_RECEIPT_OR_BANK_STATEMENT}`), + value: DocumentUploadType.RENT_RECEIPT_OR_BANK_STATEMENT + }, + { + label: t(`form.pr.docType.${DocumentUploadType.LOCAL_GOVT_BUSINESS_LICENSE}`), + value: DocumentUploadType.LOCAL_GOVT_BUSINESS_LICENSE + }, + { + label: t(`form.pr.docType.${DocumentUploadType.OTHERS}`), + value: DocumentUploadType.OTHERS + } + ] + + async function addStoredDocument (doc: File): Promise { + const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10mb + if (doc.size > MAX_FILE_SIZE) { + strrModal.openErrorModal(t('error.docUpload.fileSize.title'), t('error.docUpload.fileSize.description'), false) + return + } + + const uiDoc: UiDocument = { + file: doc, + apiDoc: {} as ApiDocument, + name: doc.name, + type: selectedDocType.value!, + id: uuidv4(), + loading: true + } + + storedDocuments.value.push(uiDoc) + + selectedDocType.value = undefined + + await sleep(3000) // dev only + await postDocument(uiDoc) + } + + function updateStoredDocument ( + id: string, + key: K, + value: UiDocument[K] + ) { + const docToUpdate = storedDocuments.value.find(doc => doc.id === id) + if (docToUpdate) { + docToUpdate[key] = value + } + } + + async function removeStoredDocument (uiDoc: UiDocument) { + const index = storedDocuments.value.findIndex(item => uiDoc.id === item.id) + storedDocuments.value.splice(index, 1) + if (uiDoc.apiDoc.fileKey) { + await deleteDocument(uiDoc.apiDoc.fileKey) + } + } + + async function postDocument (uiDoc: UiDocument): Promise { + try { + // create payload + const formData = new FormData() + formData.append('file', uiDoc.file) + formData.append('documentType', uiDoc.type) + + // submit file + const res = await $strrApi('/documents', { + method: 'POST', + body: formData + }) + + // update ui object with backend response + updateStoredDocument(uiDoc.id, 'apiDoc', res) + } catch (e) { + logFetchError(e, 'Error uploading document') + strrModal.openErrorModal(t('error.docUpload.generic.title'), t('error.docUpload.generic.description'), false) + await removeStoredDocument(uiDoc) + } finally { + // cleanup loading on ui object + updateStoredDocument(uiDoc.id, 'loading', false) + } + } + + async function deleteDocument (fileKey: string) { + try { + await $strrApi(`/documents/${fileKey}`, { + method: 'DELETE' + }) + } catch (e) { + logFetchError(e, `Error deleting document: ${fileKey}`) + } + } + + const $reset = () => { + storedDocuments.value = [] + } + + return { + apiDocuments, + storedDocuments, + selectedDocType, + docTypeOptions, + postDocument, + deleteDocument, + addStoredDocument, + removeStoredDocument, + $reset + } +}) diff --git a/strr-host-pm-web/package.json b/strr-host-pm-web/package.json index e45dcb6f7..a1ee18e00 100644 --- a/strr-host-pm-web/package.json +++ b/strr-host-pm-web/package.json @@ -45,6 +45,7 @@ "@vuepic/vue-datepicker": "^9.0.3", "country-codes-list": "^1.6.11", "nuxt": "^3.12.3", + "uuid": "^11.0.3", "vue-country-flag-next": "^2.3.2" } } diff --git a/strr-host-pm-web/pnpm-lock.yaml b/strr-host-pm-web/pnpm-lock.yaml index 1ee8946db..dc79d9c64 100644 --- a/strr-host-pm-web/pnpm-lock.yaml +++ b/strr-host-pm-web/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: nuxt: specifier: ^3.12.3 version: 3.12.3(@opentelemetry/api@1.9.0)(eslint@8.57.0)(rollup@4.18.0)(sass@1.77.6)(typescript@5.5.3)(vite@5.3.3) + uuid: + specifier: ^11.0.3 + version: 11.0.3 vue-country-flag-next: specifier: ^2.3.2 version: 2.3.2(vue@3.4.31) @@ -11825,6 +11828,11 @@ packages: /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true