diff --git a/strr-platform-web/app/assets/styles/layout.css b/strr-platform-web/app/assets/styles/layout.css index 55c6e16c3..bc951bd79 100644 --- a/strr-platform-web/app/assets/styles/layout.css +++ b/strr-platform-web/app/assets/styles/layout.css @@ -3,7 +3,7 @@ } .app-inner-container { - @apply max-w-bcGovLg w-[100vw] mx-auto px-4; + @apply max-w-bcGovLg w-full mx-auto px-4; } .app-body { diff --git a/strr-platform-web/app/components/connect/ButtonControl.vue b/strr-platform-web/app/components/connect/ButtonControl.vue index b875ca9f7..b68be421f 100644 --- a/strr-platform-web/app/components/connect/ButtonControl.vue +++ b/strr-platform-web/app/components/connect/ButtonControl.vue @@ -21,6 +21,8 @@ const rightButtons = computed(() => buttonControl.value?.rightButtons || []) :label="button.label" :trailing="button.trailing || false" :variant="button.variant || 'solid'" + :disabled="button.disabled || false" + :loading="button.loading || false" data-testid="button-control-left-button" @click="button.action()" /> diff --git a/strr-platform-web/app/components/connect/fee/Widget.vue b/strr-platform-web/app/components/connect/fee/Widget.vue index 24e297488..e0afcabd0 100644 --- a/strr-platform-web/app/components/connect/fee/Widget.vue +++ b/strr-platform-web/app/components/connect/fee/Widget.vue @@ -36,7 +36,7 @@ watch(isFoldable, (val) => { }) const toggleFolded = () => { - if (isFoldable) { + if (isFoldable.value) { folded.value = !folded.value } } diff --git a/strr-platform-web/app/components/modal/Base.vue b/strr-platform-web/app/components/modal/Base.vue new file mode 100644 index 000000000..ffbae4436 --- /dev/null +++ b/strr-platform-web/app/components/modal/Base.vue @@ -0,0 +1,85 @@ + + diff --git a/strr-platform-web/app/composables/useStrrModals.ts b/strr-platform-web/app/composables/useStrrModals.ts new file mode 100644 index 000000000..fc055fbd7 --- /dev/null +++ b/strr-platform-web/app/composables/useStrrModals.ts @@ -0,0 +1,26 @@ +// https://ui.nuxt.com/components/modal#control-programmatically +import { ModalBase } from '#components' + +export const useStrrModals = () => { + const modal = useModal() + const { t } = useI18n() + + function openAppSubmitError () { + modal.open(ModalBase, { + error: { + title: 'Error submitting application', // need to come up with different error messages for different scenarios + description: 'Some description here.' + }, + actions: [{ label: t('btn.close'), handler: () => close() }] + }) + } + + function close () { + modal.close() + } + + return { + openAppSubmitError, + close + } +} diff --git a/strr-platform-web/app/enums/application-type.ts b/strr-platform-web/app/enums/application-type.ts new file mode 100644 index 000000000..65567eeb2 --- /dev/null +++ b/strr-platform-web/app/enums/application-type.ts @@ -0,0 +1,3 @@ +export enum ApplicationType { + PLATFORM = 'PLATFORM' +} diff --git a/strr-platform-web/app/interfaces/connect-btn-control/item-i.ts b/strr-platform-web/app/interfaces/connect-btn-control/item-i.ts index 86f57310a..b50ba4142 100644 --- a/strr-platform-web/app/interfaces/connect-btn-control/item-i.ts +++ b/strr-platform-web/app/interfaces/connect-btn-control/item-i.ts @@ -7,4 +7,5 @@ export interface ConnectBtnControlItem { loading?: boolean variant?: string trailing?: boolean + disabled?: boolean } diff --git a/strr-platform-web/app/interfaces/platform-application.ts b/strr-platform-web/app/interfaces/platform-application.ts new file mode 100644 index 000000000..90092fcd1 --- /dev/null +++ b/strr-platform-web/app/interfaces/platform-application.ts @@ -0,0 +1,53 @@ +export interface ApiAddress { + country: string + address: string + addressLineTwo: string + city: string + province: string + postalCode: string +} + +export interface ApiParty { + firstName: string + lastName: string + middleName: string + phoneNumber: string + extension: string + faxNumber: string + emailAddress: string +} + +export interface ApiRep extends ApiParty { + jobTitle: string +} + +export interface ApiBusinessDetails { + legalName: string + homeJurisdiction: string + businessNumber: string + consumerProtectionBCLicenceNumber: string + noticeOfNonComplianceEmail: string + noticeOfNonComplianceOptionalEmail: string + takeDownRequestEmail: string + takeDownRequestOptionalEmail: string + mailingAddress: ApiAddress + registeredOfficeOrAttorneyForServiceDetails: { + attorneyName: string + mailingAddress: ApiAddress + } +} + +export interface ApiPlatformDetails { + brands: PlatBrand[] + listingSize: ListingSize +} + +export interface PlatformApplicationPayload { + registration: { + registrationType: ApplicationType + completingParty: ApiParty + platformRepresentatives: ApiRep[] + businessDetails: ApiBusinessDetails + platformDetails: ApiPlatformDetails + } +} diff --git a/strr-platform-web/app/layouts/connect-form.vue b/strr-platform-web/app/layouts/connect-form.vue index c6eef111e..f1a111e71 100644 --- a/strr-platform-web/app/layouts/connect-form.vue +++ b/strr-platform-web/app/layouts/connect-form.vue @@ -10,12 +10,12 @@ class="app-inner-container app-body" data-testid="strr-form-layout-slot" > -
+
-
diff --git a/strr-platform-web/app/pages/platform/application.vue b/strr-platform-web/app/pages/platform/application.vue index 50bae214d..b4220620f 100644 --- a/strr-platform-web/app/pages/platform/application.vue +++ b/strr-platform-web/app/pages/platform/application.vue @@ -35,6 +35,7 @@ onMounted(async () => { const { platformDetails } = storeToRefs(useStrrPlatformDetails()) const { platformBusiness } = storeToRefs(useStrrPlatformBusiness()) +// const { getBusinessSchema } = useStrrPlatformBusiness() watch(() => platformBusiness.value.hasCpbc, (val) => { if (val && platFeeWv.value) { removeFee(ConnectFeeCode.STR_PLAT_SM) @@ -118,16 +119,65 @@ const setPreviousStep = () => { } } -// something like this? need to discuss options -// how can we set loading state on the submit and pay button? -// i wonder if tracking the steps in a store would be useful -const handlePlatformSubmit = () => { - // validate each step - // if invalid step, set prop on Review component to highlight invalid fields/section ? - // show alert with some error text ? +// need to cleanup the setButtonControl somehow +const handlePlatformSubmit = async () => { + try { + // set buttons to loading state + setButtonControl({ + leftButtons: [ + { + action: setPreviousStep, + icon: 'i-mdi-chevron-left', + label: t('btn.back'), + variant: 'outline', + disabled: true + }, + { + action: handlePlatformSubmit, + icon: 'i-mdi-chevron-right', + label: t('btn.submitAndPay'), + trailing: true, + loading: true + } + ], + rightButtons: [] + }) + + // something like this but cleaner + // validate all forms + // const isCompletingPartyValid = getContactSchema(true).safeParse(completingParty.value).success + // const isPrimaryRepValid = getContactSchema(false).safeParse(primaryRep.value).success + // if (secondaryRep.value) { + // const isSecondaryRepValid = getContactSchema(false).safeParse(secondaryRep.value).success + // } + // const isBusDetailsValid = getBusinessSchema( + // platformBusiness.value.hasCpbc, platformBusiness.value.hasRegOffAtt) + // console.log(isCompletingPartyValid) + + // if all steps valid, submit form with store function + await submitPlatformApplication() + } catch (e) { - // if all steps valid, submit form with store function - submitPlatformApplication() + } finally { + // set buttons back to non loading state + setButtonControl({ + leftButtons: [ + { + action: setPreviousStep, + icon: 'i-mdi-chevron-left', + label: t('btn.back'), + variant: 'outline' + }, + { + action: handlePlatformSubmit, + icon: 'i-mdi-chevron-right', + label: t('btn.submitAndPay'), + trailing: true + } + ], + rightButtons: [] + }) + } } watch(activeStepIndex, (val) => { diff --git a/strr-platform-web/app/plugins/strr-api.ts b/strr-platform-web/app/plugins/strr-api.ts new file mode 100644 index 000000000..51c7425a4 --- /dev/null +++ b/strr-platform-web/app/plugins/strr-api.ts @@ -0,0 +1,29 @@ +export default defineNuxtPlugin(() => { + const strrApiUrl = useRuntimeConfig().public.strrApiURL + const accountStore = useConnectAccountStore() + + const { $keycloak } = useNuxtApp() + + const api = $fetch.create({ + baseURL: strrApiUrl, + onRequest ({ options }) { + const headers = options.headers ||= {} + if (Array.isArray(headers)) { + headers.push(['Authorization', `Bearer ${$keycloak.token}`]) + headers.push(['Account-Id', accountStore.currentAccount.id]) + } else if (headers instanceof Headers) { + headers.set('Authorization', `Bearer ${$keycloak.token}`) + headers.set('Account-Id', accountStore.currentAccount.id) + } else { + headers.Authorization = `Bearer ${$keycloak.token}` + headers['Account-Id'] = accountStore.currentAccount.id + } + } + }) + + return { + provide: { + strrApi: api + } + } +}) diff --git a/strr-platform-web/app/stores/platformApplication.ts b/strr-platform-web/app/stores/platformApplication.ts index d3a0a9e5c..290648204 100644 --- a/strr-platform-web/app/stores/platformApplication.ts +++ b/strr-platform-web/app/stores/platformApplication.ts @@ -1,31 +1,59 @@ export const useStrrPlatformApplication = defineStore('strr/platformApplication', () => { + const { $strrApi } = useNuxtApp() const { completingParty, primaryRep, secondaryRep } = storeToRefs(useStrrPlatformContact()) const { platformBusiness } = storeToRefs(useStrrPlatformBusiness()) const { platformDetails } = storeToRefs(useStrrPlatformDetails()) + const strrModal = useStrrModals() const confirmInfoAccuracy = ref(false) const confirmDelistAndCancelBookings = ref(false) - const getPlatformApplication = () => { - return { - completingParty: completingParty.value, - primaryRep: primaryRep.value, - secondaryRep: secondaryRep.value, - platformBusiness: platformBusiness.value, - platformDetails: platformDetails.value + function createApplicationBody (): PlatformApplicationPayload { + const applicationBody: PlatformApplicationPayload = { + registration: { + registrationType: ApplicationType.PLATFORM, + completingParty: formatParty(completingParty.value), + platformRepresentatives: [], + businessDetails: formatBusinessDetails(platformBusiness.value), + platformDetails: formatPlatformDetails(platformDetails.value) + } } + + if (primaryRep.value !== undefined) { + applicationBody.registration.platformRepresentatives.push( + formatRepresentative(primaryRep.value) + ) + } + + if (secondaryRep.value !== undefined) { + applicationBody.registration.platformRepresentatives.push( + formatRepresentative(secondaryRep.value) + ) + } + + return applicationBody } - // TODO: submit - function submitPlatformApplication () { - // eslint-disable-next-line - console.log('submitting platform app') + async function submitPlatformApplication () { + // validate all forms/fields first?? + try { + const body = createApplicationBody() + + // console.log('submitting application: ', body) + await $strrApi('/applications', { + method: 'POST', + body + }) + } catch (e) { + logFetchError(e, 'Error creating platform application') + strrModal.openAppSubmitError() // pass in error object ?? + } } return { confirmInfoAccuracy, confirmDelistAndCancelBookings, - getPlatformApplication, + // getPlatformApplication, submitPlatformApplication } }) diff --git a/strr-platform-web/app/utils/platform-application.ts b/strr-platform-web/app/utils/platform-application.ts new file mode 100644 index 000000000..ed60e7ab5 --- /dev/null +++ b/strr-platform-web/app/utils/platform-application.ts @@ -0,0 +1,65 @@ +export function formatPhoneNumber (phone: ConnectPhone) { + return { + phoneNumber: `${phone.countryCode ?? ''}${phone.number}`, + extension: phone.extension ?? '' + } +} + +export function formatAddress (add: ConnectAddress): ApiAddress { + return { + country: add.country, + address: add.street, + addressLineTwo: add.streetAdditional, + city: add.city, + province: add.region, + postalCode: add.postalCode + } +} + +export function formatParty (party: Contact): ApiParty { + return { + firstName: party.firstName, + middleName: party.middleName ?? '', + lastName: party.lastName, + ...formatPhoneNumber(party.phone), + faxNumber: party.faxNumber ?? '', + emailAddress: party.emailAddress + } +} + +export function formatRepresentative (rep: PlatformContact): ApiRep { + return { + ...formatParty(rep), + jobTitle: rep.position + } +} + +export function formatBusinessDetails (bus: PlatBusiness): ApiBusinessDetails { + return { + legalName: bus.legalName, + homeJurisdiction: bus.homeJurisdiction, + businessNumber: bus.businessNumber, + consumerProtectionBCLicenceNumber: bus.cpbcLicenceNumber, + noticeOfNonComplianceEmail: bus.nonComplianceEmail, + noticeOfNonComplianceOptionalEmail: bus.nonComplianceEmailOptional, + takeDownRequestEmail: bus.takeDownEmail, + takeDownRequestOptionalEmail: bus.takeDownEmailOptional, + mailingAddress: formatAddress(bus.mailingAddress), + registeredOfficeOrAttorneyForServiceDetails: { + attorneyName: bus.regOfficeOrAtt.attorneyName, + mailingAddress: formatAddress(bus.regOfficeOrAtt.mailingAddress) + } + } +} + +export function formatPlatformDetails ( + plat: { brands: PlatBrand[], listingSize: ListingSize | undefined } +): ApiPlatformDetails { + return { + brands: plat.brands.map(brand => ({ + name: brand.name, + website: brand.website + })), + listingSize: plat.listingSize! // should never be undefined after being validated + } +}