implement adding items to storage
This commit is contained in:
parent
266e7194c0
commit
90b6f666d1
@ -28,6 +28,7 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isValid = ref(true);
|
const isValid = ref(true);
|
||||||
|
const knownFields = ref<string[]>([]);
|
||||||
const invalidFields = ref<string[]>([]);
|
const invalidFields = ref<string[]>([]);
|
||||||
const formData = ref<FormData>(props.modelValue);
|
const formData = ref<FormData>(props.modelValue);
|
||||||
const formErrors = ref<FormErrors>(props.errors);
|
const formErrors = ref<FormErrors>(props.errors);
|
||||||
@ -70,6 +71,7 @@ const validateField = async (field: string) => {
|
|||||||
const validateAll = async () => {
|
const validateAll = async () => {
|
||||||
const fields = props.validators
|
const fields = props.validators
|
||||||
.map((validator) => validator.field)
|
.map((validator) => validator.field)
|
||||||
|
.filter((field) => knownFields.value.includes(field))
|
||||||
.filter((value, index, array) => array.indexOf(value) === index);
|
.filter((value, index, array) => array.indexOf(value) === index);
|
||||||
if (!fields.length) return;
|
if (!fields.length) return;
|
||||||
return Promise.allSettled(fields.map((field) => validateField(field)));
|
return Promise.allSettled(fields.map((field) => validateField(field)));
|
||||||
@ -80,6 +82,24 @@ const fieldChange = useDebounceFn(validateField, 300);
|
|||||||
provide('formData', formData);
|
provide('formData', formData);
|
||||||
provide('formErrors', formErrors);
|
provide('formErrors', formErrors);
|
||||||
provide('fieldChange', fieldChange);
|
provide('fieldChange', fieldChange);
|
||||||
|
provide('registerField', (field: string) => {
|
||||||
|
if (knownFields.value?.includes(field)) return;
|
||||||
|
knownFields.value.push(field);
|
||||||
|
});
|
||||||
|
provide('unregisterField', (field: string) => {
|
||||||
|
if (!knownFields.value?.includes(field)) return;
|
||||||
|
knownFields.value = knownFields.value.filter((entry) => field !== entry);
|
||||||
|
invalidFields.value = invalidFields.value.filter((entry) => field !== entry);
|
||||||
|
if (formErrors.value[field]) {
|
||||||
|
delete formErrors.value[field];
|
||||||
|
}
|
||||||
|
if (formData.value[field]) {
|
||||||
|
delete formData.value[field];
|
||||||
|
}
|
||||||
|
if (!isValid.value) {
|
||||||
|
isValid.value = !invalidFields.value.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'submit', data: FormSubmit): void;
|
(e: 'submit', data: FormSubmit): void;
|
||||||
@ -101,7 +121,15 @@ const onSubmit = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
formData.value = {};
|
||||||
|
formErrors.value = {};
|
||||||
|
invalidFields.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
isValid,
|
isValid,
|
||||||
|
validateAll,
|
||||||
|
reset,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
'block text-sm font-medium transition-colors duration-200',
|
'block text-sm font-medium transition-colors duration-200',
|
||||||
errors.length ? 'text-red-500' : 'text-gray-700',
|
errors.length ? 'text-red-500' : 'text-gray-700',
|
||||||
]"
|
]"
|
||||||
>{{ label }}</label
|
>{{ label }}
|
||||||
|
<span v-if="required" class="font-bold text-red-600">*</span></label
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
name="input"
|
name="input"
|
||||||
@ -14,6 +15,7 @@
|
|||||||
:type="type"
|
:type="type"
|
||||||
:id="forId"
|
:id="forId"
|
||||||
:fieldName="name"
|
:fieldName="name"
|
||||||
|
:fieldFQN="fieldName"
|
||||||
:value="value"
|
:value="value"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:invalid="!!errors?.length"
|
:invalid="!!errors?.length"
|
||||||
@ -32,6 +34,9 @@
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
|
:step="step"
|
||||||
|
:min="min"
|
||||||
|
:max="max"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@ -51,7 +56,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import set from 'lodash.set';
|
import set from 'lodash.set';
|
||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
import { computed, ref, Ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, Ref } from 'vue';
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
import { FormData, FormErrors, InputType } from './form.types';
|
import { FormData, FormErrors, InputType } from './form.types';
|
||||||
|
|
||||||
@ -61,12 +66,17 @@ const props = withDefaults(
|
|||||||
forIdPrefix?: string;
|
forIdPrefix?: string;
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
step?: string;
|
||||||
|
min?: string;
|
||||||
|
max?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
required: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
}
|
}
|
||||||
@ -82,6 +92,8 @@ const formData = inject<Ref<FormData>>('formData');
|
|||||||
const formErrors = inject<Ref<FormErrors>>('formErrors');
|
const formErrors = inject<Ref<FormErrors>>('formErrors');
|
||||||
const formGroup = inject<Ref<string>>('formGroup', ref(''));
|
const formGroup = inject<Ref<string>>('formGroup', ref(''));
|
||||||
const fieldChange = inject<(field: string) => void>('fieldChange');
|
const fieldChange = inject<(field: string) => void>('fieldChange');
|
||||||
|
const registerField = inject<(field: string) => void>('registerField');
|
||||||
|
const unregisterField = inject<(field: string) => void>('unregisterField');
|
||||||
|
|
||||||
const fieldName = computed(() =>
|
const fieldName = computed(() =>
|
||||||
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
||||||
@ -127,7 +139,12 @@ const inputClass = computed(() => {
|
|||||||
errors.value.length
|
errors.value.length
|
||||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||||
`mt-1 block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
props.type === 'checkbox' || props.type === 'radio' ? '' : 'w-full',
|
||||||
|
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
|
||||||
|
`mt-1 block rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(() => registerField?.(fieldName.value));
|
||||||
|
onBeforeUnmount(() => unregisterField?.(fieldName.value));
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
:id="forId"
|
:id="forId"
|
||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
:displayValue="(option: any) => getLabel(option)"
|
:displayValue="(option: any) => getLabel(option)"
|
||||||
|
autocomplete="off"
|
||||||
@change="query = $event.target.value"
|
@change="query = $event.target.value"
|
||||||
/>
|
/>
|
||||||
<ComboboxButton
|
<ComboboxButton
|
||||||
@ -130,6 +131,7 @@ const inputClass = computed(() => {
|
|||||||
props.invalid
|
props.invalid
|
||||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||||
|
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
|
||||||
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||||
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1',
|
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1',
|
||||||
];
|
];
|
||||||
|
@ -107,6 +107,7 @@ const inputClass = computed(() => {
|
|||||||
props.invalid
|
props.invalid
|
||||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||||
|
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
|
||||||
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||||
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1',
|
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1',
|
||||||
];
|
];
|
||||||
|
@ -4,10 +4,11 @@
|
|||||||
:label="label"
|
:label="label"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
>
|
>
|
||||||
<template #input="{ invalid, forId, value, setValue }">
|
<template #input="{ invalid, id, value, setValue }">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
:for-id="forId"
|
:for-id="id"
|
||||||
:invalid="invalid"
|
:invalid="invalid"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:initialOptions="initialOptions"
|
:initialOptions="initialOptions"
|
||||||
@ -44,6 +45,7 @@ const props = withDefaults(
|
|||||||
bindValue?: string;
|
bindValue?: string;
|
||||||
bindLabel?: string | ((obj: any) => string);
|
bindLabel?: string | ((obj: any) => string);
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<FormField
|
<FormField
|
||||||
:name="name"
|
:name="name"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
:required="required"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
@ -25,6 +26,7 @@ const props = withDefaults(
|
|||||||
forId?: string;
|
forId?: string;
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}>(),
|
}>(),
|
||||||
|
@ -3,14 +3,15 @@
|
|||||||
:name="name"
|
:name="name"
|
||||||
:label="label"
|
:label="label"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
for-id-prefix="dp-input"
|
for-id-prefix="dp-input"
|
||||||
>
|
>
|
||||||
<template #input="{ invalid, fieldName, value, setValue }">
|
<template #input="{ invalid, fieldFQN, value, setValue }">
|
||||||
<Datepicker
|
<Datepicker
|
||||||
text-input
|
text-input
|
||||||
arrow-navigation
|
arrow-navigation
|
||||||
:class="[invalid ? 'dp__invalid' : '']"
|
:class="[invalid ? 'dp__invalid' : '']"
|
||||||
:uid="fieldName"
|
:uid="fieldFQN"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:clearable="clearable"
|
:clearable="clearable"
|
||||||
@ -32,6 +33,7 @@ interface DatePickerProps extends ExtractComponentProps<typeof Datepicker> {
|
|||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<FormField
|
<FormField
|
||||||
:name="name"
|
:name="name"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
:required="required"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
@ -34,6 +35,7 @@ const props = withDefaults(
|
|||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}>(),
|
}>(),
|
||||||
|
57
src/components/form/fields/FormTextareaField.vue
Normal file
57
src/components/form/fields/FormTextareaField.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<FormField
|
||||||
|
:name="name"
|
||||||
|
:label="label"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
|
:readonly="readonly"
|
||||||
|
>
|
||||||
|
<template #input="{ invalid, id, value, onInput, onFocus, onBlur }">
|
||||||
|
<textarea
|
||||||
|
:id="id"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
:readonly="readonly"
|
||||||
|
:clearable="clearable"
|
||||||
|
:model-value="(value as string)"
|
||||||
|
:class="inputClass(invalid)"
|
||||||
|
:rows="rows"
|
||||||
|
:cols="cols"
|
||||||
|
@input="onInput"
|
||||||
|
@focus="onFocus"
|
||||||
|
@blur="onBlur"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #default><slot /></template>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormField from '../FormField.vue';
|
||||||
|
|
||||||
|
interface TextareaProps {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
rows?: string;
|
||||||
|
cols?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
clearable?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<TextareaProps>(), {
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputClass = (invalid: boolean) => {
|
||||||
|
return [
|
||||||
|
invalid
|
||||||
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||||
|
props.disabled ? 'bg-gray-100 hover:border-gray-400' : '',
|
||||||
|
`mt-1 block rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
</script>
|
296
src/components/item/NewItemModal.vue
Normal file
296
src/components/item/NewItemModal.vue
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
<template>
|
||||||
|
<Modal ref="modalRef" size="xl">
|
||||||
|
<template #title> Add a new item </template>
|
||||||
|
|
||||||
|
<template #default="{ closeModal }">
|
||||||
|
<FormAlert :message="error" />
|
||||||
|
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
||||||
|
<FormAutocompleteField
|
||||||
|
v-if="typeof item !== 'string'"
|
||||||
|
name="item"
|
||||||
|
label="Item"
|
||||||
|
required
|
||||||
|
:search-fn="searchForItems"
|
||||||
|
>
|
||||||
|
<template #notfound="{ query }">
|
||||||
|
<button @click="startAddingNewItem(query)">Add a new item</button>
|
||||||
|
</template>
|
||||||
|
</FormAutocompleteField>
|
||||||
|
<template v-if="item && typeof item === 'string'">
|
||||||
|
<FormField name="displayName" label="Display Name" required />
|
||||||
|
<FormSelectField
|
||||||
|
:options="itemTypesOptions"
|
||||||
|
required
|
||||||
|
name="type"
|
||||||
|
label="Type"
|
||||||
|
/>
|
||||||
|
<FormField name="barcode" label="Barcode" />
|
||||||
|
<FormField
|
||||||
|
name="weight"
|
||||||
|
type="number"
|
||||||
|
step="0.001"
|
||||||
|
label="Weight (g)"
|
||||||
|
/>
|
||||||
|
<FormField name="url" label="URL" />
|
||||||
|
<FormField
|
||||||
|
name="consumable"
|
||||||
|
type="checkbox"
|
||||||
|
label="Consumable / Food item"
|
||||||
|
/>
|
||||||
|
<FormField name="public" type="checkbox" label="Public item" />
|
||||||
|
<FormTextareaField name="notes" label="Notes" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<FormGroup name="transactionInfo">
|
||||||
|
<FormSelectField
|
||||||
|
:options="itemTransactionTypesOptions"
|
||||||
|
name="type"
|
||||||
|
required
|
||||||
|
label="Transaction type"
|
||||||
|
:disabled="!item"
|
||||||
|
/>
|
||||||
|
<FormDateField
|
||||||
|
name="actionAt"
|
||||||
|
label="Action committed at"
|
||||||
|
:disabled="!item"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 space-x-2">
|
||||||
|
<FormField
|
||||||
|
name="price"
|
||||||
|
label="Cost"
|
||||||
|
type="number"
|
||||||
|
step=".01"
|
||||||
|
class="col-span-3"
|
||||||
|
:disabled="!item"
|
||||||
|
/>
|
||||||
|
<FormSelectField
|
||||||
|
:options="currencies"
|
||||||
|
:disabled="!item"
|
||||||
|
name="currency"
|
||||||
|
label="Currency"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormTextareaField
|
||||||
|
:disabled="!item"
|
||||||
|
name="notes"
|
||||||
|
label="Additional notes about the transaction"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup name="additionalInfo">
|
||||||
|
<div class="grid grid-cols-3 space-x-2">
|
||||||
|
<FormDateField
|
||||||
|
name="expiresAt"
|
||||||
|
label="Expires at"
|
||||||
|
:disabled="!item"
|
||||||
|
/>
|
||||||
|
<FormDateField
|
||||||
|
name="acquiredAt"
|
||||||
|
label="Acquired at"
|
||||||
|
:disabled="!item"
|
||||||
|
/>
|
||||||
|
<FormDateField
|
||||||
|
name="consumedAt"
|
||||||
|
label="Consumed at"
|
||||||
|
:disabled="!item"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormTextareaField
|
||||||
|
name="notes"
|
||||||
|
label="Additional notes about the individual stored item"
|
||||||
|
:disabled="!item"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useAccessToken } from '../../composables/useAccessToken';
|
||||||
|
import { BACKEND_URL } from '../../constants';
|
||||||
|
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
||||||
|
import {
|
||||||
|
StorageItem,
|
||||||
|
StorageListItem,
|
||||||
|
StoredItem,
|
||||||
|
} from '../../interfaces/storage.interfaces';
|
||||||
|
import jfetch from '../../utils/jfetch';
|
||||||
|
import takeError from '../../utils/take-error';
|
||||||
|
import { FormSubmit, SelectOption } from '../form/form.types';
|
||||||
|
import Form from '../form/Form.vue';
|
||||||
|
import FormAlert from '../form/FormAlert.vue';
|
||||||
|
import FormField from '../form/FormField.vue';
|
||||||
|
import FormSelectField from '../form/fields/FormSelectField.vue';
|
||||||
|
import { IsRequired, MinLength } from '../form/validators';
|
||||||
|
import Modal from '../Modal.vue';
|
||||||
|
import { ItemType, ItemTypeName } from '../../enums/item-type.enum';
|
||||||
|
import {
|
||||||
|
TransactionType,
|
||||||
|
TransactionTypeDescription,
|
||||||
|
TransactionTypeName,
|
||||||
|
} from '../../enums/transaction-type.enum';
|
||||||
|
import FormGroup from '../form/FormGroup.vue';
|
||||||
|
import FormDateField from '../form/fields/FormDateField.vue';
|
||||||
|
import FormAutocompleteField from '../form/fields/FormAutocompleteField.vue';
|
||||||
|
import { FormValidator } from '../form/validator.types';
|
||||||
|
import deepUnref from '../../utils/deep-unref';
|
||||||
|
import FormTextareaField from '../form/fields/FormTextareaField.vue';
|
||||||
|
|
||||||
|
const defaults: any = {
|
||||||
|
item: null,
|
||||||
|
transactionInfo: {
|
||||||
|
actionAt: new Date(),
|
||||||
|
},
|
||||||
|
additionalInfo: {
|
||||||
|
acquiredAt: new Date(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { authHeader } = useAccessToken();
|
||||||
|
const modalRef = ref<InstanceType<typeof Modal>>();
|
||||||
|
const building = ref<BuildingListItem>();
|
||||||
|
const storage = ref<StorageListItem>();
|
||||||
|
const item = ref<StorageItem | string | null>(null);
|
||||||
|
const data: any = ref({
|
||||||
|
...deepUnref(defaults),
|
||||||
|
});
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'added', storage: StoredItem): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const itemTypesOptions = computed(() => {
|
||||||
|
return Object.keys(ItemTypeName).reduce<SelectOption[]>(
|
||||||
|
(list, key) => [
|
||||||
|
...list,
|
||||||
|
{
|
||||||
|
value: key.toString(),
|
||||||
|
name: ItemTypeName[key as ItemType],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemTransactionTypesOptions = computed(() => {
|
||||||
|
return Object.keys(TransactionTypeName).reduce<SelectOption[]>(
|
||||||
|
(list, key) => [
|
||||||
|
...list,
|
||||||
|
{
|
||||||
|
value: key.toString(),
|
||||||
|
name: TransactionTypeName[key as TransactionType],
|
||||||
|
description: TransactionTypeDescription[key as TransactionType],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const validators = ref<FormValidator[]>([
|
||||||
|
{
|
||||||
|
field: 'displayName',
|
||||||
|
validators: [MinLength(3), IsRequired()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
validators: [IsRequired()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'transactionInfo.type',
|
||||||
|
validators: [IsRequired()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'transactionInfo.actionAt',
|
||||||
|
validators: [IsRequired()],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currencies = [
|
||||||
|
{
|
||||||
|
value: 'EUR',
|
||||||
|
name: 'EUR',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'USD',
|
||||||
|
name: 'USD',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchForItems = async (search: string) => {
|
||||||
|
if (!search || search.length < 3) return [];
|
||||||
|
const { data: list } = await jfetch(
|
||||||
|
`${BACKEND_URL}/storage/item?searchTerm=${search}`,
|
||||||
|
{
|
||||||
|
headers: authHeader.value,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openModal: (useBuilding: BuildingListItem, useStorage: StorageListItem) => {
|
||||||
|
building.value = useBuilding;
|
||||||
|
storage.value = useStorage;
|
||||||
|
item.value = null;
|
||||||
|
data.value = {
|
||||||
|
...deepUnref(defaults),
|
||||||
|
};
|
||||||
|
modalRef.value?.openModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => data.value.item,
|
||||||
|
(selected) => {
|
||||||
|
if (selected && selected.id) {
|
||||||
|
item.value = selected;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.value = null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const startAddingNewItem = (insert: string) => {
|
||||||
|
item.value = insert;
|
||||||
|
data.value['displayName'] = insert;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (form: FormSubmit) => {
|
||||||
|
error.value = '';
|
||||||
|
if (!form.isValid || !storage.value) return;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
...form.formData,
|
||||||
|
};
|
||||||
|
delete body.item;
|
||||||
|
|
||||||
|
const requestUrl = `${BACKEND_URL}/storage/item/${storage.value.id}${
|
||||||
|
item.value && typeof item.value !== 'string' ? `/${item.value.id}` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
let createdStoredItem: StoredItem;
|
||||||
|
try {
|
||||||
|
const { data: response } = await jfetch(requestUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeader.value,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
createdStoredItem = response;
|
||||||
|
} catch (e) {
|
||||||
|
error.value = takeError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('added', createdStoredItem);
|
||||||
|
modalRef.value?.closeModal();
|
||||||
|
};
|
||||||
|
</script>
|
@ -6,6 +6,10 @@
|
|||||||
.dp__input {
|
.dp__input {
|
||||||
@apply block w-full rounded-md border-gray-300 shadow-sm transition-colors duration-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm;
|
@apply block w-full rounded-md border-gray-300 shadow-sm transition-colors duration-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
|
|
||||||
|
&.dp__disabled {
|
||||||
|
@apply bg-gray-100 hover:border-gray-400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dp__invalid .dp__input {
|
.dp__invalid .dp__input {
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
name="autocomplete"
|
name="autocomplete"
|
||||||
label="autocomplete"
|
label="autocomplete"
|
||||||
/>
|
/>
|
||||||
|
<FormTextareaField name="textarea" label="textarea" rows="3" />
|
||||||
<button type="submit">test</button>
|
<button type="submit">test</button>
|
||||||
</Form>
|
</Form>
|
||||||
</StandardLayout>
|
</StandardLayout>
|
||||||
@ -31,6 +32,7 @@ import Form from '../components/form/Form.vue';
|
|||||||
import FormField from '../components/form/FormField.vue';
|
import FormField from '../components/form/FormField.vue';
|
||||||
import { IsRequired } from '../components/form/validators';
|
import { IsRequired } from '../components/form/validators';
|
||||||
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue';
|
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue';
|
||||||
|
import FormTextareaField from '../components/form/fields/FormTextareaField.vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const user = ref(userStore.user);
|
const user = ref(userStore.user);
|
||||||
|
@ -118,6 +118,7 @@
|
|||||||
@select-room="selectRoomFromList"
|
@select-room="selectRoomFromList"
|
||||||
@select-storage="selectStorage"
|
@select-storage="selectStorage"
|
||||||
@new-storage="addNewStorage"
|
@new-storage="addNewStorage"
|
||||||
|
@add-item="newItem?.openModal(building!, selectedStorage!)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StoredItemCard
|
<StoredItemCard
|
||||||
@ -127,6 +128,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<NewStorageModal ref="newStorage" @added="storageAdded" />
|
<NewStorageModal ref="newStorage" @added="storageAdded" />
|
||||||
|
<NewItemModal ref="newItem" @added="newStoredItemAdded" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -150,6 +152,7 @@ import {
|
|||||||
StorageListItem,
|
StorageListItem,
|
||||||
StorageSetListItem,
|
StorageSetListItem,
|
||||||
StorageSharedType,
|
StorageSharedType,
|
||||||
|
StoredItem,
|
||||||
} from '../../../interfaces/storage.interfaces';
|
} from '../../../interfaces/storage.interfaces';
|
||||||
import StorageBubble from './StorageBubble.vue';
|
import StorageBubble from './StorageBubble.vue';
|
||||||
import RoomPolygon from './RoomPolygon.vue';
|
import RoomPolygon from './RoomPolygon.vue';
|
||||||
@ -158,6 +161,7 @@ import isSet from '../../../utils/is-storage-set';
|
|||||||
import ItemSelector from './ItemSelector.vue';
|
import ItemSelector from './ItemSelector.vue';
|
||||||
import StoredItemCard from '../../../components/item/StoredItemCard.vue';
|
import StoredItemCard from '../../../components/item/StoredItemCard.vue';
|
||||||
import NewStorageModal from '../../../components/item/NewStorageModal.vue';
|
import NewStorageModal from '../../../components/item/NewStorageModal.vue';
|
||||||
|
import NewItemModal from '../../../components/item/NewItemModal.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const buildingStore = useBuildingStore();
|
const buildingStore = useBuildingStore();
|
||||||
@ -174,6 +178,7 @@ const selectedSet = ref<StorageSetListItem>();
|
|||||||
const selectedStorage = ref<StorageListItem>();
|
const selectedStorage = ref<StorageListItem>();
|
||||||
|
|
||||||
const newStorage = ref<InstanceType<typeof NewStorageModal>>();
|
const newStorage = ref<InstanceType<typeof NewStorageModal>>();
|
||||||
|
const newItem = ref<InstanceType<typeof NewItemModal>>();
|
||||||
|
|
||||||
const floor = computed(() =>
|
const floor = computed(() =>
|
||||||
building.value?.floors.find(
|
building.value?.floors.find(
|
||||||
@ -319,4 +324,13 @@ const storageAdded = async (newStorage: StorageSharedType) => {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const newStoredItemAdded = async (newItem: StoredItem) => {
|
||||||
|
if (!selectedStorage.value) return;
|
||||||
|
selectedStorage.value.items = [
|
||||||
|
newItem,
|
||||||
|
...(selectedStorage.value.items || []),
|
||||||
|
];
|
||||||
|
selectedStorage.value.itemCount = selectedStorage.value.items.length;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user