FRONTEND: Add new UI components (ModalDialog, Dropdown, InputField) and store modules (AssistantStore, NodeStore) with comprehensive state management; refactor styles for consistency across buttons, input fields, and search components; enhance calculation and node creation workflows.
This commit is contained in:
parent
7280a1208f
commit
e7bcab5ae3
31 changed files with 2413 additions and 219 deletions
|
|
@ -28,8 +28,15 @@ export default {
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 3rem;
|
||||||
font-size: 24px;
|
font-size: 2.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-header {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6B869C;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
@ -38,7 +45,7 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
padding: 20px;
|
padding: 20px 60px;
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: 'Poppins', sans-serif;
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="autosuggest-container" ref="container">
|
<div class="autosuggest-container" ref="container">
|
||||||
<div class="search-input-wrapper">
|
<div class="search-wrapper">
|
||||||
|
<PhMagnifyingGlass :size="32" weight="bold" class="search-icon"/>
|
||||||
<input
|
<input
|
||||||
ref="searchInput"
|
ref="searchInput"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
|
|
@ -13,40 +14,51 @@
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<div v-if="isLoading" class="loading-indicator">
|
<div v-if="isLoading" class="loading-indicator">
|
||||||
<svg class="spinner" viewBox="0 0 24 24">
|
<spinner size="s"></spinner>
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="60" stroke-dashoffset="60">
|
|
||||||
<animate attributeName="stroke-dashoffset" dur="2s" values="60;0" repeatCount="indefinite"/>
|
|
||||||
</circle>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="showSuggestions && suggestions.length > 0"
|
<transition name="dropdown">
|
||||||
class="suggestions-dropdown"
|
<div
|
||||||
>
|
v-if="showSuggestions && suggestions.length > 0"
|
||||||
<ul class="suggestions-list" role="listbox">
|
class="suggestions-dropdown"
|
||||||
<li
|
>
|
||||||
v-for="(suggestion, index) in suggestions"
|
<ul class="suggestions-list">
|
||||||
:key="getSuggestionKey(suggestion, index)"
|
<li
|
||||||
@click="selectSuggestion(suggestion)"
|
v-for="(suggestion, index) in suggestions"
|
||||||
@mouseenter="highlightedIndex = index"
|
:key="getIdFor(suggestion, index)"
|
||||||
:class="[
|
@click="selectItem(suggestion)"
|
||||||
|
@mouseenter="highlightedIndex = index"
|
||||||
|
:class="[
|
||||||
'suggestion-item',
|
'suggestion-item',
|
||||||
{ 'highlighted': index === highlightedIndex }
|
{ 'highlighted': index === highlightedIndex }
|
||||||
]"
|
]"
|
||||||
role="option"
|
>
|
||||||
:aria-selected="index === highlightedIndex"
|
<div v-if="isFlagVariant">
|
||||||
>
|
<div class="suggestion-content-with-flag">
|
||||||
<div class="suggestion-content">
|
<flag :iso="getFlagIsoFor(suggestion)"></flag>
|
||||||
<span class="suggestion-text" v-html="highlightMatch(getSuggestionText(suggestion))"></span>
|
<div>
|
||||||
<span v-if="getSuggestionCategory(suggestion)" class="suggestion-category">
|
<div class="suggestion-title">{{ getTitleFor(suggestion) }}</div>
|
||||||
{{ getSuggestionCategory(suggestion) }}
|
<div v-if="getSubtitleFor(suggestion)" class="suggestion-subtitle">
|
||||||
</span>
|
{{ getSubtitleFor(suggestion) }}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="suggestion-content">
|
||||||
|
<span class="suggestion-title">{{ getTitleFor(suggestion) }}</span>
|
||||||
|
<basic-badge variant="grey" v-if="getSubtitleFor(suggestion)">{{
|
||||||
|
getSubtitleFor(suggestion)
|
||||||
|
}}
|
||||||
|
</basic-badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="showSuggestions && searchQuery && suggestions.length === 0 && !isLoading"
|
v-if="showSuggestions && searchQuery && suggestions.length === 0 && !isLoading"
|
||||||
|
|
@ -58,8 +70,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Spinner from "@/components/UI/Spinner.vue";
|
||||||
|
import Flag from "@/components/UI/Flag.vue";
|
||||||
|
import {useDebounceFn} from "@vueuse/core";
|
||||||
|
import BasicBadge from "@/components/UI/BasicBadge.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AutosuggestSearchbar',
|
name: 'AutosuggestSearchbar',
|
||||||
|
components: {BasicBadge, Flag, Spinner},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
// Display props
|
// Display props
|
||||||
|
|
@ -71,49 +89,51 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'No results found for "{query}"'
|
default: 'No results found for "{query}"'
|
||||||
},
|
},
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
default: "tags"
|
||||||
|
},
|
||||||
// Behavior props
|
// Behavior props
|
||||||
minChars: {
|
minChars: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 2
|
default: 2
|
||||||
},
|
},
|
||||||
debounceMs: {
|
|
||||||
type: Number,
|
|
||||||
default: 300
|
|
||||||
},
|
|
||||||
maxSuggestions: {
|
maxSuggestions: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 10
|
default: 10
|
||||||
},
|
},
|
||||||
|
|
||||||
// Data source functions
|
|
||||||
fetchSuggestions: {
|
fetchSuggestions: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
// Expected signature: async (query: string) => Array
|
// Expected signature: async (query: string) => Array
|
||||||
},
|
},
|
||||||
|
titleResolver: {
|
||||||
// Optional data extraction functions for custom suggestion objects
|
|
||||||
suggestionTextKey: {
|
|
||||||
type: [String, Function],
|
type: [String, Function],
|
||||||
default: 'text'
|
default: 'title'
|
||||||
// Can be string (property name) or function (suggestion => string)
|
|
||||||
},
|
},
|
||||||
suggestionCategoryKey: {
|
subtitleResolver: {
|
||||||
type: [String, Function],
|
type: [String, Function],
|
||||||
default: 'category'
|
default: 'subtitle'
|
||||||
// Can be string (property name) or function (suggestion => string)
|
|
||||||
},
|
},
|
||||||
suggestionKeyProperty: {
|
idResolver: {
|
||||||
type: [String, Function],
|
type: [String, Function],
|
||||||
default: 'id'
|
default: 'id'
|
||||||
// Can be string (property name) or function (suggestion => string/number)
|
|
||||||
},
|
},
|
||||||
|
flagResolver: {
|
||||||
// Initial state
|
type: [String, Function],
|
||||||
|
default: 'iso'
|
||||||
|
},
|
||||||
|
resetOnSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
initialSuggestions: {
|
initialSuggestions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
initialValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -124,25 +144,19 @@ export default {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
showSuggestions: false,
|
showSuggestions: false,
|
||||||
highlightedIndex: -1,
|
highlightedIndex: -1,
|
||||||
debounceTimer: null
|
debounceTimer: null,
|
||||||
|
debouncedSearch: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filteredSuggestions() {
|
isFlagVariant() {
|
||||||
return this.suggestions.slice(0, this.maxSuggestions)
|
return this.variant === 'flags';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onInput() {
|
onInput() {
|
||||||
if (this.debounceTimer) {
|
this.debouncedSearch(this.searchQuery);
|
||||||
clearTimeout(this.debounceTimer)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.debounceTimer = setTimeout(() => {
|
|
||||||
this.handleSearch()
|
|
||||||
}, this.debounceMs)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleSearch() {
|
async handleSearch() {
|
||||||
|
|
@ -200,7 +214,7 @@ export default {
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (this.highlightedIndex >= 0) {
|
if (this.highlightedIndex >= 0) {
|
||||||
this.selectSuggestion(this.suggestions[this.highlightedIndex])
|
this.selectItem(this.suggestions[this.highlightedIndex])
|
||||||
} else {
|
} else {
|
||||||
this.handleEnterWithoutSelection()
|
this.handleEnterWithoutSelection()
|
||||||
}
|
}
|
||||||
|
|
@ -217,10 +231,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
selectSuggestion(suggestion) {
|
selectItem(suggestion) {
|
||||||
this.searchQuery = this.getSuggestionText(suggestion)
|
if (this.resetOnSelect) {
|
||||||
|
this.searchQuery = '';
|
||||||
|
} else {
|
||||||
|
this.searchQuery = this.getTitleFor(suggestion)
|
||||||
|
|
||||||
|
}
|
||||||
this.hideSuggestions()
|
this.hideSuggestions()
|
||||||
this.$emit('suggestion-selected', suggestion)
|
this.$emit('selected', suggestion)
|
||||||
this.$emit('search', this.searchQuery)
|
this.$emit('search', this.searchQuery)
|
||||||
this.$refs.searchInput.blur()
|
this.$refs.searchInput.blur()
|
||||||
},
|
},
|
||||||
|
|
@ -236,36 +255,31 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Helper methods for extracting data from suggestion objects
|
// Helper methods for extracting data from suggestion objects
|
||||||
getSuggestionText(suggestion) {
|
getTitleFor(suggestion) {
|
||||||
if (typeof this.suggestionTextKey === 'function') {
|
if (typeof this.titleResolver === 'function') {
|
||||||
return this.suggestionTextKey(suggestion)
|
return this.titleResolver(suggestion)
|
||||||
}
|
}
|
||||||
return suggestion[this.suggestionTextKey] || suggestion.toString()
|
return suggestion[this.titleResolver] || suggestion.toString()
|
||||||
},
|
},
|
||||||
|
|
||||||
getSuggestionCategory(suggestion) {
|
getSubtitleFor(suggestion) {
|
||||||
if (typeof this.suggestionCategoryKey === 'function') {
|
if (typeof this.subtitleResolver === 'function') {
|
||||||
return this.suggestionCategoryKey(suggestion)
|
return this.subtitleResolver(suggestion)
|
||||||
}
|
}
|
||||||
return suggestion[this.suggestionCategoryKey] || null
|
return suggestion[this.subtitleResolver] || null
|
||||||
},
|
},
|
||||||
|
getFlagIsoFor(suggestion) {
|
||||||
getSuggestionKey(suggestion, index) {
|
if (typeof this.flagResolver === 'function') {
|
||||||
if (typeof this.suggestionKeyProperty === 'function') {
|
return this.flagResolver(suggestion)
|
||||||
return this.suggestionKeyProperty(suggestion)
|
|
||||||
}
|
}
|
||||||
return suggestion[this.suggestionKeyProperty] || index
|
return suggestion[this.flagResolver] || null
|
||||||
},
|
},
|
||||||
|
|
||||||
highlightMatch(text) {
|
getIdFor(suggestion, index) {
|
||||||
if (!this.searchQuery || !text) return text
|
if (typeof this.idResolver === 'function') {
|
||||||
|
return this.idResolver(suggestion)
|
||||||
const regex = new RegExp(`(${this.escapeRegex(this.searchQuery)})`, 'gi')
|
}
|
||||||
return text.replace(regex, '<mark>$1</mark>')
|
return suggestion[this.idResolver] || index
|
||||||
},
|
|
||||||
|
|
||||||
escapeRegex(string) {
|
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleClickOutside(event) {
|
handleClickOutside(event) {
|
||||||
|
|
@ -294,7 +308,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.debouncedSearch = useDebounceFn((query) => {
|
||||||
|
this.handleSearch(query);
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.searchQuery = this.initialValue;
|
||||||
document.addEventListener('click', this.handleClickOutside)
|
document.addEventListener('click', this.handleClickOutside)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -324,41 +345,76 @@ export default {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.autosuggest-container {
|
.autosuggest-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input-wrapper {
|
.search-wrapper {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-wrapper:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 100%;
|
border: none;
|
||||||
padding: 12px 16px;
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s ease;
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #002F54;
|
||||||
|
font-weight: 400;
|
||||||
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input:focus {
|
.search-icon {
|
||||||
border-color: #007bff;
|
width: 1.8rem;
|
||||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
height: 1.8rem;
|
||||||
|
margin-right: 1.2rem;
|
||||||
|
color: #6B869C;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: #6B869C;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-indicator {
|
.loading-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 16px;
|
right: 1.6rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.dropdown-enter-active {
|
||||||
width: 20px;
|
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
height: 20px;
|
}
|
||||||
color: #007bff;
|
|
||||||
|
.dropdown-leave-active {
|
||||||
|
transition: all 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-enter-from {
|
||||||
|
transform: translateY(-30px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-leave-to {
|
||||||
|
transform: translateY(-30px);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions-dropdown {
|
.suggestions-dropdown {
|
||||||
|
|
@ -367,13 +423,14 @@ export default {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid #e0e0e0;
|
border: 0.1rem solid #E3EDFF;
|
||||||
border-radius: 8px;
|
border-radius: 0.8rem;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
max-height: 300px;
|
max-height: 50rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-top: 4px;
|
margin-top: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestions-list {
|
.suggestions-list {
|
||||||
|
|
@ -383,9 +440,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion-item {
|
.suggestion-item {
|
||||||
padding: 12px 16px;
|
padding: 1.2rem 1.6rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 0.16rem solid #f3f4f6;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -395,7 +452,15 @@ export default {
|
||||||
|
|
||||||
.suggestion-item:hover,
|
.suggestion-item:hover,
|
||||||
.suggestion-item.highlighted {
|
.suggestion-item.highlighted {
|
||||||
background-color: #f8f9fa;
|
background-color: rgba(107, 134, 156, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-content-with-flag {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion-content {
|
.suggestion-content {
|
||||||
|
|
@ -404,37 +469,38 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion-text {
|
.suggestion-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 14px;
|
font-size: 1.6rem;
|
||||||
color: #333;
|
color: #001D33;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion-text :deep(mark) {
|
.suggestion-subtitle {
|
||||||
background-color: #fff3cd;
|
font-size: 1.4rem;
|
||||||
color: #856404;
|
color: #6b7280;
|
||||||
padding: 0 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion-category {
|
.suggestion-subtitle-tag {
|
||||||
font-size: 12px;
|
font-size: 1.4rem;
|
||||||
color: #666;
|
color: #ffffff;
|
||||||
background-color: #e9ecef;
|
background-color: #003c6a;
|
||||||
padding: 2px 6px;
|
padding: 0.4rem 0.8rem;
|
||||||
border-radius: 4px;
|
border-radius: 0.4rem;
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-results {
|
.no-results {
|
||||||
padding: 16px;
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 1.6rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #666;
|
color: #001D33;
|
||||||
font-style: italic;
|
border: 0.1rem solid #e0e0e0;
|
||||||
border: 1px solid #e0e0e0;
|
border-radius: 0.8rem;
|
||||||
border-radius: 8px;
|
margin-top: 0.4rem;
|
||||||
margin-top: 4px;
|
|
||||||
background: white;
|
background: white;
|
||||||
|
font-size: 1.6rem
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile responsiveness */
|
/* Mobile responsiveness */
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
weight="bold"
|
weight="bold"
|
||||||
size="18"
|
size="18"
|
||||||
class="btn-icon"
|
class="btn-icon"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<span class="btn-text"><slot></slot></span>
|
<span class="btn-text"><slot></slot></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -92,15 +91,16 @@ export default defineComponent({
|
||||||
border: 0.2rem solid transparent;
|
border: 0.2rem solid transparent;
|
||||||
border-radius: 0.8rem;
|
border-radius: 0.8rem;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
outline: none;
|
outline: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Primary variant */
|
/* Primary variant */
|
||||||
.btn--primary {
|
.btn--primary {
|
||||||
|
font-weight: 500;
|
||||||
background-color: #5AF0B4;
|
background-color: #5AF0B4;
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
|
@ -113,6 +113,7 @@ export default defineComponent({
|
||||||
|
|
||||||
/* Secondary variant */
|
/* Secondary variant */
|
||||||
.btn--secondary {
|
.btn--secondary {
|
||||||
|
font-weight: 500;
|
||||||
background-color: #002F54;
|
background-color: #002F54;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
|
@ -122,6 +123,7 @@ export default defineComponent({
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
border-color: #002F54;
|
border-color: #002F54;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -133,6 +135,7 @@ export default defineComponent({
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-text {
|
.btn-text {
|
||||||
|
|
|
||||||
23
src/frontend/src/components/UI/Box.vue
Normal file
23
src/frontend/src/components/UI/Box.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<div class="box">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Box"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.box {
|
||||||
|
display: flex;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
351
src/frontend/src/components/UI/Dropdown.vue
Normal file
351
src/frontend/src/components/UI/Dropdown.vue
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
<template>
|
||||||
|
<div class="dropdown" ref="dropdown">
|
||||||
|
<button
|
||||||
|
ref="trigger"
|
||||||
|
class="dropdown-trigger"
|
||||||
|
:class="{ 'dropdown-trigger--open': isOpen }"
|
||||||
|
@click="toggleDropdown"
|
||||||
|
@keydown="handleTriggerKeydown"
|
||||||
|
>
|
||||||
|
<span class="dropdown-trigger-text">
|
||||||
|
{{ selectedOption ? selectedOption[displayKey] : placeholder }}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="dropdown-trigger-icon"
|
||||||
|
:class="{ 'dropdown-trigger-icon--rotated': isOpen }"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="6,9 12,15 18,9"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="dropdown-fade">
|
||||||
|
<ul
|
||||||
|
v-if="isOpen"
|
||||||
|
ref="menu"
|
||||||
|
class="dropdown-menu"
|
||||||
|
@keydown="handleMenuKeydown"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="option[valueKey]"
|
||||||
|
class="dropdown-option"
|
||||||
|
:class="{
|
||||||
|
'dropdown-option--selected': isSelected(option),
|
||||||
|
'dropdown-option--focused': focusedIndex === index
|
||||||
|
}"
|
||||||
|
@click="selectOption(option)"
|
||||||
|
@mouseenter="focusedIndex = index"
|
||||||
|
>
|
||||||
|
{{ option[displayKey] }}
|
||||||
|
</li>
|
||||||
|
<li v-if="options.length === 0" class="dropdown-option dropdown-option--empty">
|
||||||
|
{{ emptyText }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Dropdown',
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number, Object],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Select an option'
|
||||||
|
},
|
||||||
|
displayKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'value'
|
||||||
|
},
|
||||||
|
valueKey: {
|
||||||
|
type: String,
|
||||||
|
default: 'id'
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
type: String,
|
||||||
|
default: 'No options available'
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'change'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isOpen: false,
|
||||||
|
focusedIndex: -1,
|
||||||
|
labelId: `dropdown-${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedOption() {
|
||||||
|
if (!this.modelValue) return null
|
||||||
|
return this.options.find(option =>
|
||||||
|
option[this.valueKey] === this.modelValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener('click', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
document.removeEventListener('click', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleDropdown() {
|
||||||
|
if (this.disabled) return
|
||||||
|
|
||||||
|
this.isOpen = !this.isOpen
|
||||||
|
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.menu?.focus()
|
||||||
|
this.focusedIndex = this.selectedOption ?
|
||||||
|
this.options.findIndex(option => option[this.valueKey] === this.modelValue) :
|
||||||
|
0
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.focusedIndex = -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectOption(option) {
|
||||||
|
this.$emit('update:modelValue', option[this.valueKey])
|
||||||
|
this.$emit('change', option)
|
||||||
|
this.closeDropdown()
|
||||||
|
this.$refs.trigger.focus()
|
||||||
|
},
|
||||||
|
closeDropdown() {
|
||||||
|
this.isOpen = false
|
||||||
|
this.focusedIndex = -1
|
||||||
|
},
|
||||||
|
isSelected(option) {
|
||||||
|
return this.modelValue === option[this.valueKey]
|
||||||
|
},
|
||||||
|
handleClickOutside(event) {
|
||||||
|
if (!this.$refs.dropdown?.contains(event.target)) {
|
||||||
|
this.closeDropdown()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleTriggerKeydown(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault()
|
||||||
|
if (!this.isOpen) {
|
||||||
|
this.toggleDropdown()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault()
|
||||||
|
if (!this.isOpen) {
|
||||||
|
this.toggleDropdown()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
if (this.isOpen) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.closeDropdown()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleMenuKeydown(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault()
|
||||||
|
this.focusedIndex = Math.min(this.focusedIndex + 1, this.options.length - 1)
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault()
|
||||||
|
this.focusedIndex = Math.max(this.focusedIndex - 1, 0)
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault()
|
||||||
|
if (this.focusedIndex >= 0 && this.options[this.focusedIndex]) {
|
||||||
|
this.selectOption(this.options[this.focusedIndex])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault()
|
||||||
|
this.closeDropdown()
|
||||||
|
this.$refs.trigger.focus()
|
||||||
|
break
|
||||||
|
case 'Tab':
|
||||||
|
this.closeDropdown()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.dropdown-trigger--open {
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
background: #EEF4FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger-text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
color: #2d3748;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger-icon {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger-icon--rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
font: inherit;
|
||||||
|
outline: none;
|
||||||
|
list-style: none;
|
||||||
|
color: #2d3748;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: white;
|
||||||
|
border: 0.1rem solid #E3EDFF;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 50rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option {
|
||||||
|
font: inherit;
|
||||||
|
padding: 1.2rem 1.6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 0.16rem solid #f3f4f6;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
color: #2d3748;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option:hover,
|
||||||
|
.dropdown-option--focused {
|
||||||
|
background-color: rgba(107, 134, 156, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option--selected {
|
||||||
|
color: #2d3748;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option--selected:hover,
|
||||||
|
.dropdown-option--selected.dropdown-option--focused {
|
||||||
|
color: #2d3748;
|
||||||
|
background-color: rgba(107, 134, 156, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option--empty {
|
||||||
|
color: #001D33;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option--empty:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition animations */
|
||||||
|
.dropdown-fade-enter-active,
|
||||||
|
.dropdown-fade-leave-active {
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled state */
|
||||||
|
.dropdown-trigger:disabled {
|
||||||
|
background-color: #f7fafc;
|
||||||
|
color: #a0aec0;
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger:disabled:hover {
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.dropdown-trigger {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flag-container">
|
||||||
<img :src="path" :alt="iso" :class="flagSizeClass">
|
<img :src="path" :alt="iso" :class="flagSizeClass">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -31,6 +31,12 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.flag-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.flag-s {
|
.flag-s {
|
||||||
height: 1.2rem; /* 12px */
|
height: 1.2rem; /* 12px */
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
src/frontend/src/components/UI/InputField.vue
Normal file
63
src/frontend/src/components/UI/InputField.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input class="input-field"
|
||||||
|
v-model="modelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "InputField",
|
||||||
|
props: {
|
||||||
|
name: 'InputField',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
117
src/frontend/src/components/UI/Modal.vue
Normal file
117
src/frontend/src/components/UI/Modal.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<transition name="modal" appear>
|
||||||
|
<div
|
||||||
|
v-if="isVisible"
|
||||||
|
class="modal-overlay"
|
||||||
|
@click="closeOnBackdrop && close()"
|
||||||
|
@keydown.esc="closeOnEscape && close()"
|
||||||
|
tabindex="-1"
|
||||||
|
ref="modalOverlay"
|
||||||
|
>
|
||||||
|
<box @click.stop>
|
||||||
|
<slot></slot>
|
||||||
|
</box>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Box from "@/components/UI/Box.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Modal",
|
||||||
|
components: {Box},
|
||||||
|
props: {
|
||||||
|
state: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
closeOnBackdrop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
closeOnEscape: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
computed: {
|
||||||
|
isVisible() {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isVisible(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.handleOpen();
|
||||||
|
} else {
|
||||||
|
this.handleClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.isVisible) {
|
||||||
|
this.handleOpen();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.handleClose();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
handleOpen() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.modalOverlay) {
|
||||||
|
this.$refs.modalOverlay.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleClose() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition animations */
|
||||||
|
.modal-enter-active,
|
||||||
|
.modal-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from,
|
||||||
|
.modal-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-active .box,
|
||||||
|
.modal-leave-active .box {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from .box,
|
||||||
|
.modal-leave-to .box {
|
||||||
|
transform: scale(0.9) translateY(-10px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
src/frontend/src/components/UI/ModalDialog.vue
Normal file
90
src/frontend/src/components/UI/ModalDialog.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<modal :state="state">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-dialog-title sub-header">{{ title }}</div>
|
||||||
|
<div class="modal-dialog-message">{{ message }}</div>
|
||||||
|
<div class="modal-dialog-actions">
|
||||||
|
<basic-button :show-icon="false" @click="action('accept')">{{ acceptText }}</basic-button>
|
||||||
|
<basic-button :show-icon="false" @click="action('deny')" v-if="denyText!=null">{{ denyText }}</basic-button>
|
||||||
|
<basic-button :show-icon="false" @click="action('dismiss')" variant="secondary">{{ dismissText }}</basic-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import Modal from "@/components/UI/Modal.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ModalDialog",
|
||||||
|
components: {Modal, BasicButton},
|
||||||
|
emits: ['click'],
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
denyText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
acceptText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'OK'
|
||||||
|
},
|
||||||
|
dismissText: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'Cancel'
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
action(action) {
|
||||||
|
this.$emit('click', action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog-title {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6B869C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog-message {
|
||||||
|
display: flex;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
max-width: 60rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -2,31 +2,67 @@
|
||||||
import {defineComponent} from 'vue'
|
import {defineComponent} from 'vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Spinner"
|
name: "Spinner",
|
||||||
|
props: {
|
||||||
|
size: {
|
||||||
|
type: String as () => 's' | 'm',
|
||||||
|
default: 'm',
|
||||||
|
validator: (value: string) => ['s', 'm'].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
loaderClass() {
|
||||||
|
return `loader loader--${this.size}`
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="loader"></span>
|
<span :class="loaderClass"></span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.loader {
|
.loader {
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border: 5px solid #6B869C;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: pulse 1s linear infinite;
|
animation: pulse 1s linear infinite;
|
||||||
|
border-color: #6B869C;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Medium size (default) */
|
||||||
|
.loader--m {
|
||||||
|
width: 4.8rem;
|
||||||
|
height: 4.8rem;
|
||||||
|
border-width: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader--m:after {
|
||||||
|
width: 4.8rem;
|
||||||
|
height: 4.8rem;
|
||||||
|
border-width: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small size */
|
||||||
|
.loader--s {
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
border-width: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader--s:after {
|
||||||
|
width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
border-width: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.loader:after {
|
.loader:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 48px;
|
border-color: #6B869C;
|
||||||
height: 48px;
|
border-style: solid;
|
||||||
border: 5px solid #6B869C;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
@ -38,10 +74,10 @@ export default defineComponent({
|
||||||
|
|
||||||
@keyframes scaleUp {
|
@keyframes scaleUp {
|
||||||
0% { transform: translate(-50%, -50%) scale(0) }
|
0% { transform: translate(-50%, -50%) scale(0) }
|
||||||
60% , 100% { transform: translate(-50%, -50%) scale(1)}
|
60% , 100% { transform: translate(-50%, -50%) scale(1)}
|
||||||
}
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% , 60% , 100%{ transform: scale(1) }
|
0% , 60% , 100%{ transform: scale(1) }
|
||||||
80% { transform: scale(1.2)}
|
80% { transform: scale(1.2)}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -88,7 +88,6 @@ export default {
|
||||||
.tooltip-wrapper {
|
.tooltip-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<img alt="LCC logo" class="logo" src="../../assets/logo.svg" width="125" height="125" />
|
<img alt="LCC logo" class="logo" src="../../assets/logo.svg" width="125" height="125" />
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><navigation-element class="navigationbox" to="/calculations">Calculation</navigation-element></li>
|
<li><navigation-element class="navigationbox" to="/calculations">My Calculation</navigation-element></li>
|
||||||
<li><navigation-element class="navigationbox" to="/reports">Reporting</navigation-element></li>
|
<li><navigation-element class="navigationbox" to="/reports">Reporting</navigation-element></li>
|
||||||
<li><navigation-element class="navigationbox" to="/config">Configuration</navigation-element></li>
|
<li><navigation-element class="navigationbox" to="/config">Configuration</navigation-element></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {defineComponent} from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "TheNewSupplierDialog"
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,15 +1,71 @@
|
||||||
<script lang="ts">
|
|
||||||
import {defineComponent} from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "MaterialItem"
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="item-container">
|
||||||
|
<div class="material-item-text">
|
||||||
|
<div class="supplier-item-name">{{name}}</div>
|
||||||
|
<div class="supplier-item-address">{{partNumber}}</div>
|
||||||
|
</div>
|
||||||
|
<icon-button icon="trash" @click="deleteClick"></icon-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MaterialItem",
|
||||||
|
components: {IconButton},
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
partNumber: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteClick() {
|
||||||
|
this.$emit("delete", this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.item-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3.6rem 3.6rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 2.4rem;
|
||||||
|
flex: 0 0 50rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-container:hover {
|
||||||
|
background-color: rgba(107, 134, 156, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplier-item-name {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #001D33
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplier-item-address {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,15 +1,93 @@
|
||||||
<script lang="ts">
|
|
||||||
import {defineComponent} from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "SupplierItem"
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="item-container">
|
||||||
|
<flag :iso="isoCode" size="l"></flag>
|
||||||
|
<div class="supplier-item-text">
|
||||||
|
<div class="supplier-item-name"> <span class="user-icon" v-if="isUserSupplier"><ph-user weight="fill" ></ph-user></span> {{name}}</div>
|
||||||
|
<div class="supplier-item-address">{{ address }}</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<icon-button icon="trash" @click="deleteClick"></icon-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import Flag from "@/components/UI/Flag.vue";
|
||||||
|
import {PhStar, PhUser} from "@phosphor-icons/vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SupplierItem",
|
||||||
|
components: {PhUser, PhStar, Flag, IconButton},
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isoCode: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isUserSupplier: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteClick() {
|
||||||
|
this.$emit("delete", this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.item-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3.6rem 3.6rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 2.4rem;
|
||||||
|
flex: 0 0 50rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-container:hover {
|
||||||
|
background-color: rgba(107, 134, 156, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.supplier-item-name {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #001D33;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplier-item-address {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {defineComponent} from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "TheDropPartNumbersDialog"
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -74,6 +74,7 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,18 +82,18 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 0.4rem;
|
||||||
padding: 6px 12px;
|
padding: 0.6rem 1.2rem;
|
||||||
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
backdrop-filter: blur(10px);*/
|
backdrop-filter: blur(10px);*/
|
||||||
border: 2px solid #E3EDFF;
|
border: 0.2rem solid #E3EDFF;
|
||||||
transition: all 0.1s ease;
|
transition: all 0.1s ease;
|
||||||
flex: 1 0 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-wrapper:hover {
|
.search-wrapper:hover {
|
||||||
background: #EEF4FF;
|
background: #EEF4FF;
|
||||||
border: 2px solid #8DB3FE;
|
border: 0.2rem solid #8DB3FE;
|
||||||
/*transform: translateY(2px);*/
|
/*transform: translateY(2px);*/
|
||||||
transform: scale(1.01);
|
transform: scale(1.01);
|
||||||
}
|
}
|
||||||
|
|
@ -103,18 +104,18 @@ export default {
|
||||||
//} */
|
//} */
|
||||||
|
|
||||||
.search-icon {
|
.search-icon {
|
||||||
width: 18px;
|
width: 1.8rem;
|
||||||
height: 18px;
|
height: 1.8rem;
|
||||||
margin-right: 12px;
|
margin-right: 1.2rem;
|
||||||
color: #6B869C;
|
color: #6B869C;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-size: 14px;
|
font-size: 1.4rem;
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
DestinationListView
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DestinationListView",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
148
src/frontend/src/components/layout/edit/MaterialEdit.vue
Normal file
148
src/frontend/src/components/layout/edit/MaterialEdit.vue
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="caption-column">Part number</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<autosuggest-searchbar v-if="editMode" :fetch-suggestions="fetchPartNumbers" :initial-value="partNumber"
|
||||||
|
placeholder="Find part number"></autosuggest-searchbar>
|
||||||
|
<span v-else>{{ partNumber }}</span>
|
||||||
|
<modal-dialog :state="modalDialogPartNumberState"
|
||||||
|
accept-text="Yes"
|
||||||
|
deny-text="No"
|
||||||
|
@click="modalDialogClick"
|
||||||
|
message="You have changed the part number. Your current master data (like packaging dimensions) might be outdated. Do you want to reload master data?"
|
||||||
|
title="Update master data"></modal-dialog>
|
||||||
|
<icon-button :icon="editIconPartNumber" @click="toggleEditMode"></icon-button>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">Description</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<span>{{ description }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">HS code</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<autosuggest-searchbar v-if="!editMode" :fetch-suggestions="fetchHsCode" :initial-value="hsCode"
|
||||||
|
placeholder="Find hs code"></autosuggest-searchbar>
|
||||||
|
<div v-else>{{ hsCode }}</div>
|
||||||
|
<div class="caption-column">Tariff rate [%]</div>
|
||||||
|
<div v-if="!editMode" class="input-field-container input-field-tariffrate">
|
||||||
|
<input v-model="tariffRate" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
<span v-else>{{ tariffRate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import Flag from "@/components/UI/Flag.vue";
|
||||||
|
import InputField from "@/components/UI/InputField.vue";
|
||||||
|
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
|
||||||
|
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "MaterialEdit",
|
||||||
|
components: {ModalDialog, AutosuggestSearchbar, InputField, Flag, IconButton},
|
||||||
|
props: {},
|
||||||
|
computed: {
|
||||||
|
editIconPartNumber() {
|
||||||
|
return this.editMode ? "check" : "pencil-simple";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
partNumber: "28152640129",
|
||||||
|
description: "gearbox housing blank 'GR4H-10",
|
||||||
|
hsCode: "87111000",
|
||||||
|
tariffRate: 0.05,
|
||||||
|
editMode: false,
|
||||||
|
modalDialogPartNumberState: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
modalDialogClick(action) {
|
||||||
|
console.log(action);
|
||||||
|
this.modalDialogPartNumberState = false;
|
||||||
|
|
||||||
|
},
|
||||||
|
toggleEditMode() {
|
||||||
|
if(this.editMode) {
|
||||||
|
this.modalDialogPartNumberState = true;
|
||||||
|
} else {
|
||||||
|
this.editMode = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchPartNumbers(query) {
|
||||||
|
return [1, 2, 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: repeat(3, fit-content(0));
|
||||||
|
gap: 1.6rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-column {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #6b7280;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption-column {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: end;
|
||||||
|
color: #001D33;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-tariffrate {
|
||||||
|
min-width: 7rem;
|
||||||
|
flex: 0 1 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
171
src/frontend/src/components/layout/edit/PackagingEdit.vue
Normal file
171
src/frontend/src/components/layout/edit/PackagingEdit.vue
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="caption-column">Length</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input v-model="huLength" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">Dimension Unit</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<dropdown :options="huDimensionUnits" v-model="huDimensionUnitSelected"></dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="caption-column">Width</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input v-model="huWidth" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column"></div>
|
||||||
|
<div class="input-column"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="caption-column">Height</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input v-model="huHeight" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column"></div>
|
||||||
|
<div class="input-column"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="caption-column">Weight</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input v-model="huWeight" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">Weight Unit</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<dropdown :options="huWeightUnits" v-model="huWeightUnitSelected"></dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="caption-column">Pieces per HU</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<tooltip text="Single pieces per HU">
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input v-model="huCount" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-column-chk">
|
||||||
|
<tooltip position="left" text="Deselect if the handling unit cannot be stacked">
|
||||||
|
<checkbox>stackable</checkbox>
|
||||||
|
</tooltip>
|
||||||
|
<tooltip position="left" text="Deselect if the handling unit cannot be transported together with other handling units">
|
||||||
|
<checkbox>mixable</checkbox>
|
||||||
|
</tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||||
|
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||||
|
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PackagingEdit",
|
||||||
|
components: {Tooltip, Dropdown, Checkbox},
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
huLength: 0.0,
|
||||||
|
huWidth: 0.0,
|
||||||
|
huHeight: 0.0,
|
||||||
|
huWeight: 0.0,
|
||||||
|
huWeightUnit: "kg",
|
||||||
|
huCount: 1,
|
||||||
|
huDimensionUnitSelected: 1,
|
||||||
|
huDimensionUnits: [{id: 1, value: "CM"}, {id: 2, value: "MM"}, {id: 3, value: "M"}],
|
||||||
|
huWeightUnitSelected: 1,
|
||||||
|
huWeightUnits: [{id: 1, value: "KG"}, {id: 2, value: "G"}, {id: 3, value: "T"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
|
grid-template-rows: repeat(5, fit-content(0));
|
||||||
|
gap: 1.6rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 5rem;
|
||||||
|
max-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-column-chk {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-self: center;
|
||||||
|
grid-column: -3/-1;
|
||||||
|
gap: 1.6rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.input-column {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex: 1 1 min-content;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption-column {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: end;
|
||||||
|
color: #001D33;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
110
src/frontend/src/components/layout/edit/PriceEdit.vue
Normal file
110
src/frontend/src/components/layout/edit/PriceEdit.vue
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="caption-column">MEK_A [EUR]</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input v-model="price" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">Oversea share [%]</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<div class="input-field-container">
|
||||||
|
<input v-model="overSeashare" class="input-field"
|
||||||
|
autocomplete="off"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">Include FCA Fee</div>
|
||||||
|
<div class="input-column">
|
||||||
|
<tooltip text="Select if a additional FCA has to be added during calculation"><checkbox></checkbox></tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||||
|
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "PriceEdit",
|
||||||
|
components: {Tooltip, Checkbox},
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
price: 0.0,
|
||||||
|
overSeashare: 0.0,
|
||||||
|
addFcaFee: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
|
gap: 1.6rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.input-column {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #6b7280;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex: 1 1 fit-content(80rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption-column {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: end;
|
||||||
|
color: #001D33;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
145
src/frontend/src/components/layout/edit/SupplierView.vue
Normal file
145
src/frontend/src/components/layout/edit/SupplierView.vue
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="caption-column">Supplier</div>
|
||||||
|
<div class="input-field input-field-name"><span class="user-icon" v-if="isUserSupplier"><ph-user
|
||||||
|
weight="fill"></ph-user></span> {{ supplierName }}
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">Address</div>
|
||||||
|
<div class="input-field input-field-address">
|
||||||
|
<div class="supplier-flag">
|
||||||
|
<flag iso="CN"/>
|
||||||
|
</div>
|
||||||
|
<div class="supplier-address">{{ supplierAddress }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="caption-column">Coordinates</div>
|
||||||
|
<div class="input-field">{{ coordinatesDMS }}</div>
|
||||||
|
<div class="footer">
|
||||||
|
<icon-button icon="pencil-simple"></icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
import IconButton from "@/components/UI/IconButton.vue";
|
||||||
|
import InputField from "@/components/UI/InputField.vue";
|
||||||
|
import Flag from "@/components/UI/Flag.vue";
|
||||||
|
import {PhUser} from "@phosphor-icons/vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SupplierView",
|
||||||
|
components: {PhUser, Flag, InputField, IconButton},
|
||||||
|
props: {
|
||||||
|
supplierName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
supplierAddress: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
supplierCoordinates: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
|
||||||
|
},
|
||||||
|
isUserSupplier: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
// Kombinierte Ausgabe beider Koordinaten
|
||||||
|
coordinatesDMS() {
|
||||||
|
return `${this.convertToDMS(this.supplierCoordinates?.lat, 'lat')}, ${this.convertToDMS(this.supplierCoordinates?.lng, 'lng')}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
convertToDMS(coordinate, type) {
|
||||||
|
|
||||||
|
if (!coordinate)
|
||||||
|
return '';
|
||||||
|
|
||||||
|
let direction;
|
||||||
|
if (type === 'lat') {
|
||||||
|
direction = coordinate >= 0 ? 'N' : 'S';
|
||||||
|
} else {
|
||||||
|
direction = coordinate >= 0 ? 'E' : 'W';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arbeite mit Absolutwert
|
||||||
|
const abs = Math.abs(coordinate);
|
||||||
|
|
||||||
|
// Grad (ganzzahliger Teil)
|
||||||
|
const degrees = Math.floor(abs);
|
||||||
|
|
||||||
|
// Minuten
|
||||||
|
const minutesFloat = (abs - degrees) * 60;
|
||||||
|
const minutes = minutesFloat.toFixed(4);
|
||||||
|
|
||||||
|
|
||||||
|
return `${degrees}° ${minutes}' ${direction}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.supplier-address {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6b7280;
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-name {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 1 10rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-address {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption-column {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: end;
|
||||||
|
color: #001D33
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
align-self: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: repeat(3, fit-content(0)) auto;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
grid-column: 1/-1;
|
||||||
|
justify-self: end;
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
DestinationEdit
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DestinationEdit",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
DestinationItem
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DestinationItem",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
122
src/frontend/src/components/layout/node/CreateNewNode.vue
Normal file
122
src/frontend/src/components/layout/node/CreateNewNode.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<div class="create-new-node-container">
|
||||||
|
<h3 class="sub-header">Create new supplier</h3>
|
||||||
|
<form @submit.prevent="send">
|
||||||
|
<div class="create-new-node-form-container">
|
||||||
|
<div class="input-field-caption">Name:</div>
|
||||||
|
<div><div class="input-field-container"><input class="input-field" v-model="nodeName"/></div></div>
|
||||||
|
<div></div>
|
||||||
|
<div class="input-field-caption">Address:</div>
|
||||||
|
<div><div class="input-field-container"><input class="input-field" v-model="nodeAddress"/></div></div>
|
||||||
|
<div>
|
||||||
|
<basic-button icon="SealCheck">Verify address</basic-button>
|
||||||
|
</div>
|
||||||
|
<div class="input-field-caption">Coordinates:</div>
|
||||||
|
<div>{{ nodeCoordinates }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="create-new-node-footer">
|
||||||
|
<basic-button variant="primary" :show-icon="false" :disabled="unverified">OK</basic-button>
|
||||||
|
<basic-button @click.prevent="cancel" variant="secondary" :show-icon="false">Cancel</basic-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CreateNewNode",
|
||||||
|
components: {BasicButton},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
nodeName: "",
|
||||||
|
nodeAddress: "",
|
||||||
|
nodeCoordinates: "",
|
||||||
|
addressVerified: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
unverified() {
|
||||||
|
return !this.addressVerified;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
send() {
|
||||||
|
console.log("Sending...");
|
||||||
|
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.$emit("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.sub-header {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6B869C;
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-caption{
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 500;
|
||||||
|
align-self: center;
|
||||||
|
justify-self: end;
|
||||||
|
color: #001D33
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field-container:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.create-new-node-container {
|
||||||
|
width: 60rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-new-node-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.6rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-new-node-form-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 1.6rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,12 +1,33 @@
|
||||||
|
|
||||||
import router from './router.js';
|
import router from './router.js';
|
||||||
//import store from './store/index.js';
|
//import store from './store/index.js';
|
||||||
import { createApp } from 'vue'
|
import {createApp} from 'vue'
|
||||||
import { createPinia } from 'pinia';
|
import {createPinia} from 'pinia';
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
|
|
||||||
import {PhX, PhTrain, PhTruckTrailer, PhBoat, PhPencilSimple, PhLock,PhLockOpen,PhWarning ,PhMagnifyingGlass, PhPlus, PhUpload, PhDownload, PhTrash, PhArchive, PhFloppyDisk, PhArrowBendUpLeft, PhCheck} from "@phosphor-icons/vue";
|
import {
|
||||||
|
PhStar,
|
||||||
|
PhCalculator,
|
||||||
|
PhSealCheck,
|
||||||
|
PhCloudArrowUp,
|
||||||
|
PhX,
|
||||||
|
PhTrain,
|
||||||
|
PhTruckTrailer,
|
||||||
|
PhBoat,
|
||||||
|
PhPencilSimple,
|
||||||
|
PhLock,
|
||||||
|
PhLockOpen,
|
||||||
|
PhWarning,
|
||||||
|
PhMagnifyingGlass,
|
||||||
|
PhPlus,
|
||||||
|
PhUpload,
|
||||||
|
PhDownload,
|
||||||
|
PhTrash,
|
||||||
|
PhArchive,
|
||||||
|
PhFloppyDisk,
|
||||||
|
PhArrowCounterClockwise,
|
||||||
|
PhCheck
|
||||||
|
} from "@phosphor-icons/vue";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
|
@ -21,7 +42,7 @@ app.component('PhMagnifyingGlass', PhMagnifyingGlass);
|
||||||
app.component('PhTrash', PhTrash);
|
app.component('PhTrash', PhTrash);
|
||||||
app.component('PhArchive', PhArchive);
|
app.component('PhArchive', PhArchive);
|
||||||
app.component('PhFloppyDisk', PhFloppyDisk);
|
app.component('PhFloppyDisk', PhFloppyDisk);
|
||||||
app.component('PhArrowBendUpLeft', PhArrowBendUpLeft);
|
app.component('PhArrowCounterClockwise', PhArrowCounterClockwise );
|
||||||
app.component('PhCheck', PhCheck);
|
app.component('PhCheck', PhCheck);
|
||||||
app.component('PhWarning', PhWarning);
|
app.component('PhWarning', PhWarning);
|
||||||
app.component('PhLock', PhLock);
|
app.component('PhLock', PhLock);
|
||||||
|
|
@ -31,7 +52,10 @@ app.component('PhBoat', PhBoat);
|
||||||
app.component('PhTrain', PhTrain);
|
app.component('PhTrain', PhTrain);
|
||||||
app.component('PhPencilSimple', PhPencilSimple);
|
app.component('PhPencilSimple', PhPencilSimple);
|
||||||
app.component('PhX', PhX);
|
app.component('PhX', PhX);
|
||||||
|
app.component('PhCloudArrowUp', PhCloudArrowUp);
|
||||||
|
app.component('PhSealCheck', PhSealCheck);
|
||||||
|
app.component('PhCalculator', PhCalculator);
|
||||||
|
app.component('PhStar', PhStar);
|
||||||
|
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,240 @@
|
||||||
<template>
|
<template>
|
||||||
<h2 class="page-header">Start Calculation</h2>
|
<div class="start-calculation-container">
|
||||||
|
<h2 class="page-header">Create Calculation</h2>
|
||||||
|
<div class="part-numbers-headers">
|
||||||
|
<h3 class="sub-header">Part Numbers</h3>
|
||||||
|
<basic-button icon="CloudArrowUp" :showIcon="true" @click="activateModal('partNumber')">Drop part numbers</basic-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="item-list">
|
||||||
|
<li class="item-list-element" v-for="material in assistantStore.materials" :key="material.id">
|
||||||
|
<material-item :part-number="material['part_number']" :name="material.name" :id="material.id" @delete="deleteMaterial"></material-item>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<modal :state="showPartNumberModal" @close="closeModal('partNumber')">
|
||||||
|
<div class="part-number-modal-container">
|
||||||
|
<h3 class="sub-header">Drop part numbers here</h3>
|
||||||
|
<div class="part-number-drop-container">
|
||||||
|
<textarea v-model="partNumberField" name="partNumbers" cols="140" rows="15"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="part-number-modal-action">
|
||||||
|
<basic-button @click="parsePartNumbers" icon="CloudArrowUp">Analyze input</basic-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
|
||||||
|
<div class="supplier-headers">
|
||||||
|
<h3 class="sub-header">Suppliers</h3>
|
||||||
|
<div class="supplier-headers-searchbar-container">
|
||||||
|
<autosuggest-searchbar @selected="selectedSupplier" placeholder="Search and add supplier ..." no-results-text='No supplier found for "{query}".' :fetch-suggestions="fetch"
|
||||||
|
variant="flags" :reset-on-select="true"
|
||||||
|
:flag-resolver="resolveFlag" title-resolver="name"
|
||||||
|
subtitle-resolver="address"></autosuggest-searchbar>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<basic-button icon="plus" @click="activateModal('newSupplier')">Create a new supplier</basic-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="item-list">
|
||||||
|
<li class="item-list-element" v-for="supplier in assistantStore.suppliers" :key="supplier.id">
|
||||||
|
<supplier-item :id="supplier.id" :iso-code="supplier.iso" :address="supplier.address"
|
||||||
|
:name="supplier.name" @delete="deleteSupplier" :is-user-supplier="supplier.isUserSupplier"></supplier-item>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<modal :close-on-backdrop="false" :state="newSupplierModalState" @close="closeModal('newSupplier')">
|
||||||
|
<create-new-node @close="closeModal('newSupplier')"></create-new-node>
|
||||||
|
</modal>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="start-calculation-footer-container">
|
||||||
|
<div class="start-calculation-footer-left">
|
||||||
|
This will create {{assistantStore.count}} calculations.
|
||||||
|
</div>
|
||||||
|
<div class="start-calculation-footer-right">
|
||||||
|
<checkbox :checked="!this.assistantStore.createEmpty" @checkbox-changed="setUseExisting">Import data from existing calculations</checkbox>
|
||||||
|
<basic-button :disabled="assistantStore.count==0" @click="createPremises" variant="secondary" :show-icon="false">Create</basic-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {useNodeStore} from "@/store/node.js";
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import MaterialItem from "@/components/layout/assistant/MaterialItem.vue";
|
||||||
|
import SupplierItem from "@/components/layout/assistant/SupplierItem.vue";
|
||||||
|
import Modal from "@/components/UI/Modal.vue";
|
||||||
|
import {useAssistantStore} from "@/store/assistant.js";
|
||||||
|
import CreateNewNode from "@/components/layout/node/CreateNewNode.vue";
|
||||||
|
import Checkbox from "@/components/UI/Checkbox.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CalculationAssistant",
|
name: "CalculationAssistant",
|
||||||
|
components: {Checkbox, CreateNewNode, Modal, SupplierItem, MaterialItem, BasicButton, AutosuggestSearchbar},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useNodeStore, useAssistantStore),
|
||||||
|
showPartNumberModal() {
|
||||||
|
return this.assistantStore.materials.length === 0 || this.partNumberModalState;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newSupplierModalState: false,
|
||||||
|
partNumberModalState: false,
|
||||||
|
partNumberField: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setUseExisting(useExisting) {
|
||||||
|
this.assistantStore.setCreateEmpty(!useExisting);
|
||||||
|
},
|
||||||
|
async createPremises() {
|
||||||
|
await this.assistantStore.createPremises();
|
||||||
|
},
|
||||||
|
selectedSupplier(supplier) {
|
||||||
|
this.assistantStore.addSupplier(supplier);
|
||||||
|
},
|
||||||
|
deleteMaterial(id) {
|
||||||
|
this.assistantStore.deleteMaterial(id);
|
||||||
|
},
|
||||||
|
deleteSupplier(id) {
|
||||||
|
this.assistantStore.deleteSupplier(id);
|
||||||
|
},
|
||||||
|
async fetch(query) {
|
||||||
|
const supplierQuery = {searchTerm: query, includeUserNode: true, nodeType: "SOURCE"};
|
||||||
|
await this.nodeStore.setQuery(supplierQuery);
|
||||||
|
return this.nodeStore.nodes;
|
||||||
|
},
|
||||||
|
resolveFlag(node) {
|
||||||
|
return node.country.iso_code;
|
||||||
|
},
|
||||||
|
activateModal(modalType) {
|
||||||
|
if (modalType === "partNumber")
|
||||||
|
this.partNumberModalState = true;
|
||||||
|
else if (modalType === "newSupplier")
|
||||||
|
this.newSupplierModalState = true;
|
||||||
|
|
||||||
|
},
|
||||||
|
closeModal(modalType) {
|
||||||
|
if (modalType === "partNumber")
|
||||||
|
this.partNumberModalState = false;
|
||||||
|
else if (modalType === "newSupplier")
|
||||||
|
this.newSupplierModalState = false;
|
||||||
|
},
|
||||||
|
parsePartNumbers() {
|
||||||
|
this.closeModal('partNumber');
|
||||||
|
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
|
||||||
|
this.partNumberField = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.nodeStore.loadNodes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.start-calculation-footer-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-calculation-footer-left {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6B869C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-calculation-footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.part-number-drop-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
/* box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);*/
|
||||||
|
border: 0.2rem solid #E3EDFF;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-number-drop-container:hover {
|
||||||
|
background: #EEF4FF;
|
||||||
|
border: 0.2rem solid #8DB3FE;
|
||||||
|
/*transform: translateY(2px);*/
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #002F54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-number-modal-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-number-modal-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-numbers-headers {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
gap: 2.4rem 2.4rem;
|
||||||
|
margin: 4.8rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list-element {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplier-headers {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supplier-headers-searchbar-container {
|
||||||
|
flex: 0 0 60rem;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,14 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<div class="edit-calculation-container">
|
||||||
|
<div class="header-container">
|
||||||
|
<h2 class="page-header">Edit Calculation</h2>
|
||||||
|
<div class="header-controls">
|
||||||
|
<basic-button :show-icon="false" variant="secondary">Close</basic-button>
|
||||||
|
<basic-button :show-icon="true" icon="Calculator" variant="primary">Close & calculate</basic-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="sub-header">Master data</h3>
|
||||||
|
<div class="master-data-container">
|
||||||
|
<box class="master-data-item master-data-stretched-item">
|
||||||
|
<supplier-view supplier-address="1258 Gonghexin Road 闸北区, 上海市 200070 People's Republic of China" supplier-name="Linde (China) Forklift Truck (Supplier)"></supplier-view>
|
||||||
|
</box>
|
||||||
|
<box class="master-data-item master-data-stretched-item master-data-packaging">
|
||||||
|
<packaging-edit></packaging-edit>
|
||||||
|
</box>
|
||||||
|
<box class="master-data-item">
|
||||||
|
<material-edit></material-edit>
|
||||||
|
</box>
|
||||||
|
<box class="master-data-item">
|
||||||
|
<price-edit></price-edit>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="sub-header">Destinations & routes</h3>
|
||||||
|
<div class="destination-list-container">
|
||||||
|
<box>
|
||||||
|
<destination-list-view></destination-list-view>
|
||||||
|
</box>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="edit-calculation-material-edit"></div>
|
||||||
|
<div class="edit-calculation-packaging-edit"></div>
|
||||||
|
<div class="edit-calculation-price-edit"></div>
|
||||||
|
<div class="edit-calculation-destination-list"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import SupplierView from "@/components/layout/edit/SupplierView.vue";
|
||||||
|
import Box from "@/components/UI/Box.vue";
|
||||||
|
import MaterialEdit from "@/components/layout/edit/MaterialEdit.vue";
|
||||||
|
import PackagingEdit from "@/components/layout/edit/PackagingEdit.vue";
|
||||||
|
import PriceEdit from "@/components/layout/edit/PriceEdit.vue";
|
||||||
|
import DestinationListView from "@/components/layout/edit/DestinationListView.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "SingleEdit"
|
name: "SingleEdit",
|
||||||
|
components: {DestinationListView, PriceEdit, PackagingEdit, MaterialEdit, Box, SupplierView, BasicButton}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<h2>Edit Calculation</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.edit-calculation-container {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.master-data-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
gap: 1.6rem;
|
||||||
|
margin: 2.4rem 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.master-data-stretched-item {
|
||||||
|
grid-row: span 2 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.master-data-packaging {
|
||||||
|
grid-column-start: 3;
|
||||||
|
grid-row-start: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.master-data-item {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination-list-container {
|
||||||
|
margin: 2.4rem 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<h2 class="page-header">Calculation</h2>
|
<h2 class="page-header">My calculations</h2>
|
||||||
|
|
||||||
<notification-bar v-if="premiseStore.error != null" variant="exception" icon="x"
|
<notification-bar v-if="premiseStore.error != null" variant="exception" icon="x"
|
||||||
@icon-clicked="premiseStore.error = null">
|
@icon-clicked="premiseStore.error = null">
|
||||||
|
|
|
||||||
178
src/frontend/src/store/assistant.js
Normal file
178
src/frontend/src/store/assistant.js
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
|
||||||
|
|
||||||
|
export const useAssistantStore = defineStore('assistant', {
|
||||||
|
state: () => ({
|
||||||
|
materials: [],
|
||||||
|
suppliers: [],
|
||||||
|
createEmpty: false,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
pagination: {},
|
||||||
|
query: {},
|
||||||
|
premises: null
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
count: state => state.materials.length * state.suppliers.length,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setCreateEmpty(createEmpty) {
|
||||||
|
this.createEmpty = createEmpty;
|
||||||
|
},
|
||||||
|
async createPremises() {
|
||||||
|
|
||||||
|
const url = `${config.backendUrl}/calculation/create/`;
|
||||||
|
|
||||||
|
const materialIds = this.materials.map((material) => material.id);
|
||||||
|
const supplierIds = this.suppliers.filter(s => s.id.startsWith('s')).map((supplier) => supplier.origId);
|
||||||
|
const userSupplierIds = this.suppliers.filter(s => s.id.startsWith('u')).map((supplier) => supplier.origId);
|
||||||
|
const jsonBody = JSON.stringify({ material: materialIds, supplier: supplierIds, user_supplier: userSupplierIds, createEmpty: this.createEmpty });
|
||||||
|
|
||||||
|
console.log(`Creation body: ${jsonBody}`);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: jsonBody
|
||||||
|
}).catch(e => {
|
||||||
|
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||||
|
console.error(this.error);
|
||||||
|
this.loading = false;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
this.error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
console.error(this.error);
|
||||||
|
this.loading = false;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||||
|
this.loading = false;
|
||||||
|
console.error(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.premises = data;
|
||||||
|
console.log(this.premises);
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
addSupplier(supplier) {
|
||||||
|
const uid = `${supplier.is_user_node ? 'u' : 's'}${supplier.id}`
|
||||||
|
|
||||||
|
if (!this.suppliers.some(s => s.id === uid)) {
|
||||||
|
this.suppliers.push({
|
||||||
|
id: uid,
|
||||||
|
origId: supplier.id,
|
||||||
|
isUserSupplier: false,
|
||||||
|
name: supplier.name,
|
||||||
|
address: supplier.address,
|
||||||
|
iso: supplier.country['iso_code']
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteMaterial(id) {
|
||||||
|
const index = this.materials.findIndex(m => m.id === id);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
this.materials.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteSupplier(id) {
|
||||||
|
const index = this.suppliers.findIndex(s => s.id === id);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
this.suppliers.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getMaterialsAndSuppliers(query) {
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.empty = true
|
||||||
|
this.materials = [];
|
||||||
|
this.suppliers = [];
|
||||||
|
|
||||||
|
|
||||||
|
const url = `${config.backendUrl}/calculation/search/`;
|
||||||
|
|
||||||
|
console.log(`${url} with query ${query}`);
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
headers.append('search', encodeURIComponent(query));
|
||||||
|
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: headers
|
||||||
|
}).catch(e => {
|
||||||
|
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||||
|
this.loading = false;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
this.error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||||
|
this.loading = false;
|
||||||
|
console.log(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const newSuppliers = [];
|
||||||
|
|
||||||
|
for (const supplier of data.supplier) {
|
||||||
|
const newSupplier = {
|
||||||
|
id: `s${supplier.id}`,
|
||||||
|
origId: supplier.id,
|
||||||
|
isUserSupplier: false,
|
||||||
|
name: supplier.name,
|
||||||
|
address: supplier.address,
|
||||||
|
iso: supplier.country['iso_code']
|
||||||
|
};
|
||||||
|
newSuppliers.push(newSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (const supplier of data.user_supplier) {
|
||||||
|
const newSupplier = {
|
||||||
|
id: `u${supplier.id}`,
|
||||||
|
origId: supplier.id,
|
||||||
|
isUserSupplier: true,
|
||||||
|
name: supplier.name,
|
||||||
|
address: supplier.address,
|
||||||
|
iso: supplier.country['iso_code']
|
||||||
|
};
|
||||||
|
newSuppliers.push(newSupplier);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
this.materials = data.materials;
|
||||||
|
this.suppliers = newSuppliers;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
68
src/frontend/src/store/node.js
Normal file
68
src/frontend/src/store/node.js
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
|
||||||
|
|
||||||
|
export const useNodeStore = defineStore('node', {
|
||||||
|
state: () => ({
|
||||||
|
nodes: [],
|
||||||
|
loading: false,
|
||||||
|
empty: true,
|
||||||
|
error: null,
|
||||||
|
pagination: {},
|
||||||
|
query: {},
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getById: (state) => {
|
||||||
|
return (id) => state.nodes.find(p => p.id === id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async setQuery(query) {
|
||||||
|
this.query = query;
|
||||||
|
await this.loadNodes();
|
||||||
|
},
|
||||||
|
async loadNodes() {
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.query.searchTerm)
|
||||||
|
params.append('filter', this.query.searchTerm);
|
||||||
|
|
||||||
|
if (this.query.nodeType)
|
||||||
|
params.append('node_type', this.query.nodeType);
|
||||||
|
|
||||||
|
if (this.query.includeUserNode)
|
||||||
|
params.append('include_user_node', this.query.includeUserNode);
|
||||||
|
|
||||||
|
const url = `${config.backendUrl}/nodes/search/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||||
|
console.log(url)
|
||||||
|
const response = await fetch(url).catch(e => {
|
||||||
|
this.error = {code: 'Network error.', message: "Please check your internet connection.", trace: null}
|
||||||
|
this.loading = false;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
this.error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.error = {code: data.error.title, message: data.error.message, trace: data.error.details}
|
||||||
|
this.loading = false;
|
||||||
|
console.log(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
this.empty = data.length === 0;
|
||||||
|
this.nodes = data;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue