Merge branch 'dev' into 6553-workerBusiness
gitea/salix-front/pipeline/pr-dev There was a failure building this commit Details

This commit is contained in:
Carlos Satorres 2025-01-30 07:49:58 +00:00
commit 962ae4ca96
40 changed files with 1734 additions and 166 deletions

4
.gitignore vendored
View File

@ -31,3 +31,7 @@ yarn-error.log*
# Cypress directories and files
/test/cypress/videos
/test/cypress/screenshots
# VitePress directories and files
/docs/.vitepress/cache
/docs/.vuepress

View File

@ -30,9 +30,11 @@ export default defineConfig({
testFiles: '**/*.spec.js',
supportFile: 'test/cypress/support/unit.js',
},
setupNodeEvents(on, config) {
import('cypress-mochawesome-reporter/plugin').then((plugin) => plugin.default(on));
// implement node event listeners here
setupNodeEvents: async (on, config) => {
const plugin = await import('cypress-mochawesome-reporter/plugin');
plugin.default(on);
return config;
},
viewportWidth: 1280,
viewportHeight: 720,

38
docs/.vitepress/config.js Normal file
View File

@ -0,0 +1,38 @@
import { defineConfig } from 'vitepress';
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: 'Lilium',
description: 'Lilium docs',
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Home', link: '/' },
{ text: 'Components', link: '/components/vnInput' },
{ text: 'Composables', link: '/composables/useArrayData' },
],
sidebar: [
{
items: [
{
text: 'Components',
collapsible: true,
collapsed: true,
items: [{ text: 'VnInput', link: '/components/vnInput' }],
},
{
text: 'Composables',
collapsible: true,
collapsed: true,
items: [
{ text: 'useArrayData', link: '/composables/useArrayData' },
],
},
],
},
],
socialLinks: [{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }],
},
});

136
docs/components/vnInput.md Normal file
View File

@ -0,0 +1,136 @@
# VnInput
`VnInput` is a custom input component that provides various useful features such as validation, input clearing, and more.
## Props
### `modelValue`
- **Type:** `String | Number`
- **Default:** `null`
- **Description:** The value of the model bound to the component.
### `isOutlined`
- **Type:** `Boolean`
- **Default:** `false`
- **Description:** If `true`, the component is rendered with an outlined style.
### `info`
- **Type:** `String`
- **Default:** `''`
- **Description:** Additional information displayed alongside the component.
### `clearable`
- **Type:** `Boolean`
- **Default:** `true`
- **Description:** If `true`, the component shows a button to clear the input.
### `emptyToNull`
- **Type:** `Boolean`
- **Default:** `true`
- **Description:** If `true`, converts empty inputs to `null`.
### `insertable`
- **Type:** `Boolean`
- **Default:** `false`
- **Description:** If `true`, allows the insertion of new values.
### `maxlength`
- **Type:** `Number`
- **Default:** `null`
- **Description:** The maximum number of characters allowed in the input.
### `uppercase`
- **Type:** `Boolean`
- **Default:** `false`
- **Description:** If `true`, converts the input text to uppercase.
## Emits
### `update:modelValue`
- **Description:** Emits the updated model value.
- **Behavior:** This event is emitted whenever the input value changes. It is used to update the model value bound to the component.
### `update:options`
- **Description:** Emits the updated options.
- **Behavior:** This event is emitted when the component's options change. It is useful for components with dynamic options.
### `keyup.enter`
- **Description:** Emits an event when the Enter key is pressed.
- **Behavior:** This event is emitted whenever the Enter key is pressed while the input is focused. It can be used to handle specific actions when the input is confirmed.
### `remove`
- **Description:** Emits an event to remove the current value.
- **Behavior:** This event is emitted when the clear button (close icon) is clicked. It is used to handle the removal of the current input value.
## Functions
### `focus`
- **Description:** Focuses the input.
- **Behavior:** This function is exposed so it can be called from outside the component. It uses `vnInputRef.value.focus()` to focus the input.
### `handleKeydown`
- **Description:** Handles the `keydown` event of the input.
- **Behavior:** This function is called whenever a key is pressed while the input is focused. If the pressed key is `Backspace`, it does nothing. If `insertable` is `true` and the pressed key is a number, it calls `handleInsertMode`.
### `handleInsertMode`
- **Description:** Handles the insertion mode of values.
- **Behavior:** This function is called when `insertable` is `true` and a numeric key is pressed. It inserts the value at the cursor position and updates the input value. Then, it moves the cursor to the correct position.
### `handleUppercase`
- **Description:** Converts the input value to uppercase.
- **Behavior:** This function is called when the uppercase icon is clicked. It converts the current input value to uppercase.
## Usage
```vue
<template>
<VnInput
v-model="inputValue"
:isOutlined="true"
info="Additional information"
:clearable="true"
:emptyToNull="true"
:insertable="false"
:maxlength="50"
:uppercase="true"
@update:modelValue="handleUpdate"
@keyup.enter="handleEnter"
@remove="handleRemove"
/>
</template>
<script setup>
import { ref } from 'vue';
import VnInput from 'src/components/common/VnInput.vue';
const inputValue = ref('');
const handleUpdate = (value) => {
console.log('Updated value:', value);
};
const handleEnter = () => {
console.log('Enter pressed');
};
const handleRemove = () => {
console.log('Value removed');
};
</script>
```

View File

@ -0,0 +1,215 @@
# useArrayData
`useArrayData` is a composable function that provides a set of utilities for managing array data in a Vue component. It leverages Pinia for state management and provides various methods for fetching, filtering, and manipulating data.
## Usage
```javascript
import { useArrayData } from 'src/composables/useArrayData';
const {
fetch,
applyFilter,
addFilter,
getCurrentFilter,
setCurrentFilter,
addFilterWhere,
addOrder,
deleteOrder,
refresh,
destroy,
loadMore,
store,
totalRows,
updateStateParams,
isLoading,
deleteOption,
reset,
resetPagination,
} = useArrayData('myKey', userOptions);
```
## Parameters
### `key`
- **Type:** `String`
- **Description:** A unique key to identify the data store.
### `userOptions`
- **Type:** `Object`
- **Description:** An object containing user-defined options for configuring the data store.
## Methods
### `fetch`
Fetches data from the server.
#### Parameters
- **`options`** : An object with the following properties:
- `append` (Boolean): Whether to append the fetched data to the existing data.
- `updateRouter` (Boolean): Whether to update the router with the current filter.
#### Returns
- **`Promise`** : A promise that resolves with the fetched data.
### `applyFilter`
Applies a filter to the data.
#### Parameters
- **`filter`** : An object containing the filter criteria.
- **`params`** : Additional parameters for the filter.
- **`fetchOptions`** : Options for the fetch method.
#### Returns
- **`Promise`** : A promise that resolves with the filtered data.
### `addFilter`
Adds a filter to the existing filters.
#### Parameters
- **`filter`** : An object containing the filter criteria.
- **`params`** : Additional parameters for the filter.
#### Returns
- **`Promise`** : A promise that resolves with the updated filter and parameters.
### `getCurrentFilter`
Gets the current filter applied to the data.
#### Returns
- **`Object`** : The current filter and parameters.
### `setCurrentFilter`
Sets the current filter for the data.
#### Returns
- **`Object`** : The current filter and parameters.
### `addFilterWhere`
Adds a `where` clause to the existing filters.
#### Parameters
- **`where`** : An object containing the `where` clause.
#### Returns
- **`Promise`** : A promise that resolves when the filter is applied.
### `addOrder`
Adds an order to the existing orders.
#### Parameters
- **`field`** : The field to order by.
- **`direction`** : The direction of the order (`ASC` or `DESC`).
#### Returns
- **`Promise`** : A promise that resolves with the updated order.
### `deleteOrder`
Deletes an order from the existing orders.
#### Parameters
- **`field`** : The field to delete the order for.
#### Returns
- **`Promise`** : A promise that resolves when the order is deleted.
### `refresh`
Refreshes the data by re-fetching it from the server.
#### Returns
- **`Promise`** : A promise that resolves with the refreshed data.
### `destroy`
Destroys the data store for the given key.
### `loadMore`
Loads more data by incrementing the pagination.
#### Returns
- **`Promise`** : A promise that resolves with the additional data.
### `updateStateParams`
Updates the state parameters with the given data.
#### Parameters
- **`data`** : The data to update the state parameters with.
### `deleteOption`
Deletes an option from the store.
#### Parameters
- **`option`** : The option to delete.
### `reset`
Resets the store to its default state.
#### Parameters
- **`opts`** : An array of options to reset.
### `resetPagination`
Resets the pagination for the store.
## Computed Properties
### `totalRows`
- **Description:** The total number of rows in the data.
- **Type:** `Number`
### `isLoading`
- **Description:** Whether the data is currently being loaded.
- **Type:** `Boolean`
```vue
<script setup>
import { useArrayData } from 'src/composables/useArrayData';
const userOptions = {
url: '/api/data',
limit: 10,
};
const arrayData = useArrayData('myKey', userOptions);
</script>
```
```
```

13
docs/index.md Normal file
View File

@ -0,0 +1,13 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: 'Lilium'
text: 'Lilium docs'
tagline: Powered by Verdnatura
actions:
- theme: brand
text: Docs
link: /components/vnInput
---

View File

@ -1,70 +1,74 @@
{
"name": "salix-front",
"version": "25.06.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
"private": true,
"packageManager": "pnpm@8.15.1",
"type": "module",
"scripts": {
"resetDatabase": "cd ../salix && gulp docker",
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest",
"test:unit:ci": "vitest run",
"commitlint": "commitlint --edit",
"prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js"
},
"dependencies": {
"@quasar/cli": "^2.4.1",
"@quasar/extras": "^1.16.16",
"axios": "^1.4.0",
"chromium": "^3.0.3",
"croppie": "^2.6.5",
"moment": "^2.30.1",
"pinia": "^2.1.3",
"quasar": "^2.17.7",
"validator": "^13.9.0",
"vue": "^3.5.13",
"vue-i18n": "^9.3.0",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^19.1.0",
"@intlify/unplugin-vue-i18n": "^0.8.2",
"@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^2.0.8",
"@quasar/quasar-app-extension-qcalendar": "^4.0.2",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14",
"cypress": "^13.6.6",
"cypress-mochawesome-reporter": "^3.8.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-vue": "^9.32.0",
"husky": "^8.0.0",
"postcss": "^8.4.23",
"prettier": "^3.4.2",
"sass": "^1.83.4",
"vitest": "^0.34.0"
},
"engines": {
"node": "^20 || ^18 || ^16",
"npm": ">= 8.1.2",
"yarn": ">= 1.21.1",
"bun": ">= 1.0.25"
},
"overrides": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.11",
"vitest": "^0.31.1"
}
}
"name": "salix-front",
"version": "25.06.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",
"private": true,
"packageManager": "pnpm@8.15.1",
"type": "module",
"scripts": {
"resetDatabase": "cd ../salix && gulp docker",
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open",
"test:e2e:ci": "npm run resetDatabase && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest",
"test:unit:ci": "vitest run",
"commitlint": "commitlint --edit",
"prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@quasar/cli": "^2.4.1",
"@quasar/extras": "^1.16.16",
"axios": "^1.4.0",
"chromium": "^3.0.3",
"croppie": "^2.6.5",
"moment": "^2.30.1",
"pinia": "^2.1.3",
"quasar": "^2.17.7",
"validator": "^13.9.0",
"vue": "^3.5.13",
"vue-i18n": "^9.3.0",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^19.1.0",
"@intlify/unplugin-vue-i18n": "^0.8.2",
"@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^2.0.8",
"@quasar/quasar-app-extension-qcalendar": "^4.0.2",
"@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0",
"@vue/test-utils": "^2.4.4",
"autoprefixer": "^10.4.14",
"cypress": "^13.6.6",
"cypress-mochawesome-reporter": "^3.8.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-vue": "^9.32.0",
"husky": "^8.0.0",
"postcss": "^8.4.23",
"prettier": "^3.4.2",
"sass": "^1.83.4",
"vitepress": "^1.6.3",
"vitest": "^0.34.0"
},
"engines": {
"node": "^20 || ^18 || ^16",
"npm": ">= 8.1.2",
"yarn": ">= 1.21.1",
"bun": ">= 1.0.25"
},
"overrides": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.11",
"vitest": "^0.31.1"
}
}

File diff suppressed because it is too large Load Diff

6
proxy-serve.js Normal file
View File

@ -0,0 +1,6 @@
export default [
{
path: '/api',
rule: { target: 'http://0.0.0.0:3000' },
},
];

View File

@ -179,7 +179,6 @@ export default configure(function (/* ctx */) {
'render', // keep this as last one
],
},
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
pwa: {
workboxMode: 'generateSW', // or 'injectManifest'

View File

@ -349,4 +349,11 @@ es:
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
Plant: Planta natural
Flower: Flor fresca
Handmade: Hecho a mano
Artificial: Artificial
Green: Verdes frescos
Accessories: Complementos florales
Fruit: Fruta
</i18n>

View File

@ -20,6 +20,7 @@ const appName = 'Lilium';
const pinnedModulesRef = ref();
onMounted(() => stateStore.setMounted());
const refresh = () => window.location.reload();
</script>
<template>
<QHeader color="white" elevated>
@ -64,6 +65,13 @@ onMounted(() => stateStore.setMounted());
<QSpace />
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div id="actions-prepend"></div>
<QIcon
name="refresh"
size="md"
color="red"
v-if="state.get('error')"
@click="refresh"
/>
<QBtn
:class="{ 'q-pa-none': quasar.platform.is.mobile }"
id="pinnedModules"

View File

@ -0,0 +1,12 @@
export default function (initialFooter, data) {
const footer = data.reduce(
(acc, row) => {
Object.entries(initialFooter).forEach(([key, initialValue]) => {
acc[key] += row?.[key] !== undefined ? row[key] : initialValue;
});
return acc;
},
{ ...initialFooter }
);
return footer;
}

View File

@ -26,6 +26,7 @@ onMounted(() => {
round
dense
icon="dock_to_left"
data-cy="toggle-right-drawer"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}

View File

@ -175,7 +175,11 @@ const handleUppercase = () => {
v-if="!$attrs.disabled && !($attrs.readonly) && $props.uppercase"
@click="handleUppercase"
class="uppercase-icon"
/>
>
<QTooltip>
{{ t('Convert to uppercase') }}
</QTooltip>
</QIcon>
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon v-if="info" name="info">
@ -188,13 +192,26 @@ const handleUppercase = () => {
</div>
</template>
<style>
.uppercase-icon {
transition: color 0.3s, transform 0.2s;
cursor: pointer;
}
.uppercase-icon:hover {
color: #ed9937;
transform: scale(1.2);
}
</style>
<i18n>
en:
inputMin: Must be more than {value}
maxLength: The value exceeds {value} characters
inputMax: Must be less than {value}
es:
inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value}
Convert to uppercase: Convertir a mayúsculas
</i18n>

View File

@ -98,8 +98,8 @@ function checkIsMain() {
/>
<div :id="searchbarId"></div>
</slot>
<RightAdvancedMenu :is-main-section="isMainSection && rightFilter">
<template #advanced-menu v-if="$slots[advancedMenuSlot]">
<RightAdvancedMenu :is-main-section="isMainSection">
<template #advanced-menu v-if="$slots[advancedMenuSlot] || rightFilter">
<slot :name="advancedMenuSlot">
<VnTableFilter
v-if="rightFilter && columns"

View File

@ -170,7 +170,7 @@ function emitStoreData() {
async function paginate() {
const { page, rowsPerPage, sortBy, descending } = pagination.value;
if (!props.url) return;
if (!arrayData.store.url) return;
isLoading.value = true;
await arrayData.loadMore();

View File

@ -10,4 +10,4 @@ import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue
base-url="Departments"
:descriptor="DepartmentDescriptor"
/>
</template>
</template>

View File

@ -44,7 +44,7 @@ const transferEntry = async () => {
<template>
<QItem v-ripple clickable @click="showEntryReport">
<QItemSection>{{ $t('entryList.list.showEntryReport') }}</QItemSection>
<QItemSection>{{ $t('entry.descriptorMenu.showEntryReport') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="openDialog">
<QItemSection>{{ t('transferEntry') }}</QItemSection>

View File

@ -76,6 +76,8 @@ entry:
warehouseInFk: Warehouse in
search: Search entries
searchInfo: You can search by entry reference
descriptorMenu:
showEntryReport: Show entry report
entryFilter:
params:
invoiceNumber: Invoice number

View File

@ -75,6 +75,8 @@ entry:
warehouseInFk: Alm. entrada
daysOnward: Días adelante
daysAgo: Días atras
descriptorMenu:
showEntryReport: Ver informe del pedido
search: Buscar entradas
searchInfo: Puedes buscar por referencia de entrada
entryFilter:

View File

@ -368,7 +368,7 @@ watchEffect(selectedRows);
<VnSelect
url="InvoiceOutSerials"
v-model="data.serial"
:label="t('invoicein.serial')"
:label="t('invoiceOutModule.serial')"
:options="invoiceOutSerialsOptions"
option-label="description"
option-value="code"

View File

@ -60,6 +60,7 @@ invoiceOutModule:
amount: Amount
company: Company
address: Address
serial: Serial
invoiceOutList:
tableVisibleColumns:
id: ID
@ -70,4 +71,4 @@ invoiceOutList:
invoiceOutSerial: Serial
ticket: Ticket
taxArea: Tax area
customsAgent: Custom Agent
customsAgent: Custom Agent

View File

@ -11,7 +11,7 @@ invoiceOut:
isActive: Activo
hasToInvoice: Debe facturar
hasVerifiedData: Datos verificados
workerName: Comercial
workerName: Comercial
card:
issued: Fecha emisión
customerCard: Ficha del cliente
@ -54,12 +54,13 @@ invoiceOut:
verifiedData: Datos comprobados
comercial: Comercial
errors:
downloadCsvFailed: Error al descargar CSV
downloadCsvFailed: Error al descargar CSV
invoiceOutModule:
customer: Cliente
amount: Importe
company: Empresa
address: Consignatario
serial: Serie
invoiceOutList:
tableVisibleColumns:
id: ID
@ -70,4 +71,4 @@ invoiceOutList:
invoiceOutSerial: Serial
ticket: Ticket
taxArea: Area
customsAgent: Agente de aduanas
customsAgent: Agente de aduanas

View File

@ -48,14 +48,14 @@ const itemPackingTypesOptions = ref([]);
>
<template #form="{ data }">
<VnRow>
<VnInput v-model="data.code" :label="t('shared.code')" />
<VnInput v-model="data.name" :label="t('shared.name')" />
<VnInput v-model="data.code" :label="t('itemType.shared.code')" />
<VnInput v-model="data.name" :label="t('itemType.shared.name')" />
</VnRow>
<VnRow>
<VnSelect
url="Workers/search"
v-model="data.workerFk"
:label="t('shared.worker')"
:label="t('itemType.shared.worker')"
sort-by="nickname ASC"
:fields="['id', 'nickname']"
option-label="nickname"
@ -83,7 +83,7 @@ const itemPackingTypesOptions = ref([]);
>
<VnSelect
v-model="data.categoryFk"
:label="t('shared.category')"
:label="t('itemType.shared.category')"
:options="categoriesOptions"
option-value="id"
option-label="name"
@ -93,27 +93,30 @@ const itemPackingTypesOptions = ref([]);
<VnRow>
<VnSelect
v-model="data.temperatureFk"
:label="t('shared.temperature')"
:label="t('itemType.shared.temperature')"
:options="temperaturesOptions"
option-value="code"
option-label="name"
hide-selected
/>
<VnInput v-model="data.life" :label="t('shared.life')" />
<VnInput v-model="data.life" :label="t('itemType.summary.life')" />
</VnRow>
<VnRow>
<VnSelect
v-model="data.itemPackingTypeFk"
:label="t('shared.itemPackingType')"
:label="t('itemType.shared.itemPackingType')"
:options="itemPackingTypesOptions"
option-value="code"
option-label="description"
hide-selected
/>
<VnInput v-model="data.maxRefs" :label="t('shared.maxRefs')" />
<VnInput v-model="data.maxRefs" :label="t('itemType.shared.maxRefs')" />
</VnRow>
<VnRow>
<QCheckbox v-model="data.isFragile" :label="t('shared.fragile')" />
<QCheckbox
v-model="data.isFragile"
:label="t('itemType.shared.fragile')"
/>
</VnRow>
</template>
</FormModel>

View File

@ -50,15 +50,15 @@ const setData = (entity) => (data.value = useCardDescription(entity.code, entity
@on-fetch="setData"
>
<template #body="{ entity }">
<VnLv :label="t('shared.code')" :value="entity.code" />
<VnLv :label="t('shared.name')" :value="entity.name" />
<VnLv :label="t('shared.worker')">
<VnLv :label="t('itemType.shared.code')" :value="entity.code" />
<VnLv :label="t('itemType.shared.name')" :value="entity.name" />
<VnLv :label="t('itemType.shared.worker')">
<template #value>
<span class="link">{{ entity.worker?.firstName }}</span>
<WorkerDescriptorProxy :id="entity.worker?.id" />
</template>
</VnLv>
<VnLv :label="t('shared.category')" :value="entity.category?.name" />
<VnLv :label="t('itemType.shared.category')" :value="entity.category?.name" />
</template>
</CardDescriptor>
</template>

View File

@ -76,13 +76,6 @@ itemTags:
searchbar:
label: Search item
info: Search by item id
itemType:
shared:
code: Code
name: Name
worker: Worker
category: Category
temperature: Temperature
item:
params:
daysOnward: Days onward

View File

@ -80,6 +80,9 @@ itemType:
worker: Trabajador
category: Reino
temperature: Temperatura
searchbar:
label: Buscar artículo
info: Buscar por id de artículo
params:
state: asfsdf
item:
@ -216,6 +219,6 @@ item:
item: 'Artículo'
achieved: 'Conseguido'
concept: 'Concepto'
state: 'Estado'
state: 'Estado'
search: 'Buscar artículo'
searchInfo: 'Puedes buscar por id'
searchInfo: 'Puedes buscar por id'

View File

@ -1,8 +1,12 @@
<script setup>
import RouteDescriptor from 'pages/Route/Card/RouteDescriptor.vue';
import VnCardBeta from 'src/components/common/VnCardBeta.vue';
</script>
<template>
<VnCardBeta data-key="Route" base-url="Routes" :descriptor="RouteDescriptor" />
<VnCardBeta
data-key="Route"
base-url="Routes"
custom-url="Routes/filter"
:descriptor="RouteDescriptor"
/>
</template>

View File

@ -0,0 +1,191 @@
<script setup>
import { computed, onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import tableFooter from 'src/components/VnTable/filters/tableFooter';
import { dashIfEmpty, toCurrency, toDateHourMin } from 'src/filters';
import { useState } from 'src/composables/useState';
import { useStateStore } from 'src/stores/useStateStore';
import VnTable from 'src/components/VnTable/VnTable.vue';
import InvoiceInDescriptorProxy from 'src/pages/InvoiceIn/Card/InvoiceInDescriptorProxy.vue';
import SupplierBalanceFilter from './SupplierBalanceFilter.vue';
import { onMounted } from 'vue';
import FetchData from 'src/components/FetchData.vue';
const { t } = useI18n();
const route = useRoute();
const state = useState();
const stateStore = useStateStore();
const user = state.getUser();
const tableRef = ref();
const companyId = ref();
const companyUser = ref(user.value.companyFk);
const balances = ref([]);
const userParams = ref({
supplierId: route.params.id,
companyId: companyId.value ?? companyUser.value,
isBooked: false,
});
const columns = computed(() => [
{
align: 'left',
name: 'dated',
label: t('Creation date'),
format: ({ dated }) => toDateHourMin(dated),
cardVisible: true,
},
{
align: 'left',
name: 'sref',
label: t('Reference'),
isTitle: true,
class: 'extend',
format: ({ sref }) => dashIfEmpty(sref),
},
{
align: 'left',
name: 'bank',
label: t('Bank'),
cardVisible: true,
},
{
align: 'left',
name: 'invoiceEuros',
label: t('Debit'),
format: ({ invoiceEuros }) => toCurrency(invoiceEuros),
isId: true,
},
{
align: 'left',
name: 'paymentEuros',
label: t('Havings'),
format: ({ paymentEuros }) => toCurrency(paymentEuros),
cardVisible: true,
},
{
align: 'left',
name: 'euroBalance',
label: t('Balance'),
format: ({ euroBalance }) => toCurrency(euroBalance),
cardVisible: true,
},
{
align: 'left',
name: 'isBooked',
label: t('Conciliated'),
cardVisible: true,
},
]);
onBeforeMount(() => {
stateStore.rightDrawer = true;
companyId.value = companyUser.value;
});
onMounted(async () => {
Object.assign(userParams, {
supplierId: route.params.id,
companyId: companyId.value ?? companyUser.value,
isConciliated: false,
});
});
function setFooter(data) {
const initialFooter = {
invoiceEuros: 0,
paymentEuros: 0,
euroBalance: 0,
};
tableRef.value.footer = tableFooter(initialFooter, data);
}
async function onFetch(data) {
setFooter(data);
return;
}
function round(value) {
return Math.round(value * 100) / 100;
}
const onFetchCurrencies = ([currency]) => {
userParams.value.currencyFk = currency?.id;
};
</script>
<template>
<QDrawer side="right" :width="265" v-model="stateStore.rightDrawer">
<SupplierBalanceFilter data-key="SupplierBalance" />
</QDrawer>
<FetchData
url="Currencies"
:filter="{ fields: ['id', 'code'], where: { code: 'EUR' } }"
sort-by="code"
@on-fetch="onFetchCurrencies"
auto-load
/>
<VnTable
v-if="userParams.currencyFk"
ref="tableRef"
data-key="SupplierBalance"
url="Suppliers/receipts"
search-url="balance"
:user-params="userParams"
:columns="columns"
:right-search="false"
:is-editable="false"
:column-search="false"
@on-fetch="onFetch"
:disable-option="{ card: true }"
:footer="true"
:order="['dated ASC']"
data-cy="supplierBalanceTable"
auto-load
:map-key="false"
>
<template #column-balance="{ rowIndex }">
{{ toCurrency(balances[rowIndex]?.balance) }}
</template>
<template #column-sref="{ row }">
<span class="link" v-if="row.statementType === 'invoiceIn'">
{{ dashIfEmpty(row.sref) }}
<InvoiceInDescriptorProxy :id="row.id" />
</span>
<span v-else> {{ dashIfEmpty(row.sref) }}</span>
</template>
<template #column-footer-invoiceEuros>
<span>
{{ toCurrency(round(tableRef.footer.invoiceEuros)) }}
</span>
</template>
<template #column-footer-paymentEuros>
<span>
{{ toCurrency(round(tableRef.footer.paymentEuros)) }}
</span>
</template>
<template #column-footer-euroBalance>
<span>
{{ toCurrency(round(tableRef.footer.euroBalance)) }}
</span>
</template>
</VnTable>
</template>
<i18n>
es:
Company: Empresa
Total by company: Total por empresa
Date: Fecha
Creation date: Fecha de creación
Reference: Referencia
Bank: Caja
Debit: Debe
Havings: Haber
Balance: Balance
Conciliated: Conciliado
</i18n>

View File

@ -0,0 +1,121 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
const { t } = useI18n();
defineProps({
dataKey: {
type: String,
required: true,
},
});
</script>
<template>
<VnFilterPanel
:data-key="dataKey"
:search-button="true"
:redirect="false"
:unremovable-params="['supplierId', 'companyId']"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem>
<QItemSection>
<VnInputDate
:label="t('params.from')"
v-model="params.from"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
:label="t('params.bankFk')"
v-model="params.bankFk"
url="Accountings"
option-label="bank"
:include="{ relation: 'accountingType' }"
sort-by="id"
dense
outlined
rounded
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.id }} {{ scope.opt.bank }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
:label="t('params.currencyFk')"
url="Currencies"
:filter="{ fields: ['id', 'name'] }"
order="code"
v-model="params.currencyFk"
option-value="id"
option-label="name"
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
v-model="params.isConciliated"
:label="t('params.isConciliated')"
/></QItemSection>
</QItem>
</template>
</VnFilterPanel>
</template>
<i18n>
en:
params:
search: General search
supplierId: Supplier
categoryId: Category
from: From
to: To
isConciliated: Is conciliated
currencyFk: Currency
bankFk: Bank
companyId: Comapany
isBooked: Is booked
es:
params:
supplierId: Proveedor
isConciliated: Conciliado
currencyFk: Moneda
New payment: Añadir pago
Date: Fecha
from: Desde
to: Hasta
companyId: Empresa
isBooked: Contabilizado
bankFk: Caja
Amount: Importe
Reference: Referencia
Cash: Efectivo
</i18n>

View File

@ -185,33 +185,31 @@ const entriesTableRows = computed(() => {
return entries.value;
});
const entriesTotalHb = computed(() =>
entriesTableRows.value.reduce((acc, { hb }) => acc + hb, 0)
);
const entriesTotals = computed(() => {
const totals = {
hb: 0,
freightValue: 0,
packageValue: 0,
cc: 0,
pallet: 0,
m3: 0,
};
const entriesTotalFreight = computed(() =>
toCurrency(
entriesTableRows.value.reduce((acc, { freightValue }) => acc + freightValue, 0)
)
);
entriesTableRows.value.forEach((row) => {
for (const key in totals) {
totals[key] += row[key] || 0;
}
});
const entriesTotalPackageValue = computed(() =>
toCurrency(
entriesTableRows.value.reduce((acc, { packageValue }) => acc + packageValue, 0)
)
);
const entriesTotalCc = computed(() =>
entriesTableRows.value.reduce((acc, { cc }) => acc + cc, 0)
);
const entriesTotalPallet = computed(() =>
entriesTableRows.value.reduce((acc, { pallet }) => acc + pallet, 0)
);
const entriesTotalM3 = computed(() =>
entriesTableRows.value.reduce((acc, { m3 }) => acc + m3, 0)
);
return {
hb: totals.hb.toFixed(2),
freight: toCurrency(totals.freightValue),
packageValue: toCurrency(totals.packageValue),
cc: totals.cc.toFixed(2),
pallet: totals.pallet.toFixed(2),
m3: totals.m3.toFixed(2),
};
});
const getTravelEntries = async (id) => {
const { data } = await axios.get(`Travels/${id}/getEntries`);
@ -382,12 +380,12 @@ const getLink = (param) => `#/travel/${entityId.value}/${param}`;
<QTd></QTd>
<QTd></QTd>
<QTd></QTd>
<QTd class="text-bold">{{ entriesTotalHb }}</QTd>
<QTd class="text-bold">{{ entriesTotalFreight }}</QTd>
<QTd class="text-bold">{{ entriesTotalPackageValue }}</QTd>
<QTd class="text-bold">{{ entriesTotalCc }}</QTd>
<QTd class="text-bold">{{ entriesTotalPallet }}</QTd>
<QTd class="text-bold">{{ entriesTotalM3 }}</QTd>
<QTd class="text-bold">{{ entriesTotals.hb }}</QTd>
<QTd class="text-bold">{{ entriesTotals.freight }}</QTd>
<QTd class="text-bold">{{ entriesTotals.packageValue }}</QTd>
<QTd class="text-bold">{{ entriesTotals.cc }}</QTd>
<QTd class="text-bold">{{ entriesTotals.pallet }}</QTd>
<QTd class="text-bold">{{ entriesTotals.m3 }}</QTd>
</template>
</QTable>
</QCard>

View File

@ -21,8 +21,8 @@ const { t, te } = i18n.global;
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
? createWebHistory
: createWebHashHistory;
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
@ -99,9 +99,15 @@ export default defineRouter(function (/* { store, ssrContext } */) {
title += builtTitle;
}
document.title = title;
});
Router.onError(({ message }) => {
const errorMessages = [
'Failed to fetch dynamically imported module',
'Importing a module script failed',
];
state.set('error', errorMessages.some(message.includes));
});
return Router;
});

View File

@ -21,6 +21,7 @@ export default {
'SupplierAccounts',
'SupplierContacts',
'SupplierAddresses',
'SupplierBalance',
'SupplierConsumption',
'SupplierAgencyTerm',
'SupplierDms',
@ -144,6 +145,16 @@ export default {
component: () =>
import('src/pages/Supplier/Card/SupplierAddressesCreate.vue'),
},
{
path: 'balance',
name: 'SupplierBalance',
meta: {
title: 'balance',
icon: 'balance',
},
component: () =>
import('src/pages/Supplier/Card/SupplierBalance.vue'),
},
{
path: 'consumption',
name: 'SupplierConsumption',

View File

@ -0,0 +1,11 @@
describe('Supplier Balance', () => {
beforeEach(() => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/supplier/1/balance`);
});
it('Should load layout', () => {
cy.get('.q-page').should('be.visible');
});
});

View File

@ -3,9 +3,7 @@ describe('Client balance', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/balance', {
timeout: 5000,
});
cy.visit('#/customer/1101/balance');
});
it('Should load layout', () => {
cy.get('.q-page').should('be.visible');

View File

@ -9,7 +9,7 @@ describe('InvoiceInList', () => {
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit(`/#/invoice-in/list`);
cy.get('#searchbar input').type('{enter}');
cy.get('#searchbar input').should('be.visible').type('{enter}');
});
it('should redirect on clicking a invoice', () => {
@ -22,7 +22,7 @@ describe('InvoiceInList', () => {
});
});
// https://redmine.verdnatura.es/issues/8420
it.skip('should open the details', () => {
it('should open the details', () => {
cy.get(firstDetailBtn).click();
cy.get(summaryHeaders).eq(1).contains('Basic data');
cy.get(summaryHeaders).eq(4).contains('Vat');

View File

@ -1,13 +1,6 @@
/// <reference types="cypress" />
describe('InvoiceOut list', () => {
const invoice = {
Ticket: { val: '8' },
Serial: { val: 'Española rapida', type: 'select' },
};
const invoiceError = {
Ticket: { val: '1' },
Serial: { val: 'Española rapida', type: 'select' },
};
const serial = 'Española rapida';
beforeEach(() => {
cy.viewport(1920, 1080);
@ -32,14 +25,16 @@ describe('InvoiceOut list', () => {
it('should give an error when manual invoicing a ticket that is already invoiced', () => {
cy.dataCy('vnTableCreateBtn').click();
cy.fillInForm(invoiceError);
cy.dataCy('InvoiceOutCreateTicketinput').type(1);
cy.selectOption('[data-cy="InvoiceOutCreateSerialSelect"]', serial);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('This ticket is already invoiced');
});
it('should create a manual invoice and enter to its summary', () => {
cy.dataCy('vnTableCreateBtn').click();
cy.fillInForm(invoice);
cy.dataCy('InvoiceOutCreateTicketinput').type(8);
cy.selectOption('[data-cy="InvoiceOutCreateSerialSelect"]', serial);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
});

View File

@ -10,14 +10,14 @@ describe('VnLog', () => {
cy.openRightMenu();
});
it.skip('should filter by insert actions', () => {
it('should filter by insert actions', () => {
cy.checkOption(':nth-child(7) > .q-checkbox');
cy.get('.q-page').click();
cy.validateContent(chips[0], 'Document');
cy.validateContent(chips[1], 'Beginning');
});
it.skip('should filter by entity', () => {
it('should filter by entity', () => {
cy.selectOption('.q-drawer--right .q-item > .q-select', 'Claim');
cy.get('.q-page').click();
cy.validateContent(chips[0], 'Claim');

View File

@ -71,7 +71,7 @@ Cypress.Commands.add('getValue', (selector) => {
return cy
.get(
selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input',
)
.invoke('val');
}
@ -264,7 +264,7 @@ Cypress.Commands.add('openListSummary', (row) => {
Cypress.Commands.add('openRightMenu', (element) => {
if (element) cy.waitForElement(element);
cy.get('#actions-append').click();
cy.get('[data-cy="toggle-right-drawer"]').click();
});
Cypress.Commands.add('openLeftMenu', (element) => {
@ -330,7 +330,7 @@ Cypress.Commands.add('clickButtonsDescriptor', (id) => {
Cypress.Commands.add('openUserPanel', () => {
cy.get(
'.column > .q-avatar > .q-avatar__content > .q-img > .q-img__container > .q-img__image'
'.column > .q-avatar > .q-avatar__content > .q-img > .q-img__container > .q-img__image',
).click();
});
@ -356,7 +356,7 @@ Cypress.Commands.add('checkValueForm', (id, search) => {
Cypress.Commands.add('checkValueSelectForm', (id, search) => {
cy.get(
`.grid-create > :nth-child(${id}) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > .q-field__input`
`.grid-create > :nth-child(${id}) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > .q-field__input`,
).should('have.value', search);
});