diff --git a/src/components/__tests__/FormFieldCompanyAddress.cy.js b/src/components/__tests__/FormFieldCompanyAddress.cy.js
index 4f8373549..39fee83f8 100644
--- a/src/components/__tests__/FormFieldCompanyAddress.cy.js
+++ b/src/components/__tests__/FormFieldCompanyAddress.cy.js
@@ -96,6 +96,13 @@ describe('', () => {
// set organization ID in store
store.setOrganizationId(organizationId);
cy.wrap(store.getOrganizationId).should('eq', organizationId);
+ /**
+ * Manually load subsidiaries.
+ * In the live application, this is handled by `onMounted` hook,
+ * or calling `loadSubsidiariesToStore` method in another
+ * component.
+ */
+ store.loadSubsidiariesToStore(null);
// wait for subsidiaries API
cy.waitForSubsidiariesApi(
subsidiariesResponse,
diff --git a/src/components/__tests__/FormFieldSelectTable.cy.js b/src/components/__tests__/FormFieldSelectTable.cy.js
index e4a1ec2ab..c581c1b11 100644
--- a/src/components/__tests__/FormFieldSelectTable.cy.js
+++ b/src/components/__tests__/FormFieldSelectTable.cy.js
@@ -1,18 +1,24 @@
+import { ref } from 'vue';
import FormFieldSelectTable from 'components/form/FormFieldSelectTable.vue';
import { rideToWorkByBikeConfig } from 'src/boot/global_vars';
import { i18n } from '../../boot/i18n';
import { useApiGetOrganizations } from 'src/composables/useApiGetOrganizations';
import { createPinia, setActivePinia } from 'pinia';
+import { vModelAdapter } from 'app/test/cypress/utils';
import {
OrganizationLevel,
OrganizationType,
} from 'src/components/types/Organization';
+import { interceptOrganizationsApi } from '../../../test/cypress/support/commonTests';
import { useRegisterChallengeStore } from 'src/stores/registerChallenge';
+// variables
const { contactEmail } = rideToWorkByBikeConfig;
+const model = ref(null);
describe('', () => {
let options;
+ let organizationId;
let subsidiaryId;
before(() => {
@@ -32,6 +38,12 @@ describe('', () => {
},
);
});
+ // set common organizationId from fixture
+ cy.fixture('formFieldCompanyCreate').then(
+ (formFieldCompanyCreateResponse) => {
+ organizationId = formFieldCompanyCreateResponse.id;
+ },
+ );
// set common subsidiaryId from fixture
cy.fixture('formOrganizationOptions').then((formOrganizationOptions) => {
subsidiaryId = formOrganizationOptions[0].subsidiaries[0].id;
@@ -95,8 +107,20 @@ describe('', () => {
context('organization company', () => {
beforeEach(() => {
cy.interceptCitiesGetApi(rideToWorkByBikeConfig, i18n);
+ // intercept both POST and GET requests for organizations
+ interceptOrganizationsApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ OrganizationType.company,
+ );
+ cy.interceptSubsidiaryPostApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ organizationId,
+ );
cy.mount(FormFieldSelectTable, {
props: {
+ ...vModelAdapter(model),
options: options,
organizationLevel: OrganizationLevel.organization,
organizationType: OrganizationType.company,
@@ -219,6 +243,76 @@ describe('', () => {
cy.dataCy('dialog-button-submit').click();
cy.dataCy('dialog-add-option').should('not.exist');
});
+
+ it('allows to add a new organization', () => {
+ cy.fixture('apiPostSubsidiaryRequest').then(
+ (apiPostSubsidiaryRequest) => {
+ cy.fixture('formFieldCompanyCreateRequest').then(
+ (formFieldCompanyCreateRequest) => {
+ // open add company dialog
+ cy.dataCy('button-add-option').click();
+ // verify that dialog is visible
+ cy.dataCy('dialog-add-option').should('be.visible');
+ cy.dataCy('dialog-add-option')
+ .find('h3')
+ .should('be.visible')
+ .and('contain', i18n.global.t('form.company.titleAddCompany'));
+ // fill in the form
+ cy.fillOrganizationSubsidiaryForm(
+ formFieldCompanyCreateRequest,
+ apiPostSubsidiaryRequest,
+ );
+ // submit the form
+ cy.dataCy('dialog-button-submit').click();
+ // wait for API call
+ cy.waitForOrganizationPostApi();
+ // verify that dialog is closed
+ cy.dataCy('dialog-add-option').should('not.exist');
+ // test emitted events
+ cy.fixture('formFieldCompanyCreate').then(
+ (formFieldCompanyCreateResponse) => {
+ // test that create:option event was emitted
+ cy.wrap(Cypress.vueWrapper.emitted('create:option')).should(
+ 'have.length',
+ 1,
+ );
+ // test that event payload is correct
+ cy.wrap(
+ Cypress.vueWrapper.emitted('create:option')[0][0],
+ ).should('deep.equal', formFieldCompanyCreateResponse);
+ // test that model value was updated
+ cy.wrap(model)
+ .its('value')
+ .should('eq', formFieldCompanyCreateResponse.id);
+ },
+ );
+ // open dialog again
+ cy.dataCy('button-add-option').click();
+ cy.dataCy('dialog-add-option').should('be.visible');
+ // verify that dialog form was reset
+ cy.dataCy('form-add-company-name')
+ .find('input')
+ .should('have.value', '');
+ cy.dataCy('form-add-company-vat-id')
+ .find('input')
+ .should('have.value', '');
+ cy.dataCy('form-add-subsidiary-street')
+ .find('input')
+ .should('have.value', '');
+ cy.dataCy('form-add-subsidiary-house-number')
+ .find('input')
+ .should('have.value', '');
+ cy.dataCy('form-add-subsidiary-city')
+ .find('input')
+ .should('have.value', '');
+ cy.dataCy('form-add-subsidiary-zip')
+ .find('input')
+ .should('have.value', '');
+ },
+ );
+ },
+ );
+ });
});
context('organization company selected', () => {
diff --git a/src/components/__tests__/FormSelectOrganization.cy.js b/src/components/__tests__/FormSelectOrganization.cy.js
index 4be4adb57..82bb93a05 100644
--- a/src/components/__tests__/FormSelectOrganization.cy.js
+++ b/src/components/__tests__/FormSelectOrganization.cy.js
@@ -1,8 +1,11 @@
+import { computed } from 'vue';
import { createPinia, setActivePinia } from 'pinia';
import FormSelectOrganization from 'components/form/FormSelectOrganization.vue';
import { i18n } from '../../boot/i18n';
import { OrganizationType } from 'src/components/types/Organization';
import { useRegisterChallengeStore } from 'src/stores/registerChallenge';
+import { interceptOrganizationsApi } from '../../../test/cypress/support/commonTests';
+import { rideToWorkByBikeConfig } from 'src/boot/global_vars';
// selectors
const selectorFormFieldCompanyAddress = 'form-company-address';
@@ -10,13 +13,34 @@ const selectorFormFieldSelectTable = 'form-select-table-company';
const selectorFormSelectOrganization = 'form-select-organization';
describe('', () => {
+ let organizationId;
+
it('has translation for all strings', () => {
cy.testLanguageStringsInContext([], 'index.component', i18n);
});
+ before(() => {
+ setActivePinia(createPinia());
+ // set common organizationId from fixture
+ cy.fixture('formFieldCompanyCreate').then(
+ (formFieldCompanyCreateResponse) => {
+ organizationId = formFieldCompanyCreateResponse.id;
+ },
+ );
+ });
+
context('desktop', () => {
beforeEach(() => {
- setActivePinia(createPinia());
+ cy.interceptSubsidiariesGetApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ organizationId,
+ );
+ interceptOrganizationsApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ OrganizationType.company,
+ );
cy.mount(FormSelectOrganization, {
props: {},
});
@@ -28,7 +52,16 @@ describe('', () => {
context('mobile', () => {
beforeEach(() => {
- setActivePinia(createPinia());
+ cy.interceptSubsidiariesGetApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ organizationId,
+ );
+ interceptOrganizationsApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ OrganizationType.company,
+ );
cy.mount(FormSelectOrganization, {
props: {},
});
@@ -37,6 +70,102 @@ describe('', () => {
coreTests();
});
+
+ context('create new organization and subsidiary', () => {
+ beforeEach(() => {
+ cy.interceptCitiesGetApi(rideToWorkByBikeConfig, i18n);
+ interceptOrganizationsApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ OrganizationType.company,
+ );
+ cy.interceptSubsidiaryPostApi(
+ rideToWorkByBikeConfig,
+ i18n,
+ organizationId,
+ );
+ cy.mount(FormSelectOrganization, {
+ props: {},
+ });
+ cy.viewport('iphone-6');
+ });
+
+ it('allows to create a new organization and subsidiary', () => {
+ cy.fixture('formFieldCompany').then((formFieldCompanyResponse) => {
+ cy.fixture('apiPostSubsidiaryRequest').then(
+ (apiPostSubsidiaryRequest) => {
+ cy.fixture('apiPostSubsidiaryResponse').then(
+ (apiPostSubsidiaryResponse) => {
+ cy.fixture('formFieldCompanyCreateRequest').then(
+ (formFieldCompanyCreateRequest) => {
+ cy.fixture('formFieldCompanyCreate').then(
+ (formFieldCompanyCreateResponse) => {
+ cy.wrap(useRegisterChallengeStore()).then((store) => {
+ store.setOrganizationType(OrganizationType.company);
+ const organizationId = computed(
+ () => store.getOrganizationId,
+ );
+ const subsidiaryId = computed(
+ () => store.getSubsidiaryId,
+ );
+ // open dialog form
+ cy.dataCy('button-add-option').click();
+ // verify that dialog is visible
+ cy.dataCy('dialog-add-option').should('be.visible');
+ cy.dataCy('dialog-add-option')
+ .find('h3')
+ .should('be.visible')
+ .and(
+ 'contain',
+ i18n.global.t('form.company.titleAddCompany'),
+ );
+ // fill in the form
+ cy.fillOrganizationSubsidiaryForm(
+ formFieldCompanyCreateRequest,
+ apiPostSubsidiaryRequest,
+ );
+ // submit the form
+ cy.dataCy('dialog-button-submit').click();
+ // check that POST organization API was called
+ cy.waitForOrganizationPostApi();
+ // check that POST subsidiary API was called
+ cy.waitForSubsidiaryPostApi();
+ // check that a new organization was added to the list
+ cy.dataCy('form-select-table-options')
+ .find('.q-radio__label')
+ .should(
+ 'have.length',
+ formFieldCompanyResponse.count + 1,
+ );
+ cy.dataCy('form-select-table-options')
+ .find('.q-radio__label')
+ .contains(formFieldCompanyCreateRequest.name)
+ .should('exist');
+ // check that organizationId was saved in store
+ cy.wrap(organizationId)
+ .its('value')
+ .should('equal', formFieldCompanyCreateResponse.id);
+ // check that subsidiaryId was saved in store
+ cy.wrap(subsidiaryId)
+ .its('value')
+ .should('equal', apiPostSubsidiaryResponse.id);
+ // check that the subsidary (address) input has correct value
+ cy.dataCy('form-company-address').should(
+ 'contain',
+ apiPostSubsidiaryRequest.address.street,
+ );
+ });
+ },
+ );
+ },
+ );
+ },
+ );
+ },
+ );
+ });
+ });
+ });
});
function coreTests() {
diff --git a/src/components/form/FormAddCompany.vue b/src/components/form/FormAddCompany.vue
index 8006876a6..66e21879b 100644
--- a/src/components/form/FormAddCompany.vue
+++ b/src/components/form/FormAddCompany.vue
@@ -154,7 +154,7 @@ export default defineComponent({
{{ $t('form.company.textSubsidiaryAddress') }}
-
+
store.isLoadingSubsidiaries);
+ const subsidiaries = computed(() => store.subsidiaries);
+
const { isLoading: isLoadingCreateSubsidiary, createSubsidiary } =
useApiPostSubsidiary(logger);
const isLoading = computed(
() => isLoadingSubsidiaries.value || isLoadingCreateSubsidiary.value,
);
-
- const store = useRegisterChallengeStore();
/**
* If organization ID is set, load subsidiaries.
* This ensures, that options are loaded on page refresh.
@@ -105,7 +102,7 @@ export default defineComponent({
onMounted(async () => {
if (store.getOrganizationId) {
logger?.info('Loading subsidiaries.');
- await loadSubsidiaries(store.getOrganizationId);
+ await store.loadSubsidiariesToStore(logger);
} else {
logger?.debug(
`Organization was not selected <${store.getOrganizationId}>,` +
@@ -113,28 +110,9 @@ export default defineComponent({
);
}
});
- /**
- * Watch for organization ID changes.
- * This clears the subsidiary options and state after organization change.
- * Then loads subsidiaries for the new organization.
- * Should not be triggered on mounted not to clear saved subsidiary ID.
- */
- watch(
- () => store.getOrganizationId,
- (newValue) => {
- logger?.debug(
- `Register challenge store organization ID updated to <${newValue}>.`,
- );
- subsidiaryId.value = null;
- if (newValue) {
- logger?.info('Loading subsidiaries.');
- loadSubsidiaries(newValue);
- }
- },
- );
const options = computed(() =>
- subsidiaries.value?.map((subsidiary) => ({
+ store.getSubsidiaries?.map((subsidiary) => ({
label: getAddressString(subsidiary.address),
value: subsidiary.id,
})),
diff --git a/src/components/form/FormFieldSelectTable.vue b/src/components/form/FormFieldSelectTable.vue
index b53e45a1b..215707bcf 100644
--- a/src/components/form/FormFieldSelectTable.vue
+++ b/src/components/form/FormFieldSelectTable.vue
@@ -57,6 +57,8 @@ import { useOrganizations } from '../../composables/useOrganizations';
import { useSelectTable } from '../../composables/useSelectTable';
import { useValidation } from '../../composables/useValidation';
import { useApiPostTeam } from '../../composables/useApiPostTeam';
+import { useApiPostOrganization } from '../../composables/useApiPostOrganization';
+import { useApiPostSubsidiary } from '../../composables/useApiPostSubsidiary';
// enums
import { OrganizationType, OrganizationLevel } from '../types/Organization';
@@ -72,6 +74,23 @@ import {
FormSelectTableOption,
FormTeamFields,
} from '../types/Form';
+import type { OrganizationSubsidiary } from '../types/Organization';
+
+// utils
+import { deepObjectWithSimplePropsCopy } from '../../utils';
+
+const emptyFormCompanyFields: FormCompanyFields = {
+ name: '',
+ vatId: '',
+ address: {
+ street: '',
+ houseNumber: '',
+ city: '',
+ zip: '',
+ cityChallenge: null,
+ department: '',
+ },
+};
export default defineComponent({
name: 'FormFieldSelectTable',
@@ -110,21 +129,12 @@ export default defineComponent({
// user input for filtering
const query = ref('');
const formRef = ref(null);
- const organizationNew = ref({
- name: '',
- vatId: '',
- address: {
- street: '',
- houseNumber: '',
- city: '',
- zip: '',
- cityChallenge: null,
- department: '',
- },
- });
- const teamNew = ref({
- name: '',
- });
+ const organizationNew = ref(
+ deepObjectWithSimplePropsCopy(
+ emptyFormCompanyFields,
+ ) as FormCompanyFields,
+ );
+ const teamNew = ref({ name: '' });
const selectOrganizationRef = ref(null);
/**
@@ -159,14 +169,6 @@ export default defineComponent({
// controls dialog visibility
const isDialogOpen = ref(false);
- // close dialog
- const onClose = (): void => {
- if (formRef.value) {
- formRef.value.reset();
- }
- isDialogOpen.value = false;
- };
-
/**
* Validates the form.
* If form is valid it submits the data.
@@ -187,17 +189,110 @@ export default defineComponent({
};
const { createTeam } = useApiPostTeam(logger);
+ const { isLoading: isLoadingCreateOrganization, createOrganization } =
+ useApiPostOrganization(logger);
+ const { isLoading: isLoadingCreateSubsidiary, createSubsidiary } =
+ useApiPostSubsidiary(logger);
+ const isLoading = computed(
+ () =>
+ isLoadingCreateOrganization.value || isLoadingCreateSubsidiary.value,
+ );
+
const registerChallengeStore = useRegisterChallengeStore();
+ const subsidiaryId = computed({
+ get: (): number | null => registerChallengeStore.getSubsidiaryId,
+ set: (value: number | null) =>
+ registerChallengeStore.setSubsidiaryId(value),
+ });
+
+ /**
+ * Triggered by clicking on an option.
+ * If organization option was changed, resets subsidiary ID and reloads
+ * subsidiaries.
+ * @param {number | null} value - ID of the selected option.
+ */
+ const onChangeOption = (value: number | null): void => {
+ if (props.organizationLevel === OrganizationLevel.organization) {
+ logger?.debug(`Organizations option changed to <${value}>.`);
+ logger?.info('Resetting subsidiary ID to null.');
+ registerChallengeStore.setSubsidiaryId(null);
+ logger?.info('Reloading subsidiaries data from the API.');
+ registerChallengeStore.loadSubsidiariesToStore(logger);
+ }
+ };
/**
* Submit dialog form based on organization level
- * If `company`, create a new company
+ * If `company`, create a new company (and subsidiary)
* If `team`, create a new team
* @returns {Promise}
*/
const submitDialogForm = async (): Promise => {
if (props.organizationLevel === OrganizationLevel.organization) {
- // TODO: Create a new company
+ // create organization
+ if (!props.organizationType) {
+ logger?.info('No organization type provided.');
+ return;
+ }
+ logger?.info('Create new organization.');
+ const organizationData = await createOrganization(
+ organizationNew.value.name,
+ organizationNew.value.vatId,
+ props.organizationType,
+ );
+
+ if (!organizationData?.id) {
+ logger?.error('New organization ID not found.');
+ return;
+ }
+ logger?.debug(
+ `New organization was created with ID <${organizationData.id}> and name <${organizationData.name}>.`,
+ );
+
+ logger?.debug(
+ `Updating organization model ID from <${inputValue.value}> to <${organizationData.id}>.`,
+ );
+ // set organization ID in store
+ inputValue.value = organizationData.id;
+ // emit event to append data to organization options
+ emit('create:option', organizationData);
+
+ // create subsidiary
+ logger?.info('Create new subsidiary.');
+ const subsidiaryData = await createSubsidiary(
+ organizationData.id,
+ organizationNew.value.address,
+ );
+ if (subsidiaryData?.id) {
+ logger?.debug(
+ `New subsidiary was created with data <${JSON.stringify(subsidiaryData, null, 2)}>.`,
+ );
+ // set subsidiary ID in store
+ logger?.debug(
+ `Updating subsidiary ID from <${subsidiaryId.value}> to <${subsidiaryData.id}>`,
+ );
+ registerChallengeStore.setSubsidiaryId(subsidiaryData.id);
+ logger?.debug(`Subsidiary ID model set to <${subsidiaryId.value}>.`);
+ // create a new subsidiary array
+ const newSubsidiary: OrganizationSubsidiary = {
+ id: subsidiaryData.id,
+ address: {
+ street: subsidiaryData.street,
+ houseNumber: subsidiaryData.houseNumber,
+ city: subsidiaryData.city,
+ zip: subsidiaryData.zip,
+ cityChallenge: subsidiaryData.cityChallenge,
+ department: subsidiaryData.department,
+ },
+ teams: [],
+ };
+ // set subsidiary options to array with new subsidiary
+ registerChallengeStore.setSubsidiaries([newSubsidiary]);
+ } else {
+ logger?.error('New subsidiary data not found.');
+ }
+ // close dialog
+ onClose();
} else if (props.organizationLevel === OrganizationLevel.team) {
logger?.info('Create team.');
const subsidiaryId = registerChallengeStore.getSubsidiaryId;
@@ -213,8 +308,7 @@ export default defineComponent({
// emit `create:option` event
emit('create:option', data);
// close dialog
- isDialogOpen.value = false;
- logger?.info('Close add team modal dialog.');
+ onClose();
// store data in v-model (emits to parent component)
inputValue.value = data.id;
logger?.debug(`New team model ID set to <${inputValue.value}>.`);
@@ -222,6 +316,25 @@ export default defineComponent({
}
};
+ // close dialog
+ const onClose = (): void => {
+ if (formRef.value) {
+ formRef.value.reset();
+ }
+ // reset organizationNew and teamNew
+ organizationNew.value = deepObjectWithSimplePropsCopy(
+ emptyFormCompanyFields,
+ ) as FormCompanyFields;
+ teamNew.value = { name: '' };
+ logger?.debug(
+ `Reset new organization model value <${organizationNew.value}>.`,
+ );
+ logger?.debug(`Reset new team model value <${teamNew.value}>.`);
+ // close dialog
+ isDialogOpen.value = false;
+ logger?.info('Close add option modal dialog.');
+ };
+
const { getOrganizationLabels } = useOrganizations();
const { getSelectTableLabels } = useSelectTable();
@@ -278,8 +391,10 @@ export default defineComponent({
teamNew,
titleDialog,
isFilled,
+ isLoading,
onClose,
onSubmit,
+ onChangeOption,
OrganizationType,
OrganizationLevel,
selectOrganizationRef,
@@ -351,6 +466,7 @@ export default defineComponent({
:label="item.label"
color="primary"
data-cy="form-select-table-option"
+ @update:model-value="onChangeOption"
/>