0
0
Fork 0

Merge pull request '4673-claim_module' (#22) from 4673-claim_module into dev

Reviewed-on: verdnatura/salix-front#22
This commit is contained in:
Joan Sanchez 2022-10-26 05:32:57 +00:00
commit df8856dba6
45 changed files with 24438 additions and 333 deletions

21312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@
"axios": "^0.21.1",
"core-js": "^3.6.5",
"quasar": "^2.7.3",
"validator": "^13.7.0",
"vue": "^3.0.0",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.0"

View File

@ -99,7 +99,12 @@ module.exports = configure(function (ctx) {
// https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-framework
framework: {
config: {},
config: {
brand: {
primary: 'orange'
}
},
lang: 'es',
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack

View File

@ -64,7 +64,19 @@ function responseError(error) {
return Promise.resolve(error);
}
axios.interceptors.response.use((response) => response, responseError);
axios.interceptors.response.use((response) => {
const { method } = response.config;
const isSaveRequest = method === 'patch';
if (isSaveRequest) {
quasar.notify({
message: t('globals.dataSaved'),
type: 'positive',
icon: 'check',
});
}
return response;
}, responseError);
</script>
<template>

View File

@ -18,4 +18,4 @@ axios.interceptors.request.use(
function (error) {
return Promise.reject(error);
}
);
);

View File

@ -3,7 +3,7 @@ import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';
const i18n = createI18n({
locale: 'en',
locale: 'es',
messages,
legacy: false
});

View File

@ -27,11 +27,11 @@ onMounted(() => {
stack
size="lg"
:icon="module.icon"
color="orange-6"
color="primary"
class="col-4 button"
:to="{ name: module.stateName }"
>
<div class="text-center text-orange-6 button-text">
<div class="text-center text-primary button-text">
{{ t(`${module.name}.pageTitles.${module.title}`) }}
</div>
</q-btn>

View File

@ -21,24 +21,41 @@ async function onToggleFavoriteModule(moduleName, event) {
</script>
<template>
<q-expansion-item
:default-opened="true"
:label="t('globals.favoriteModules')"
v-if="navigation.favorites.value.length"
>
<q-list padding>
<template v-for="module in navigation.favorites.value" :key="module.title">
<div class="module" v-if="!module.children">
<q-item
clickable
v-ripple
active-class="text-orange"
:key="module.title"
:to="{ name: module.stateName }"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
>
<q-item-section avatar :if="module.icon">
<q-icon :name="module.icon" />
<q-list padding>
<q-item-label header>{{ t('globals.favoriteModules') }}</q-item-label>
<template v-for="module in navigation.favorites.value" :key="module.title">
<div class="module" v-if="!module.children">
<q-item
clickable
v-ripple
active-class="text-primary"
:key="module.title"
:to="{ name: module.stateName }"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
>
<q-item-section avatar :if="module.icon">
<q-icon :name="module.icon" />
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
<q-item-section side>
<div @click="onToggleFavoriteModule(module.name, $event)" class="row items-center">
<q-icon name="vn:pin_off"></q-icon>
</div>
</q-item-section>
</q-item>
</div>
<template v-if="module.children">
<q-expansion-item
class="module"
active-class="text-primary"
:label="t(`${module.name}.pageTitles.${module.title}`)"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
:to="{ name: module.stateName }"
>
<template #header>
<q-item-section avatar>
<q-icon :name="module.icon"></q-icon>
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
<q-item-section side>
@ -46,47 +63,25 @@ async function onToggleFavoriteModule(moduleName, event) {
<q-icon name="vn:pin_off"></q-icon>
</div>
</q-item-section>
</q-item>
</div>
<template v-if="module.children">
<q-expansion-item
class="module"
active-class="text-orange"
:label="t(`${module.name}.pageTitles.${module.title}`)"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
:to="{ name: module.stateName }"
>
<template #header>
<q-item-section avatar>
<q-icon :name="module.icon"></q-icon>
</template>
<template v-for="section in module.children" :key="section.title">
<q-item
clickable
v-ripple
active-class="text-primary"
:to="{ name: section.stateName }"
v-if="!section.roles || !section.roles.length || hasAny(section.roles)"
>
<q-item-section avatar :if="section.icon">
<q-icon :name="section.icon" />
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${module.title}`) }}</q-item-section>
<q-item-section side>
<div @click="onToggleFavoriteModule(module.name, $event)" class="row items-center">
<q-icon name="vn:pin_off"></q-icon>
</div>
</q-item-section>
</template>
<template v-for="section in module.children" :key="section.title">
<q-item
clickable
v-ripple
active-class="text-orange"
:to="{ name: section.stateName }"
v-if="!section.roles || !section.roles.length || hasAny(section.roles)"
>
<q-item-section avatar :if="section.icon">
<q-icon :name="section.icon" />
</q-item-section>
<q-item-section>{{ t(`${module.name}.pageTitles.${section.title}`) }}</q-item-section>
</q-item>
</template>
</q-expansion-item>
</template>
<q-item-section>{{ t(`${module.name}.pageTitles.${section.title}`) }}</q-item-section>
</q-item>
</template>
</q-expansion-item>
</template>
</q-list>
</q-expansion-item>
</template>
</q-list>
<q-separator />
@ -98,7 +93,7 @@ async function onToggleFavoriteModule(moduleName, event) {
class="module"
clickable
v-ripple
active-class="text-orange"
active-class="text-primary"
:key="module.title"
:to="{ name: module.stateName }"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
@ -122,7 +117,7 @@ async function onToggleFavoriteModule(moduleName, event) {
<template v-if="module.children">
<q-expansion-item
class="module"
active-class="text-orange"
active-class="text-primary"
:label="t(`${module.name}.pageTitles.${module.title}`)"
v-if="!module.roles || !module.roles.length || hasAny(module.roles)"
:to="{ name: module.stateName }"
@ -146,7 +141,7 @@ async function onToggleFavoriteModule(moduleName, event) {
<q-item
clickable
v-ripple
active-class="text-orange"
active-class="text-primary"
:to="{ name: section.stateName }"
v-if="!section.roles || !section.roles.length || hasAny(section.roles)"
>

View File

@ -0,0 +1,30 @@
<template>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="QInput" square />
</div>
<div class="col">
<q-skeleton type="QInput" square />
</div>
</div>
<div class="row q-gutter-md">
<q-skeleton type="QBtn" />
<q-skeleton type="QBtn" />
</div>
</template>

View File

@ -0,0 +1,57 @@
<template>
<div class="header bg-primary q-pa-sm q-mb-md">
<q-skeleton type="rect" square />
</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
<div class="col">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</div>
</div>
</template>
<style lang="scss" scoped>
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
}
}
</style>

View File

@ -10,6 +10,10 @@ const $props = defineProps({
type: String,
default: '',
},
filter: {
type: Object,
default: null,
},
autoLoad: {
type: Boolean,
default: false,
@ -29,6 +33,7 @@ const $props = defineProps({
});
defineEmits(['onNavigate']);
defineExpose({ fetch });
const isLoading = ref(false);
const hasMoreData = ref(false);
@ -38,10 +43,11 @@ const pagination = ref({
page: 1,
});
const rows = ref([]);
const rows = ref(null);
onMounted(() => {
if ($props.autoLoad) fetch();
else rows.value = [];
});
async function fetch() {
@ -54,6 +60,8 @@ async function fetch() {
skip: rowsPerPage * (page - 1),
};
Object.assign(filter, $props.filter);
if (sortBy) filter.order = sortBy;
const { data } = await axios.get($props.url, {
@ -67,6 +75,7 @@ async function fetch() {
hasMoreData.value = data.length === rowsPerPage;
if (!rows.value) rows.value = [];
for (const row of data) rows.value.push(row);
pagination.value.rowsNumber = rows.value.length;
@ -80,7 +89,7 @@ async function fetch() {
async function onLoad(...params) {
const done = params[1];
if (rows.value.length === 0) return done(false);
if (!rows.value || rows.value.length === 0) return done(false);
pagination.value.page = pagination.value.page + 1;
@ -93,7 +102,7 @@ async function onLoad(...params) {
<template>
<q-infinite-scroll @load="onLoad" :offset="offset" class="column items-center">
<div class="card-list q-gutter-y-md">
<div v-if="rows" class="card-list q-gutter-y-md">
<q-card class="card" v-for="row of rows" :key="row.id">
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<q-item-section class="q-pa-md" @click="$emit('onNavigate', row.id)">
@ -131,6 +140,25 @@ async function onLoad(...params) {
<q-spinner color="orange" size="md" />
</div>
</div>
<div v-if="!rows" class="card-list q-gutter-y-md">
<q-card class="card" v-for="$index in $props.rowsPerPage" :key="$index">
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<q-item-section class="q-pa-md">
<q-skeleton type="rect" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" class="q-mb-md" square />
<q-skeleton type="text" square />
<q-skeleton type="text" square />
</q-item-section>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<q-skeleton type="circle" class="q-mb-md" size="40px" />
<q-skeleton type="circle" class="q-mb-md" size="40px" />
<q-skeleton type="circle" class="q-mb-md" size="40px" />
</q-card-actions>
</q-item>
</q-card>
</div>
</q-infinite-scroll>
</template>

View File

@ -1,6 +1,6 @@
<script setup>
import { onMounted, computed } from 'vue';
import { Dark } from 'quasar';
import { Dark, Quasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import axios from 'axios';
@ -13,6 +13,21 @@ const session = useSession();
const router = useRouter();
const { t, locale } = useI18n();
const userLocale = computed({
get() {
return locale.value;
},
set(value) {
locale.value = value;
if (value === 'en') value = 'en-GB';
import(`quasar/lang/${value}`).then((language) => {
Quasar.lang.set(language.default);
});
},
});
const darkMode = computed({
get() {
return Dark.isActive;
@ -35,6 +50,7 @@ function updatePreferences() {
}
if (user.value.lang) {
locale.value = user.value.lang;
userLocale.value = user.value.lang;
}
}
@ -66,9 +82,9 @@ function logout() {
<div class="column panel">
<div class="text-h6 q-mb-md">{{ t('components.userPanel.settings') }}</div>
<q-toggle
v-model="locale"
v-model="userLocale"
@update:model-value="saveLanguage"
:label="t(`globals.lang['${locale}']`)"
:label="t(`globals.lang['${userLocale}']`)"
icon="public"
color="orange"
false-value="es"

View File

@ -4,7 +4,7 @@ const navigation = useNavigation();
describe('useNavigation', () => {
it('should return the routes for all modules', async () => {
expect(navigation.modules.value.length).toEqual(3);
expect(navigation.modules.value.length).toBeGreaterThan(1);
});
it('should return a proper formated object without the children property', async () => {

View File

@ -9,7 +9,6 @@ const user = ref({
});
const roles = ref([]);
const drawer = ref(true);
export function useState() {

View File

@ -0,0 +1,86 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import validator from 'validator';
const models = ref(null);
export function useValidator() {
if (!models.value) fetch();
function fetch() {
axios.get('Schemas/ModelInfo')
.then(response => models.value = response.data)
}
function validate(propertyRule) {
const modelInfo = models.value;
if (!modelInfo || !propertyRule) return;
const rule = propertyRule.split('.');
const model = rule[0];
const property = rule[1];
const modelName = model.charAt(0).toUpperCase() + model.slice(1);
if (!modelInfo[modelName]) return;
const modelValidations = modelInfo[modelName].validations;
if (!modelValidations[property]) return;
const rules = modelValidations[property].map((validation) => {
return validations(validation)[validation.validation];
});
if (property === 'socialName') {
console.log(modelValidations[property])
}
return rules;
}
const { t } = useI18n();
const validations = function (validation) {
return {
presence: (value) => {
let message = `Value can't be empty`;
if (validation.message)
message = t(validation.message) || validation.message
return !validator.isEmpty(value ? String(value) : '') || message
},
length: (value) => {
const options = {
min: validation.min || validation.is,
max: validation.max || validation.is
};
value = String(value);
if (!value) value = '';
let message = `Value should have at most ${options.max} characters`;
if (validation.is)
message = `Value should be ${validation.is} characters long`;
if (validation.min)
message = `Value should have at least ${validation.min} characters`;
if (validation.min && validation.max)
message = `Value should have a length between ${validation.min} and ${validation.max}`;
return validator.isLength(value, options) || message;
},
numericality: (value) => {
if (validation.int)
return validator.isInt(value) || 'Value should be integer'
return validator.isNumeric(value) || 'Value should be a number'
},
custom: (value) => validation.bindedFunction(value) || 'Invalid value'
};
};
return {
validate
};
}

112
src/core/lib/validator.js Normal file
View File

@ -0,0 +1,112 @@
import * as validator from 'validator';
export const validators = {
presence: ($translate, value) => {
if (validator.isEmpty(value ? String(value) : ''))
throw new Error(_($translate, `Value can't be empty`));
},
absence: ($translate, value) => {
if (!validator.isEmpty(value))
throw new Error(_($translate, `Value should be empty`));
},
length: ($translate, value, conf) => {
let options = {
min: conf.min || conf.is,
max: conf.max || conf.is
};
let val = value ? String(value) : '';
if (!validator.isLength(val, options)) {
if (conf.is) {
throw new Error(_($translate,
`Value should be %s characters long`, [conf.is]));
} else if (conf.min && conf.max) {
throw new Error(_($translate,
`Value should have a length between %s and %s`, [conf.min, conf.max]));
} else if (conf.min) {
throw new Error(_($translate,
`Value should have at least %s characters`, [conf.min]));
} else {
throw new Error(_($translate,
`Value should have at most %s characters`, [conf.max]));
}
}
},
numericality: ($translate, value, conf) => {
if (conf.int) {
if (!validator.isInt(value))
throw new Error(_($translate, `Value should be integer`));
} else if (!validator.isNumeric(value))
throw new Error(_($translate, `Value should be a number`));
},
inclusion: ($translate, value, conf) => {
if (!validator.isIn(value, conf.in))
throw new Error(_($translate, `Invalid value`));
},
exclusion: ($translate, value, conf) => {
if (validator.isIn(value, conf.in))
throw new Error(_($translate, `Invalid value`));
},
format: ($translate, value, conf) => {
if (!validator.matches(value, conf.with))
throw new Error(_($translate, `Invalid value`));
},
custom: ($translate, value, conf) => {
if (!conf.bindedFunction(value))
throw new Error(_($translate, `Invalid value`));
}
};
/**
* Checks if value satisfies a set of validations.
*
* @param {*} value The value
* @param {Array} validations Array with validations
*/
export function validateAll($translate, value, validations) {
for (let conf of validations)
validate($translate, value, conf);
}
/**
* Checks if value satisfies a validation.
*
* @param {*} value The value
* @param {Object} conf The validation configuration
*/
export function validate($translate, value, conf) {
let validator = validators[conf.validation];
try {
let isEmpty = value == null || value === '';
if (isEmpty)
checkNull($translate, value, conf);
if (validator && (!isEmpty || conf.validation == 'presence'))
validator($translate, value, conf);
} catch (e) {
let message = conf.message ? conf.message : e.message;
throw new Error(_($translate, message));
}
}
/**
* Checks if value satisfies a blank or not null validation.
*
* @param {*} value The value
* @param {Object} conf The validation configuration
*/
export function checkNull($translate, value, conf) {
if (conf.allowBlank === false && value === '')
throw new Error(_($translate, `Value can't be blank`));
else if (conf.allowNull === false && value == null)
throw new Error(_($translate, `Value can't be null`));
}
export function _($translate, text, params = []) {
text = $translate.instant(text);
for (let i = 0; i < params.length; i++) {
text = text.replace('%s', params[i]);
}
return text;
}

View File

@ -12,7 +12,7 @@
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #1976d2;
$primary: #ff9800;
$secondary: #26a69a;
$accent: #9c27b0;

View File

@ -1,5 +1,11 @@
import toLowerCase from './toLowerCase';
import toDate from './toDate';
import toCurrency from './toCurrency';
import toPercentage from './toPercentage';
export default {
export {
toLowerCase,
toDate,
toCurrency,
toPercentage,
};

21
src/filters/toCurrency.js Normal file
View File

@ -0,0 +1,21 @@
import { useI18n } from 'vue-i18n';
export default function (value, symbol = 'EUR', fractionSize = 2) {
if (value == null || value === '') return;
const { locale } = useI18n();
const options = {
style: 'currency',
currency: symbol,
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
};
const lang = locale.value == 'es' ? 'de' : locale.value;
return new Intl.NumberFormat(lang, options)
.format(value);
}

12
src/filters/toDate.js Normal file
View File

@ -0,0 +1,12 @@
import { useI18n } from 'vue-i18n';
export default function (value, options = {}) {
if (!value) return;
if (!options.dateStyle) options.dateStyle = 'short';
const { locale } = useI18n();
const date = new Date(value);
return new Intl.DateTimeFormat(locale.value, options).format(date)
}

View File

@ -0,0 +1,18 @@
import { useI18n } from 'vue-i18n';
export default function (value, fractionSize = 2) {
if (value == null || value === '') return;
const { locale } = useI18n();
const options = {
style: 'percent',
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
};
return new Intl.NumberFormat(locale, options)
.format(parseFloat(value));
}

View File

@ -12,6 +12,18 @@ export default {
theme: 'Theme',
logOut: 'Log out',
dataSaved: 'Data saved',
add: 'Add',
create: 'Create',
save: 'Save',
remove: 'Remove',
reset: 'Reset',
cancel: 'Cancel',
yes: 'Yes',
no: 'No',
noChanges: 'No changes to save',
confirmRemove: 'You are about to delete this row. Are you sure?',
rowAdded: 'Row added',
rowRemoved: 'Row removed'
},
moduleIndex: {
allModules: 'All modules'
@ -41,6 +53,7 @@ export default {
customers: 'Customers',
list: 'List',
createCustomer: 'Create customer',
summary: 'Summary',
basicData: 'Basic Data'
},
list: {
@ -48,6 +61,87 @@ export default {
email: 'Email',
customerOrders: 'Display customer orders',
moreOptions: 'More options'
},
card: {
customerList: 'Customer list',
customerId: 'Claim ID',
salesPerson: 'Sales person',
credit: 'Credit',
securedCredit: 'Secured credit',
payMethod: 'Pay method',
debt: 'Debt',
isDisabled: 'Customer is disabled',
isFrozen: 'Customer is frozen',
hasDebt: 'Customer has debt',
notChecked: 'Customer not checked',
noWebAccess: 'Web access is disabled'
},
summary: {
basicData: 'Basic data',
fiscalAddress: 'Fiscal address',
fiscalData: 'Fiscal data',
billingData: 'Billing data',
consignee: 'Consignee',
businessData: 'Business data',
financialData: 'Financial data',
customerId: 'Customer ID',
name: 'Name',
contact: 'Contact',
phone: 'Phone',
mobile: 'Mobile',
email: 'Email',
salesPerson: 'Sales person',
contactChannel: 'Contact channel',
socialName: 'Social name',
fiscalId: 'Fiscal ID',
postcode: 'Postcode',
province: 'Province',
country: 'Country',
street: 'Address',
isEqualizated: 'Is equalizated',
isActive: 'Is active',
invoiceByAddress: 'Invoice by address',
verifiedData: 'Verified data',
hasToInvoice: 'Has to invoice',
notifyByEmail: 'Notify by email',
vies: 'VIES',
payMethod: 'Pay method',
bankAccount: 'Bank account',
dueDay: 'Due day',
hasLcr: 'Has LCR',
hasCoreVnl: 'Has core VNL',
hasB2BVnl: 'Has B2B VNL',
addressName: 'Address name',
addressCity: 'City',
addressStreet: 'Street',
username: 'Username',
webAccess: 'Web access',
totalGreuge: 'Total greuge',
mana: 'Mana',
priceIncreasingRate: 'Price increasing rate',
averageInvoiced: 'Average invoiced',
claimRate: 'Claming rate',
risk: 'Risk',
riskInfo: 'Invoices minus payments plus orders not yet invoiced',
credit: 'Credit',
creditInfo: `Company's maximum risk`,
securedCredit: 'Secured credit',
securedCreditInfo: `Solunion's maximum risk`,
balance: 'Balance',
balanceInfo: 'Invoices minus payments',
balanceDue: 'Balance due',
balanceDueInfo: 'Deviated invoices minus payments',
recoverySince: 'Recovery since',
},
basicData: {
socialName: 'Fiscal name',
businessType: 'Business type',
contact: 'Contact',
email: 'Email',
phone: 'Phone',
mobile: 'Mobile',
salesPerson: 'Sales person',
contactChannel: 'Contact channel'
}
},
ticket: {
@ -55,8 +149,27 @@ export default {
tickets: 'Tickets',
list: 'List',
createTicket: 'Create ticket',
summary: 'Summary',
basicData: 'Basic Data'
},
list: {
nickname: 'Nickname',
state: 'State',
shipped: 'Shipped',
landed: 'Landed',
salesPerson: 'Sales person',
total: 'Total'
},
card: {
ticketId: 'Ticket ID',
state: 'State',
customerId: 'Customer ID',
salesPerson: 'Sales person',
agency: 'Agency',
shipped: 'Shipped',
warehouse: 'Warehouse',
customerCard: 'Customer card'
},
boxing: {
expedition: 'Expedition',
item: 'Item',
@ -65,9 +178,66 @@ export default {
selectTime: 'Select time:',
selectVideo: 'Select video:',
notFound: 'No videos available'
}
},
claim: {
pageTitles: {
claims: 'Claims',
list: 'List',
createClaim: 'Create claim',
rmaList: 'RMA',
summary: 'Summary',
basicData: 'Basic Data',
rma: 'RMA'
},
list: {
customer: 'Customer',
assignedTo: 'Assigned',
created: 'Created',
state: 'State'
},
rmaList: {
code: 'Code',
newRma: 'New RMA...'
},
rma: {
user: 'User',
created: 'Created'
},
card: {
claimId: 'Claim ID',
assignedTo: 'Assigned',
created: 'Created',
state: 'State',
ticketId: 'Ticket ID',
customerSummary: 'Customer summary',
claimedTicket: 'Claimed ticket'
},
summary: {
customer: 'Customer',
assignedTo: 'Assigned',
attendedBy: 'Attended by',
created: 'Created',
state: 'State',
details: {
title: 'Details',
columns: {
item: 'Item',
landed: 'Delivered',
quantity: 'Quantity'
}
},
},
basicData: {
customer: 'Customer',
assignedTo: 'Assigned',
created: 'Created',
state: 'State',
packages: 'Packages',
picked: 'Picked',
returnOfMaterial: 'Return of material authorization (RMA)'
},
},
components: {
topbar: {},
userPanel: {
@ -78,6 +248,11 @@ export default {
noData: 'No data to display',
openCard: 'View card',
openSummary: 'Open summary'
},
card: {
mainList: 'Main list',
summary: 'Summary',
moreOptions: 'More options',
}
},
};

View File

@ -11,7 +11,19 @@ export default {
favoriteModules: 'Módulos favoritos',
theme: 'Tema',
logOut: 'Cerrar sesión',
dataSaved: 'Datos guardados'
dataSaved: 'Datos guardados',
add: 'Añadir',
create: 'Crear',
save: 'Guardar',
remove: 'Eliminar',
reset: 'Restaurar',
cancel: 'Cancelar',
yes: 'Si',
no: 'No',
noChanges: 'Sin cambios que guardar',
confirmRemove: 'Vas a eliminar este registro. ¿Continuar?',
rowAdded: 'Fila añadida',
rowRemoved: 'Fila eliminada'
},
moduleIndex: {
allModules: 'Todos los módulos'
@ -41,6 +53,7 @@ export default {
customers: 'Clientes',
list: 'Listado',
createCustomer: 'Crear cliente',
summary: 'Resumen',
basicData: 'Datos básicos'
},
list: {
@ -48,6 +61,86 @@ export default {
email: 'Email',
customerOrders: 'Mostrar órdenes del cliente',
moreOptions: 'Más opciones'
},
card: {
customerId: 'ID cliente',
salesPerson: 'Comercial',
credit: 'Crédito',
securedCredit: 'Crédito asegurado',
payMethod: 'Método de pago',
debt: 'Riesgo',
isDisabled: 'El cliente está desactivado',
isFrozen: 'El cliente está congelado',
hasDebt: 'El cliente tiene riesgo',
notChecked: 'El cliente no está comprobado',
noWebAccess: 'El acceso web está desactivado'
},
summary: {
basicData: 'Datos básicos',
fiscalAddress: 'Dirección fiscal',
fiscalData: 'Datos fiscales',
billingData: 'Datos de facturación',
consignee: 'Consignatario',
businessData: 'Datos comerciales',
financialData: 'Datos financieros',
customerId: 'ID cliente',
name: 'Nombre',
contact: 'Contacto',
phone: 'Teléfono',
mobile: 'Móvil',
email: 'Email',
salesPerson: 'Comercial',
contactChannel: 'Canal de contacto',
socialName: 'Razón social',
fiscalId: 'NIF/CIF',
postcode: 'Código postal',
province: 'Provincia',
country: 'País',
street: 'Calle',
isEqualizated: 'Equalizado',
isActive: 'Activo',
invoiceByAddress: 'Facturar por consignatario',
verifiedData: 'Datos verificados',
hasToInvoice: 'Facturar',
notifyByEmail: 'Notificar por email',
vies: 'VIES',
payMethod: 'Método de pago',
bankAccount: 'Cuenta bancaria',
dueDay: 'Día de pago',
hasLcr: 'Recibido LCR',
hasCoreVnl: 'Recibido core VNL',
hasB2BVnl: 'Recibido B2B VNL',
addressName: 'Nombre de la dirección',
addressCity: 'Ciudad',
addressStreet: 'Calle',
username: 'Usuario',
webAccess: 'Acceso web',
totalGreuge: 'Greuge total',
mana: 'Maná',
priceIncreasingRate: 'Ratio de incremento de precio',
averageInvoiced: 'Facturación media',
claimRate: 'Ratio de reclamaciones',
risk: 'Riesgo',
riskInfo: 'Facturas menos recibos mas pedidos sin facturar',
credit: 'Crédito',
creditInfo: `Riesgo máximo asumido por la empresa`,
securedCredit: 'Crédito asegurado',
securedCreditInfo: `Riesgo máximo asumido por Solunion`,
balance: 'Balance',
balanceInfo: 'Facturas menos recibos',
balanceDue: 'Saldo vencido',
balanceDueInfo: 'Facturas fuera de plazo menos recibos',
recoverySince: 'Recobro desde',
},
basicData: {
socialName: 'Nombre fiscal',
businessType: 'Tipo de negocio',
contact: 'Contacto',
email: 'Email',
phone: 'Teléfono',
mobile: 'Móvil',
salesPerson: 'Comercial',
contactChannel: 'Canal de contacto'
}
},
ticket: {
@ -55,8 +148,27 @@ export default {
tickets: 'Tickets',
list: 'Listado',
createTicket: 'Crear ticket',
summary: 'Resumen',
basicData: 'Datos básicos'
},
list: {
nickname: 'Alias',
state: 'Estado',
shipped: 'Enviado',
landed: 'Entregado',
salesPerson: 'Comercial',
total: 'Total'
},
card: {
ticketId: 'ID ticket',
state: 'Estado',
customerId: 'ID cliente',
salesPerson: 'Comercial',
agency: 'Agencia',
shipped: 'Enviado',
warehouse: 'Almacén',
customerCard: 'Ficha del cliente'
},
boxing: {
expedition: 'Expedición',
item: 'Artículo',
@ -67,6 +179,64 @@ export default {
notFound: 'No hay vídeos disponibles'
}
},
claim: {
pageTitles: {
claims: 'Reclamaciones',
list: 'Listado',
createClaim: 'Crear reclamación',
rmaList: 'RMA',
summary: 'Resumen',
basicData: 'Datos básicos',
rma: 'RMA'
},
list: {
customer: 'Cliente',
assignedTo: 'Asignada a',
created: 'Creada',
state: 'Estado'
},
rmaList: {
code: 'Código',
newRma: 'Nuevo RMA...'
},
rma: {
user: 'Usuario',
created: 'Creado'
},
card: {
claimId: 'ID reclamación',
assignedTo: 'Asignada a',
created: 'Creada',
state: 'Estado',
ticketId: 'ID ticket',
customerSummary: 'Resumen del cliente',
claimedTicket: 'Ticket reclamado'
},
summary: {
customer: 'Cliente',
assignedTo: 'Asignada a',
attendedBy: 'Atendida por',
created: 'Creada',
state: 'Estado',
details: {
title: 'Detalles',
columns: {
item: 'Artículo2',
landed: 'Entregado',
quantity: 'Cantidad'
}
},
},
basicData: {
customer: 'Cliente',
assignedTo: 'Asignada a',
created: 'Creada',
state: 'Estado',
packages: 'Bultos',
picked: 'Recogida',
returnOfMaterial: 'Autorización de retorno de materiales (RMA)'
}
},
components: {
topbar: {},
userPanel: {
@ -77,6 +247,11 @@ export default {
noData: 'Sin datos que mostrar',
openCard: 'Ver ficha',
openSummary: 'Abrir detalles'
},
card: {
mainList: 'Listado principal',
summary: 'Resumen',
moreOptions: 'Más opciones',
}
},
};

View File

@ -3,7 +3,7 @@ import Navbar from 'src/components/Navbar.vue';
</script>
<template>
<q-layout view="hHh lpR fFf">
<q-layout view="hHh LpR fFf">
<Navbar />
<router-view></router-view>
</q-layout>

View File

@ -0,0 +1,243 @@
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'src/components/SkeletonForm.vue';
onMounted(() => {
fetch();
fetchWorkers();
fetchClaimStates();
});
const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const { validate } = useValidator();
const session = useSession();
const token = session.getToken();
const claim = ref(null);
const claimCopy = ref(null);
const hasChanges = ref(false);
function fetch() {
const id = route.params.id;
const filter = {
include: [
{
relation: 'client',
scope: {
fields: ['name'],
},
},
],
};
const options = { params: { filter } };
axios.get(`Claims/${id}`, options).then(({ data }) => {
claim.value = data;
claimCopy.value = Object.assign({}, data);
watch(claim.value, () => (hasChanges.value = true));
});
}
const workers = ref([]);
const workersCopy = ref([]);
function fetchWorkers() {
const filter = {
where: {
role: 'salesPerson',
},
};
const options = { params: { filter } };
axios.get(`Workers/activeWithRole`, options).then(({ data }) => {
workers.value = data;
workersCopy.value = data;
});
}
const claimStates = ref([]);
const claimStatesCopy = ref([]);
function fetchClaimStates() {
axios.get(`ClaimStates`).then(({ data }) => {
claimStates.value = data;
claimStatesCopy.value = data;
});
}
function filter(value, update, options, originalOptions, filter) {
update(
() => {
if (value === '') {
options.value = originalOptions.value;
return;
}
options.value = options.value.filter(filter);
},
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
}
function filterWorkers(value, update) {
const search = value.toLowerCase();
filter(value, update, workers, workersCopy, (row) => {
const id = row.id;
const name = row.name.toLowerCase();
const idMatch = id == search;
const nameMatch = name.indexOf(search) > -1;
return idMatch || nameMatch;
});
}
function filterStates(value, update) {
const search = value.toLowerCase();
filter(value, update, claimStates, claimStatesCopy, (row) => {
const description = row.description.toLowerCase();
return description.indexOf(search) > -1;
});
}
function save() {
const id = route.params.id;
const formData = claim.value;
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
});
}
axios.patch(`Claims/${id}`, formData).then((hasChanges.value = false));
}
function onReset() {
claim.value = claimCopy.value;
hasChanges.value = false;
}
</script>
<template>
<q-page class="q-pa-md">
<div class="container">
<q-card class="q-pa-md">
<skeleton-form v-if="!claim" />
<q-form v-if="claim" @submit="save" @reset="onReset" greedy>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input v-model="claim.client.name" :label="t('claim.basicData.customer')" disable />
</div>
<div class="col">
<q-input v-model="claim.created" mask="####-##-##" fill-mask="_" autofocus>
<template #append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="claim.created" mask="YYYY-MM-DD">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-select
v-model="claim.workerFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('claim.basicData.assignedTo')"
map-options
use-input
@filter="filterWorkers"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
<template #before>
<q-avatar color="orange">
<q-img
v-if="claim.workerFk"
:src="`/api/Images/user/160x160/${claim.workerFk}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
</template>
</q-select>
</div>
<div class="col">
<q-select
v-model="claim.claimStateFk"
:options="claimStates"
option-value="id"
option-label="description"
emit-value
:label="t('claim.basicData.state')"
map-options
use-input
@filter="filterStates"
:rules="validate('claim.claimStateFk')"
:input-debounce="0"
>
</q-select>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="claim.packages"
:label="t('claim.basicData.packages')"
:rules="validate('claim.packages')"
/>
</div>
<div class="col">
<q-input
v-model="claim.rma"
:label="t('claim.basicData.returnOfMaterial')"
:rules="validate('claim.rma')"
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-checkbox v-model="claim.hasToPickUp" :label="t('claim.basicData.picked')" />
</div>
</div>
<div>
<q-btn :label="t('globals.save')" type="submit" color="primary" />
<q-btn :label="t('globals.reset')" type="reset" class="q-ml-sm" color="primary" flat />
</div>
</q-form>
</q-card>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
}
</style>

View File

@ -0,0 +1,195 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useState } from 'src/composables/useState';
import { toDate } from 'src/filters';
const route = useRoute();
const { t } = useI18n();
const state = useState();
onMounted(async () => {
await fetch();
});
const claim = ref({});
async function fetch() {
const entityId = route.params.id;
const filter = {
include: [
{ relation: 'client' },
{ relation: 'claimState' },
{
relation: 'claimState',
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};
const options = { params: { filter } };
const { data } = await axios.get(`Claims/${entityId}`, options);
if (data) claim.value = data;
}
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
}
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit text-grey-8 descriptor">
<div class="header bg-primary q-pa-sm">
<router-link :to="{ path: '/claim/list' }">
<q-btn round flat dense size="md" icon="view_list" color="white">
<q-tooltip>{{ t('components.card.mainList') }}</q-tooltip>
</q-btn>
</router-link>
<router-link :to="{ name: 'ClaimSummary', params: { id: route.params.id } }">
<q-btn round flat dense size="md" icon="launch" color="white">
<q-tooltip>{{ t('components.card.summary') }}</q-tooltip>
</q-btn>
</router-link>
<q-btn round flat dense size="md" icon="more_vert" color="white">
<q-tooltip>{{ t('components.card.moreOptions') }}</q-tooltip>
<!-- <q-menu>
<q-list>
<q-item clickable v-ripple>Option 1</q-item>
<q-item clickable v-ripple>Option 2</q-item>
</q-list>
</q-menu> -->
</q-btn>
</div>
<div v-if="claim.client" class="q-py-sm">
<q-list>
<q-item-label header class="ellipsis text-h5" :lines="1">
{{ claim.client.name }}
<q-tooltip>{{ claim.client.name }}</q-tooltip>
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.card.claimId') }}</q-item-label>
<q-item-label>#{{ claim.id }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.card.created') }}</q-item-label>
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.card.assignedTo') }}</q-item-label>
<q-item-label>{{ claim.worker.user.name }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.card.state') }}</q-item-label>
<q-item-label>
<q-chip :color="stateColor(claim.claimState.code)" dense>
{{ claim.claimState.description }}
</q-chip>
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.card.ticketId') }}</q-item-label>
<q-item-label>{{ claim.ticketFk }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-actions>
<q-btn
size="md"
icon="vn:client"
color="primary"
:to="{ name: 'CustomerCard', params: { id: claim.clientFk } }"
>
<q-tooltip>{{ t('claim.card.customerSummary') }}</q-tooltip>
</q-btn>
<q-btn
size="md"
icon="vn:ticket"
color="primary"
:to="{ name: 'TicketCard', params: { id: claim.ticketFk } }"
>
<q-tooltip>{{ t('claim.card.claimedTicket') }}</q-tooltip>
</q-btn>
</q-card-actions>
</div>
<!-- Skeleton -->
<div id="descriptor-skeleton" v-if="!claim.client">
<div class="col q-pl-sm q-pa-sm">
<q-skeleton type="text" square height="45px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
</div>
<q-card-actions>
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
</q-card-actions>
</div>
<q-separator />
<q-list>
<q-item :to="{ name: 'ClaimBasicData' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="vn:settings" />
</q-item-section>
<q-item-section>{{ t('claim.pageTitles.basicData') }}</q-item-section>
</q-item>
<q-item :to="{ name: 'ClaimRma' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="vn:barcode" />
</q-item-section>
<q-item-section>{{ t('claim.pageTitles.rma') }}</q-item-section>
</q-item>
</q-list>
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view v-if="claim.id" :claim="claim"></router-view>
</q-page-container>
</template>
<style lang="scss">
.q-scrollarea__content {
max-width: 100%;
}
</style>
<style lang="scss" scoped>
.descriptor {
h5 {
margin: 0 15px;
}
.header {
display: flex;
justify-content: space-between;
}
.q-card__actions {
justify-content: center;
}
#descriptor-skeleton .q-card__actions {
justify-content: space-around;
}
}
</style>

View File

@ -0,0 +1,136 @@
<script setup>
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import SmartCard from 'src/components/SmartCard.vue';
import { toDate } from 'src/filters';
onMounted(() => fetch());
const $props = defineProps({
claim: {
type: Object,
required: true,
},
});
const quasar = useQuasar();
const { t } = useI18n();
const filter = {
include: {
relation: 'worker',
scope: {
include: {
relation: 'user',
},
},
},
where: {
code: $props.claim.rma,
},
};
function fetch() {
//console.log($props.claim);
}
function addRow() {
const formData = {
code: $props.claim.rma,
};
axios.post(`ClaimRmas`, formData).then(() => {
quasar.notify({
type: 'positive',
message: t('globals.rowAdded'),
icon: 'check',
});
});
}
const confirmShown = ref(false);
const rmaId = ref(null);
function confirmRemove(id) {
confirmShown.value = true;
rmaId.value = id;
}
function remove() {
const id = rmaId.value;
axios.delete(`ClaimRmas/${id}`).then(() => {
confirmShown.value = false;
quasar.notify({
type: 'positive',
message: t('globals.rowRemoved'),
icon: 'check',
});
});
}
function hide() {
rmaId.value = null;
}
</script>
<template>
<q-page class="q-pa-md sticky">
<q-page-sticky expand position="top">
<q-toolbar class="bg-grey-9">
<q-space />
<div class="q-gutter-md">
<q-btn icon="add" :label="t('globals.add')" color="primary" @click="addRow()" />
</div>
</q-toolbar>
</q-page-sticky>
<smart-card ref="card" url="/ClaimRmas" :filter="filter" sort-by="id DESC" auto-load>
<template #header="{ row }">
<q-item-label caption>{{ t('claim.rma.user') }}</q-item-label>
<q-item-label>{{ row.worker.user.name }}</q-item-label>
</template>
<template #labels="{ row }">
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.rma.created') }}</q-item-label>
<q-item-label>{{ toDate(row.created, { timeStyle: 'medium' }) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
<template #actions="{ row }">
<q-btn flat round color="orange" icon="vn:bin" @click="confirmRemove(row.id)">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</template>
</smart-card>
</q-page>
<q-dialog v-model="confirmShown" persistent @hide="hide">
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="warning" color="primary" text-color="white" />
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style lang="scss" scoped>
.q-toolbar {
background-color: $grey-9;
}
.sticky {
padding-top: 66px;
}
.q-page-sticky {
z-index: 2998;
}
</style>

View File

@ -0,0 +1,189 @@
<script setup>
import { onMounted, defineProps, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { toDate } from 'src/filters';
import SkeletonSummary from 'src/components/SkeletonSummary';
onMounted(() => fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
claimId: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.claimId || route.params.id);
const claim = ref(null);
const salesClaimed = ref(null);
function fetch() {
const id = entityId.value;
axios.get(`Claims/${id}/getSummary`).then(({ data }) => {
claim.value = data.claim;
salesClaimed.value = data.salesClaimed;
});
}
const detailsColumns = ref([
{
name: 'item',
label: t('claim.summary.details.columns.item'),
field: (row) => row.sale.itemFk,
sortable: true,
},
{
name: 'landed',
label: t('claim.summary.details.columns.landed'),
field: (row) => row.sale.ticket.landed,
format: (value) => toDate(value),
sortable: true,
},
{
name: 'quantity',
label: t('claim.summary.details.columns.quantity'),
field: (row) => row.sale.quantity,
sortable: true,
},
{
name: 'claimed',
label: 'Claimed',
field: (row) => row.quantity,
sortable: true,
},
{
name: 'description',
label: 'Description',
field: (row) => row.sale.concept,
},
{
name: 'price',
label: 'Price',
field: (row) => row.sale.price,
sortable: true,
},
{
name: 'discount',
label: 'Discount',
field: (row) => row.sale.discount,
format: (value) => `${value} %`,
sortable: true,
},
{
name: 'total',
label: 'Total',
field: ({ sale }) => sale.quantity * sale.price * ((100 - sale.discount) / 100),
sortable: true,
},
]);
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
}
</script>
<template>
<q-page class="q-pa-md">
<div class="summary container">
<q-card>
<skeleton-summary v-if="!claim" />
<template v-if="claim">
<div class="header bg-primary q-pa-sm q-mb-md">{{ claim.id }} - {{ claim.client.name }}</div>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.created') }}</q-item-label>
<q-item-label>{{ toDate(claim.created) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.state') }}</q-item-label>
<q-item-label>
<q-chip :color="stateColor(claim.claimState.code)" dense>
{{ claim.claimState.description }}
</q-chip>
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.assignedTo') }}</q-item-label>
<q-item-label>{{ claim.worker.user.nickname }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.summary.attendedBy') }}</q-item-label>
<q-item-label>{{ claim.client.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-section class="q-pa-md">
<h6>{{ t('claim.summary.details.title') }}</h6>
<q-table :columns="detailsColumns" :rows="salesClaimed" flat></q-table>
</q-card-section>
<q-card-section class="q-pa-md">
<h6>Action</h6>
<q-separator />
<div id="slider-container">
<q-slider
v-model="claim.responsibility"
label
:label-value="'Responsibility'"
label-always
color="primary"
markers
:marker-labels="[
{ value: 1, label: 'Company' },
{ value: 5, label: 'Person' },
]"
:min="1"
:max="5"
readonly
/>
</div>
</q-card-section>
</template>
</q-card>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 950px;
}
.summary {
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
</style>

View File

@ -0,0 +1,52 @@
<script setup>
import { reactive, watch } from 'vue'
const customer = reactive({
name: '',
});
watch(() => customer.name, () => {
console.log('customer.name changed');
});
</script>
<template>
<q-page class="q-pa-md">
<q-card class="q-pa-md">
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<q-input
filled
v-model="customer.name"
label="Your name *"
hint="Name and surname"
lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']"
/>
<q-input
filled
type="number"
v-model="age"
label="Your age *"
lazy-rules
:rules="[
val => val !== null && val !== '' || 'Please type your age',
val => val > 0 && val < 100 || 'Please type a real age'
]"
/>
<div>
<q-btn label="Submit" type="submit" color="primary" />
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</q-card>
</q-page>
</template>
<style lang="scss" scoped>
.card {
width: 100%;
max-width: 60em;
}
</style>

View File

@ -0,0 +1,115 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import SmartCard from 'src/components/SmartCard.vue';
import { toDate } from 'src/filters/index';
import ClaimSummary from './Card/ClaimSummary.vue';
const router = useRouter();
const { t } = useI18n();
const filter = {
include: [
{
relation: 'client',
},
{
relation: 'claimState',
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};
function stateColor(code) {
if (code === 'pending') return 'green';
if (code === 'managed') return 'orange';
if (code === 'resolved') return 'red';
}
function navigate(id) {
router.push({ path: `/claim/${id}` });
}
const preview = ref({
shown: false,
});
function showPreview(id) {
preview.value.shown = true;
preview.value.data = {
claimId: id,
};
}
</script>
<template>
<q-page class="q-pa-md">
<smart-card url="/Claims" :filter="filter" sort-by="id DESC" @on-navigate="navigate" auto-load>
<template #labels="{ row }">
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.list.customer') }}</q-item-label>
<q-item-label>{{ row.client.name }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.list.assignedTo') }}</q-item-label>
<q-item-label>{{ row.worker.user.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.list.created') }}</q-item-label>
<q-item-label>{{ toDate(row.created) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('claim.list.state') }}</q-item-label>
<q-item-label>
<q-chip :color="stateColor(row.claimState.code)" dense>
{{ row.claimState.description }}
</q-chip>
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
<template #actions="{ row }">
<q-btn color="grey-7" round flat icon="more_vert">
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
<q-menu cover auto-close>
<q-list>
<q-item clickable>
<q-item-section avatar>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add a note</q-item-section>
</q-item>
<q-item clickable>
<q-item-section avatar>
<q-icon name="logs" />
</q-item-section>
<q-item-section>Display claim logs</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn>
</template>
</smart-card>
</q-page>
<q-dialog v-model="preview.shown">
<claim-summary :claim-id="preview.data.claimId" />
</q-dialog>
</template>

View File

@ -0,0 +1,17 @@
<script setup>
import { useState } from 'src/composables/useState';
import LeftMenu from 'src/components/LeftMenu.vue';
const state = useState();
</script>
<template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit text-grey-8">
<LeftMenu />
</q-scroll-area>
</q-drawer>
<q-page-container>
<router-view></router-view>
</q-page-container>
</template>

View File

@ -0,0 +1,115 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import SmartCard from 'src/components/SmartCard.vue';
const quasar = useQuasar();
const { t } = useI18n();
const card = ref(null);
const newRma = ref({
code: '',
crated: new Date(),
});
function submit() {
const formData = newRma.value;
if (formData.code === '') return;
axios
.post('ClaimRmas', formData)
.then(() => {
newRma.value = {
code: '',
crated: new Date(),
};
})
.then(() => card.value.fetch());
}
const confirmShown = ref(false);
const rmaId = ref(null);
function confirm(id) {
confirmShown.value = true;
rmaId.value = id;
}
function remove() {
const id = rmaId.value;
axios.delete(`ClaimRmas/${id}`).then(() => {
confirmShown.value = false;
quasar.notify({
type: 'positive',
message: 'Entry deleted',
icon: 'check',
});
});
}
function hide() {
rmaId.value = null;
}
</script>
<template>
<q-page class="q-pa-md sticky">
<q-page-sticky expand position="top" :offset="[16, 16]">
<q-card class="card q-pa-md">
<q-form @submit="submit">
<q-input v-model="newRma.code" :label="t('claim.rmaList.newRma')" class="q-mb-md" />
<div class="text-caption">$(0) entries</div>
</q-form>
</q-card>
</q-page-sticky>
<smart-card ref="card" url="/ClaimRmas" sort-by="id DESC" auto-load>
<template #labels="{ row }">
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('claim.rmaList.code') }}</q-item-label>
<q-item-label>{{ row.code }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
<template #actions="{ row }">
<q-btn flat round color="primary" icon="vn:bin" @click="confirm(row.id)">
<q-tooltip>{{ t('globals.remove') }}</q-tooltip>
</q-btn>
</template>
</smart-card>
</q-page>
<q-dialog v-model="confirmShown" persistent @hide="hide">
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="warning" color="primary" text-color="white" />
<span class="q-ml-sm">{{ t('globals.confirmRemove') }}</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat :label="t('globals.no')" color="primary" v-close-popup autofocus />
<q-btn flat :label="t('globals.yes')" color="primary" @click="remove()" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<style lang="scss" scoped>
.sticky {
padding-top: 156px;
}
.card {
width: 100%;
max-width: 60em;
}
.q-page-sticky {
z-index: 2998;
}
</style>

View File

@ -1,5 +1,248 @@
<script setup>
import { ref, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import axios from 'axios';
import { useSession } from 'src/composables/useSession';
import { useValidator } from 'src/composables/useValidator';
import SkeletonForm from 'src/components/SkeletonForm.vue';
onMounted(() => {
fetch();
fetchWorkers();
fetchBusinessTypes();
fetchContactChannels();
});
const route = useRoute();
const quasar = useQuasar();
const { t } = useI18n();
const { validate } = useValidator();
const session = useSession();
const token = session.getToken();
const customer = ref(null);
const customerCopy = ref(null);
const hasChanges = ref(false);
function fetch() {
const id = route.params.id;
const filter = {
include: [],
};
const options = { params: { filter } };
axios.get(`Clients/${id}`, options).then(({ data }) => {
customer.value = data;
customerCopy.value = Object.assign({}, data);
watch(customer.value, () => (hasChanges.value = true));
});
}
const businessTypes = ref([]);
function fetchBusinessTypes() {
axios.get(`BusinessTypes`).then(({ data }) => {
businessTypes.value = data;
});
}
const contactChannels = ref([]);
function fetchContactChannels() {
axios.get(`ContactChannels`).then(({ data }) => {
contactChannels.value = data;
});
}
const workers = ref([]);
const workersCopy = ref([]);
function fetchWorkers() {
const filter = {
where: {
role: 'salesPerson',
},
};
const options = { params: { filter } };
axios.get(`Workers/activeWithRole`, options).then(({ data }) => {
workers.value = data;
workersCopy.value = data;
});
}
function filter(value, update, options, originalOptions, filter) {
update(
() => {
if (value === '') {
options.value = originalOptions.value;
return;
}
options.value = options.value.filter(filter);
},
(ref) => {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
);
}
function filterWorkers(value, update) {
const search = value.toLowerCase();
filter(value, update, workers, workersCopy, (row) => {
const id = row.id;
const name = row.name.toLowerCase();
const idMatch = id == search;
const nameMatch = name.indexOf(search) > -1;
return idMatch || nameMatch;
});
}
function save() {
const id = route.params.id;
const formData = customer.value;
if (!hasChanges.value) {
return quasar.notify({
type: 'negative',
message: t('globals.noChanges'),
});
}
axios.patch(`Clients/${id}`, formData).then((hasChanges.value = false));
}
function onReset() {
customer.value = customerCopy.value;
hasChanges.value = false;
}
</script>
<template>
<q-page class="q-pa-md">
<q-card class="q-pa-md">Basic Data</q-card>
<div class="container">
<q-card class="q-pa-md">
<skeleton-form v-if="!customer" />
<q-form v-if="customer" @submit="save" @reset="onReset" greedy>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="customer.socialName"
:label="t('customer.basicData.socialName')"
:rules="validate('client.socialName')"
autofocus
/>
</div>
<div class="col">
<q-select
v-model="customer.businessTypeFk"
:options="businessTypes"
option-value="code"
option-label="description"
emit-value
:label="t('customer.basicData.businessType')"
map-options
:rules="validate('client.businessTypeFk')"
:input-debounce="0"
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="customer.contact"
:label="t('customer.basicData.contact')"
:rules="validate('client.contact')"
clearable
/>
</div>
<div class="col">
<q-input
v-model="customer.email"
type="email"
:label="t('customer.basicData.email')"
:rules="validate('client.email')"
clearable
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-input
v-model="customer.phone"
:label="t('customer.basicData.phone')"
:rules="validate('client.phone')"
clearable
/>
</div>
<div class="col">
<q-input
v-model="customer.mobile"
:label="t('customer.basicData.mobile')"
:rules="validate('client.mobile')"
clearable
/>
</div>
</div>
<div class="row q-gutter-md q-mb-md">
<div class="col">
<q-select
v-model="customer.salesPersonFk"
:options="workers"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.salesPerson')"
map-options
use-input
@filter="filterWorkers"
:rules="validate('client.salesPersonFk')"
:input-debounce="0"
>
<template #before>
<q-avatar color="orange">
<q-img
v-if="customer.salesPersonFk"
:src="`/api/Images/user/160x160/${customer.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
/>
</q-avatar>
</template>
</q-select>
</div>
<div class="col">
<q-select
v-model="customer.contactChannelFk"
:options="contactChannels"
option-value="id"
option-label="name"
emit-value
:label="t('customer.basicData.contactChannel')"
map-options
:rules="validate('client.contactChannelFk')"
:input-debounce="0"
/>
</div>
</div>
<div>
<q-btn :label="t('globals.save')" type="submit" color="primary" />
<q-btn :label="t('globals.reset')" type="reset" class="q-ml-sm" color="primary" flat />
</div>
</q-form>
</q-card>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 800px;
}
</style>

View File

@ -1,23 +1,26 @@
<script setup>
import { computed, ref, onMounted } from 'vue';
import { useState } from 'src/composables/useState';
import { useRouter } from 'vue-router';
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useState } from 'src/composables/useState';
import { toCurrency } from 'src/filters';
const route = useRoute();
const state = useState();
const router = useRouter();
const { t } = useI18n();
onMounted(async () => {
await fetch();
});
const entityId = computed(function () {
return router.currentRoute.value.params.id;
});
const customer = ref({});
const customer = ref(null);
async function fetch() {
const { data } = await axios.get(`Clients/${entityId.value}`);
const entityId = route.params.id;
const { data } = await axios.get(`Clients/${entityId}/getCard`);
if (data) customer.value = data;
}
@ -25,78 +28,133 @@ async function fetch() {
<template>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit text-grey-8 descriptor">
<div class="header bg-orange q-pa-sm">
<div class="header bg-primary q-pa-sm">
<router-link :to="{ path: '/customer/list' }">
<q-btn round flat dense size="md" icon="view_list" color="white">
<q-tooltip>Customer list</q-tooltip>
<q-tooltip>{{ t('components.card.mainList') }}</q-tooltip>
</q-btn>
</router-link>
<router-link :to="{ path: '/customer/list' }">
<router-link :to="{ name: 'CustomerSummary', params: { id: route.params.id } }">
<q-btn round flat dense size="md" icon="launch" color="white">
<q-tooltip>Customer preview</q-tooltip>
<q-tooltip>{{ t('components.card.summary') }}</q-tooltip>
</q-btn>
</router-link>
<q-btn round flat dense size="md" icon="more_vert" color="white">
<q-tooltip>More options</q-tooltip>
<q-menu>
<q-tooltip>{{ t('components.card.moreOptions') }}</q-tooltip>
<!-- <q-menu>
<q-list>
<q-item clickable v-ripple>Option 1</q-item>
<q-item clickable v-ripple>Option 2</q-item>
</q-list>
</q-menu>
</q-menu> -->
</q-btn>
</div>
<h5>{{ customer.name }}</h5>
<div v-if="customer" class="q-py-sm">
<q-list>
<q-item-label header class="ellipsis text-h5" :lines="1">
{{ customer.name }}
<q-tooltip>{{ customer.name }}</q-tooltip>
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.card.customerId') }}</q-item-label>
<q-item-label>#{{ customer.id }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('customer.card.salesPerson') }}</q-item-label>
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.card.credit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.credit) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('customer.card.securedCredit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.card.payMethod') }}</q-item-label>
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('customer.card.debt') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.debt) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-card-actions class="q-gutter-md">
<q-icon v-if="customer.isActive == false" name="vn:disabled" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.isDisabled') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.isFreezed == true" name="vn:frozen" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.isFrozen') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.debt > customer.credit" name="vn:risk" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.hasDebt') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.isTaxDataChecked == false" name="vn:no036" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.notChecked') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.account.active == false" name="vn:noweb" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.noWebAccess') }}</q-tooltip>
</q-icon>
</q-card-actions>
<!-- <q-card-actions>
<q-btn size="md" icon="vn:ticket" color="primary">
<q-tooltip>Ticket list</q-tooltip>
</q-btn>
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>Customer ID</q-item-label>
<q-item-label>#{{ customer.id }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Email</q-item-label>
<q-item-label>{{ customer.email }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<q-btn size="md" icon="vn:invoice-out" color="primary">
<q-tooltip>Invoice Out list</q-tooltip>
</q-btn>
<q-card-actions>
<q-btn size="md" icon="vn:ticket" color="orange">
<q-tooltip>Ticket list</q-tooltip>
</q-btn>
<q-btn size="md" icon="vn:basketadd" color="primary">
<q-tooltip>Order list</q-tooltip>
</q-btn>
<q-btn size="md" icon="vn:invoice-out" color="orange">
<q-tooltip>Invoice Out list</q-tooltip>
</q-btn>
<q-btn size="md" icon="face" color="primary">
<q-tooltip>View user</q-tooltip>
</q-btn>
<q-btn size="md" icon="vn:basketadd" color="orange">
<q-tooltip>Order list</q-tooltip>
</q-btn>
<q-btn size="md" icon="expand_more" color="primary">
<q-tooltip>More options</q-tooltip>
</q-btn>
</q-card-actions> -->
</div>
<!-- Skeleton -->
<div id="descriptor-skeleton" v-if="!customer">
<div class="col q-pl-sm q-pa-sm">
<q-skeleton type="text" square height="45px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
</div>
<q-btn size="md" icon="face" color="orange">
<q-tooltip>View user</q-tooltip>
</q-btn>
<q-btn size="md" icon="expand_more" color="orange">
<q-tooltip>More options</q-tooltip>
</q-btn>
</q-card-actions>
<q-card-actions>
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
</q-card-actions>
</div>
<q-separator />
<q-list>
<q-item :to="{ name: 'CustomerBasicData' }" clickable v-ripple active-class="text-orange">
<q-item :to="{ name: 'CustomerBasicData' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="person" />
<q-icon name="vn:settings" />
</q-item-section>
<q-item-section>Basic data</q-item-section>
<q-item-section>{{ t('customer.pageTitles.basicData') }}</q-item-section>
</q-item>
<q-item clickable v-ripple>
<!-- <q-item clickable v-ripple>
<q-item-section avatar>
<q-icon name="notes" />
</q-item-section>
@ -111,7 +169,7 @@ async function fetch() {
<q-item-section>Option</q-item-section>
</q-item>
</q-list>
</q-expansion-item>
</q-expansion-item> -->
</q-list>
</q-scroll-area>
</q-drawer>

View File

@ -0,0 +1,494 @@
<script setup>
import { onMounted, defineProps, ref, computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { toCurrency, toPercentage, toDate } from 'src/filters';
import SkeletonSummary from 'src/components/SkeletonSummary';
onMounted(() => fetch());
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
customerId: {
type: Number,
default: 0,
},
});
const entityId = computed(() => $props.customerId || route.params.id);
const customer = ref(null);
function fetch() {
const id = entityId.value;
axios.get(`Clients/${id}/summary`).then(({ data }) => {
customer.value = data;
});
}
const balanceDue = computed(() => {
const [defaulter] = customer.value.defaulters;
return defaulter.amount;
});
const balanceDueWarning = computed(() => (balanceDue.value ? 'negative' : ''));
const claimRate = computed(() => {
const data = customer.value;
return data.claimsRatio.claimingRate * 100;
});
const priceIncreasingRate = computed(() => {
const data = customer.value;
return data.claimsRatio.priceIncreasing / 100;
});
const debtWarning = computed(() => {
const data = customer.value;
return data.debt.debt > data.credit ? 'negative' : '';
});
const creditWarning = computed(() => {
const data = customer.value;
const tooMuchInsurance = data.credit > data.creditInsurance;
const noCreditInsurance = data.credit && data.creditInsurance == null;
return tooMuchInsurance || noCreditInsurance ? 'negative' : '';
});
</script>
<template>
<q-page class="q-pa-md">
<div class="summary container">
<q-card>
<skeleton-summary v-if="!customer" />
<template v-if="customer">
<div class="header bg-primary q-pa-sm q-mb-md">{{ customer.id }} - {{ customer.name }}</div>
<div class="row q-pa-md q-col-gutter-md q-mb-md">
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.basicData') }}
<router-link :to="{ name: 'CustomerBasicData' }">
<q-icon name="open_in_new" />
</router-link>
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.customerId') }}</q-item-label>
<q-item-label>{{ customer.id }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.name') }}</q-item-label>
<q-item-label>{{ customer.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.contact') }}</q-item-label>
<q-item-label>{{ customer.contact }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.salesPerson') }}</q-item-label>
<q-item-label>{{ customer.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.phone') }}</q-item-label>
<q-item-label>{{ customer.phone }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.mobile') }}</q-item-label>
<q-item-label>{{ customer.mobile }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.email') }}</q-item-label>
<q-item-label>{{ customer.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.contactChannel') }}</q-item-label>
<q-item-label>{{ customer.contactChannel.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.fiscalAddress') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.socialName') }}</q-item-label>
<q-item-label>{{ customer.socialName }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.fiscalId') }}</q-item-label>
<q-item-label>{{ customer.fi }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.postcode') }}</q-item-label>
<q-item-label>{{ customer.postcode }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.province') }}</q-item-label>
<q-item-label>{{ customer.province.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.country') }}</q-item-label>
<q-item-label>{{ customer.country.country }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.street') }}</q-item-label>
<q-item-label>{{ customer.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.fiscalData') }}
</q-item-label>
<q-item dense>
<q-checkbox
v-model="customer.isEqualizated"
:label="t('customer.summary.isEqualizated')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isActive"
:label="t('customer.summary.isActive')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoiceByAddress"
:label="t('customer.summary.invoiceByAddress')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isTaxDataChecked"
:label="t('customer.summary.verifiedData')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasToInvoice"
:label="t('customer.summary.hasToInvoice')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.isToBeMailed"
:label="t('customer.summary.notifyByEmail')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox v-model="customer.isVies" :label="t('customer.summary.vies')" disable />
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.billingData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.payMethod') }}</q-item-label>
<q-item-label>{{ customer.payMethod.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.bankAccount') }}</q-item-label>
<q-item-label>{{ customer.iban }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.dueDay') }}</q-item-label>
<q-item-label>{{ customer.dueDay }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasLcr"
:label="t('customer.summary.hasLcr')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasCoreVnl"
:label="t('customer.summary.hasCoreVnl')"
disable
/>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.hasSepaVnl"
:label="t('customer.summary.hasB2BVnl')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.consignee') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressName') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.nickname }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressCity') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.city }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.addressStreet') }}</q-item-label>
<q-item-label>{{ customer.defaultAddress.street }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.webAccess') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.username') }}</q-item-label>
<q-item-label>{{ customer.account.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item dense>
<q-checkbox
v-model="customer.account.active"
:label="t('customer.summary.webAccess')"
disable
/>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.businessData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.totalGreuge') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.totalGreuge) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.mana">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.mana') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.mana.mana) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{
t('customer.summary.priceIncreasingRate')
}}</q-item-label>
<q-item-label>{{ toPercentage(priceIncreasingRate) }}</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="customer.averageInvoiced">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.averageInvoiced') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.averageInvoiced.invoiced) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.claimRate') }}</q-item-label>
<q-item-label>{{ toPercentage(claimRate) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<div class="col">
<q-list>
<q-item-label header class="text-h6">
{{ t('customer.summary.financialData') }}
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.risk') }}</q-item-label>
<q-item-label :class="debtWarning">
{{ toCurrency(customer.debt.debt) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.riskInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.credit') }}</q-item-label>
<q-item-label :class="creditWarning">
{{ toCurrency(customer.credit) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.creditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.securedCredit') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.creditInsurance) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.securedCreditInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balance') }}</q-item-label>
<q-item-label>{{ toCurrency(customer.sumRisk) || toCurrency(0) }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('customer.summary.balanceDue') }}</q-item-label>
<q-item-label :class="balanceDueWarning">
{{ toCurrency(balanceDue) }}
</q-item-label>
</q-item-section>
<q-item-section side>
<q-icon name="vn:info">
<q-tooltip>{{ t('customer.summary.balanceDueInfo') }}</q-tooltip>
</q-icon>
</q-item-section>
</q-item>
<q-item v-if="customer.recovery">
<q-item-section>
<q-item-label caption>{{ t('customer.summary.recoverySince') }}</q-item-label>
<q-item-label>{{ toDate(customer.recovery.started) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</template>
</q-card>
</div>
</q-page>
</template>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.q-card {
width: 100%;
max-width: 1200px;
}
.negative {
color: red;
}
.summary {
.q-list {
.q-item__label--header {
display: flex;
justify-content: space-between;
a {
color: $primary;
}
}
}
.row {
flex-wrap: wrap;
.col {
min-width: 250px;
}
}
.header {
text-align: center;
font-size: 18px;
}
#slider-container {
max-width: 80%;
margin: 0 auto;
.q-slider {
.q-slider__marker-labels:nth-child(1) {
transform: none;
}
.q-slider__marker-labels:nth-child(2) {
transform: none;
left: auto !important;
right: 0%;
}
}
}
}
</style>

View File

@ -1,7 +1,9 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import SmartCard from 'src/components/SmartCard.vue';
import CustomerSummary from './Card/CustomerSummary.vue';
const router = useRouter();
const { t } = useI18n();
@ -9,6 +11,17 @@ const { t } = useI18n();
function navigate(id) {
router.push({ path: `/customer/${id}` });
}
const preview = ref({
shown: false,
});
function showPreview(id) {
preview.value.shown = true;
preview.value.data = {
customerId: id,
};
}
</script>
<template>
@ -54,7 +67,7 @@ function navigate(id) {
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="preview">
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="vn:ticket">
@ -63,4 +76,7 @@ function navigate(id) {
</template>
</smart-card>
</q-page>
<q-dialog v-model="preview.shown">
<customer-summary :customer-id="preview.data.customerId" />
</q-dialog>
</template>

View File

@ -1,23 +1,12 @@
<script setup>
import { ref } from 'vue';
import { useState } from 'src/composables/useState';
//import LeftMenu from 'src/components/LeftMenu.vue';
const state = useState();
const miniState = ref(true);
</script>
<template>
<q-drawer
v-model="state.drawer.value"
show-if-above
:mini="miniState"
@mouseover="miniState = false"
@mouseout="miniState = true"
mini-to-overlay
:width="256"
:breakpoint="500"
>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit text-grey-8">
<LeftMenu />
</q-scroll-area>

View File

@ -1,27 +1,16 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref } from 'vue';
import { useState } from 'src/composables/useState';
import LeftMenu from 'src/components/LeftMenu.vue';
import { useNavigation } from 'src/composables/useNavigation';
const { t } = useI18n();
const state = useState();
const miniState = ref(true);
const modules = useNavigation();
</script>
<template>
<q-drawer
v-model="state.drawer.value"
show-if-above
:mini="miniState"
@mouseover="miniState = false"
@mouseout="miniState = true"
mini-to-overlay
:width="256"
:breakpoint="500"
>
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit text-grey-8">
<LeftMenu />
</q-scroll-area>
@ -55,7 +44,7 @@ const modules = useNavigation();
class="col-4 button"
:to="{ name: module.stateName }"
>
<div class="text-center text-orange-6 button-text">
<div class="text-center text-primary button-text">
{{ t(`${module.name}.pageTitles.${module.title}`) }}
</div>
</q-btn>

View File

@ -1,24 +1,185 @@
<script setup>
//import { computed } from 'vue';
//import { useState } from 'src/composables/useState';
//import { useRouter } from 'vue-router';
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
//const state = useState();
//const router = useRouter();
/*const entityId = computed(function () {
return router.currentRoute.value.params.id;
});*/
import { useState } from 'src/composables/useState';
import { toDate } from 'src/filters';
const route = useRoute();
const state = useState();
const { t } = useI18n();
onMounted(async () => {
await fetch();
});
const ticket = ref(null);
async function fetch() {
const entityId = route.params.id;
const { data } = await axios.get(`Tickets/${entityId}/summary`);
if (data) ticket.value = data;
}
function stateColor(state) {
if (state.code === 'OK') return 'text-green';
if (state.code === 'FREE') return 'text-blue-3';
if (state.alertLevel === 1) return 'text-primary';
if (state.alertLevel === 0) return 'text-red';
}
</script>
<template>
<!--<q-drawer v-model="state.drawer.value" show-if-above :width="200" :breakpoint="500">
<q-scroll-area class="fit text-grey-8">
<router-link :to="{ path: '/customer/list' }">
<q-icon name="arrow_back" size="md" color="primary" />
</router-link>
<div>Customer ID: {{ entityId }}</div>
</q-scroll-area>
</q-drawer>-->
<q-drawer v-model="state.drawer.value" show-if-above :width="256" :breakpoint="500">
<q-scroll-area class="fit text-grey-8 descriptor">
<div class="header bg-primary q-pa-sm">
<router-link :to="{ path: '/ticket/list' }">
<q-btn round flat dense size="md" icon="view_list" color="white">
<q-tooltip>{{ t('components.card.mainList') }}</q-tooltip>
</q-btn>
</router-link>
<router-link :to="{ name: 'TicketSummary', params: { id: route.params.id } }">
<q-btn round flat dense size="md" icon="launch" color="white">
<q-tooltip>{{ t('components.card.summary') }}</q-tooltip>
</q-btn>
</router-link>
<q-btn round flat dense size="md" icon="more_vert" color="white">
<q-tooltip>{{ t('components.card.moreOptions') }}</q-tooltip>
<!-- <q-menu>
<q-list>
<q-item clickable v-ripple>Option 1</q-item>
<q-item clickable v-ripple>Option 2</q-item>
</q-list>
</q-menu> -->
</q-btn>
</div>
<div v-if="ticket" class="q-py-sm">
<q-list>
<q-item-label header class="ellipsis text-h5" :lines="1">
{{ ticket.nickname }}
<q-tooltip>{{ ticket.nickname }}</q-tooltip>
</q-item-label>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.card.ticketId') }}</q-item-label>
<q-item-label>#{{ ticket.id }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('ticket.card.state') }}</q-item-label>
<q-item-label :class="stateColor(ticket.ticketState.state)">
{{ ticket.ticketState.state.name }}
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.card.customerId') }}</q-item-label>
<q-item-label>{{ ticket.clientFk }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('ticket.card.salesPerson') }}</q-item-label>
<q-item-label>{{ ticket.client.salesPersonUser.name }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.card.agency') }}</q-item-label>
<q-item-label>{{ ticket.agencyMode.name }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('ticket.card.shipped') }}</q-item-label>
<q-item-label>{{ toDate(ticket.shipped) }}</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>{{ t('ticket.card.warehouse') }}</q-item-label>
<q-item-label>{{ ticket.warehouse.name }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<!-- <q-card-actions class="q-gutter-md">
<q-icon v-if="customer.isActive == false" name="vn:disabled" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.isDisabled') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.isFreezed == true" name="vn:frozen" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.isFrozen') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.debt > customer.credit" name="vn:risk" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.hasDebt') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.isTaxDataChecked == false" name="vn:no036" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.notChecked') }}</q-tooltip>
</q-icon>
<q-icon v-if="customer.account.active == false" name="vn:noweb" size="xs" color="primary">
<q-tooltip>{{ t('customer.card.noWebAccess') }}</q-tooltip>
</q-icon>
</q-card-actions> -->
<q-card-actions>
<q-btn size="md" icon="vn:client" color="primary">
<q-tooltip>{{ t('ticket.card.customerCard') }}</q-tooltip>
</q-btn>
</q-card-actions>
</div>
<!-- Skeleton -->
<div id="descriptor-skeleton" v-if="!ticket">
<div class="col q-pl-sm q-pa-sm">
<q-skeleton type="text" square height="45px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
<q-skeleton type="text" square height="18px" />
</div>
<q-card-actions>
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
<q-skeleton size="40px" />
</q-card-actions>
</div>
<q-separator />
<q-list>
<!-- <q-item :to="{ name: 'TicketBasicData' }" clickable v-ripple>
<q-item-section avatar>
<q-icon name="vn:settings" />
</q-item-section>
<q-item-section>{{ t('ticket.pageTitles.basicData') }}</q-item-section>
</q-item> -->
</q-list>
</q-scroll-area> </q-drawer
>-->
<q-page-container>
<router-view></router-view>
</q-page-container>
</template>
<style lang="scss">
.q-scrollarea__content {
max-width: 100%;
}
</style>
<style lang="scss" scoped>
.descriptor {
h5 {
margin: 0 15px;
}
.header {
display: flex;
justify-content: space-between;
}
.q-card__actions {
justify-content: center;
}
}
</style>

View File

View File

@ -1,110 +1,140 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
const router = useRouter();
import SmartCard from 'src/components/SmartCard.vue';
import { toDate, toCurrency } from 'src/filters/index';
// import CustomerSummary from './Card/CustomerSummary.vue';
const customers = [
{
id: 1101,
name: 'Bruce Wayne',
username: 'batman',
email: 'batman@gotham',
phone: '555-555-5555',
expanded: ref(false),
},
{
id: 1102,
name: 'James Gordon',
username: 'jamesgordon',
email: 'jamesgordon@gotham',
phone: '555-555-1111',
expanded: ref(false),
},
];
const router = useRouter();
const { t } = useI18n();
const filter = {
include: [
{
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['name'],
},
},
},
},
{
relation: 'ticketState',
scope: {
fields: ['stateFk', 'code', 'alertLevel'],
include: {
relation: 'state',
scope: {
fields: ['name'],
},
},
},
},
],
};
function stateColor(state) {
if (state.code === 'OK') return 'green';
if (state.code === 'FREE') return 'blue-3';
if (state.alertLevel === 1) return 'orange';
if (state.alertLevel === 0) return 'red';
}
function navigate(id) {
router.push({ path: `/customer/${id}` });
router.push({ path: `/ticket/${id}` });
}
const preview = ref({
shown: false,
});
function showPreview(id) {
preview.value.shown = true;
preview.value.data = {
customerId: id,
};
}
</script>
<template>
<q-page class="q-pa-md">
<div class="column items-center q-gutter-y-md">
<q-card v-for="customer in customers" :key="customer.id" class="card">
<!-- v-ripple :to="{ path: '/dashboard' }" -->
<q-item v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
<q-item-section class="q-pa-md">
<div class="text-h6">{{ customer.name }}</div>
<q-item-label caption>@{{ customer.username }}</q-item-label>
<div class="q-mt-md">
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>Email</q-item-label>
<q-item-label>{{ customer.email }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>Phone</q-item-label>
<q-item-label>{{ customer.phone }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
</q-item-section>
<q-btn color="grey-7" round flat icon="more_vert">
<q-menu cover auto-close>
<q-list>
<q-item clickable>
<q-item-section>Action 1</q-item-section>
</q-item>
<q-item clickable>
<q-item-section>Action 2</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<q-separator vertical />
<q-card-actions vertical class="justify-between">
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(customer.id)" />
<q-btn flat round color="accent" icon="preview" />
<q-btn flat round color="accent" icon="vn:ticket" />
<q-card-actions>
<q-btn
color="grey"
round
flat
dense
:icon="customer.expanded.value ? 'keyboard_arrow_up' : 'keyboard_arrow_down'"
@click="customer.expanded.value = !customer.expanded.value"
/>
</q-card-actions>
</q-card-actions>
</q-item>
<q-slide-transition>
<div v-show="customer.expanded.value">
<q-separator />
<q-card-section class="text-subitle2">
<q-list>
<q-item clickable>
<q-item-section>
<q-item-label>Address</q-item-label>
<q-item-label caption>Avenue 11</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
</div>
</q-slide-transition>
</q-card>
</div>
</q-page>
</template>
<smart-card url="/Tickets" :filter="filter" sort-by="id DESC" @on-navigate="navigate" auto-load>
<template #labels="{ row }">
<q-list>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('ticket.list.nickname') }}</q-item-label>
<q-item-label>{{ row.nickname }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('ticket.list.state') }}</q-item-label>
<q-item-label>
<q-chip :color="stateColor(row.ticketState)" dense>
{{ row.ticketState.state.name }}
</q-chip>
</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section>
<q-item-label caption>{{ t('ticket.list.shipped') }}</q-item-label>
<q-item-label>{{ toDate(row.shipped) }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('ticket.list.landed') }}</q-item-label>
<q-item-label>{{ toDate(row.landed) }}</q-item-label>
</q-item-section>
</q-item>
<q-item class="q-pa-none">
<q-item-section v-if="row.client.salesPersonUser">
<q-item-label caption>{{ t('ticket.list.salesPerson') }}</q-item-label>
<q-item-label>{{ row.client.salesPersonUser.name }}</q-item-label>
</q-item-section>
<q-item-section>
<q-item-label caption>{{ t('ticket.list.total') }}</q-item-label>
<q-item-label>{{ toCurrency(row.totalWithVat) }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</template>
<template #actions="{ row }">
<q-btn color="grey-7" round flat icon="more_vert">
<q-tooltip>{{ t('customer.list.moreOptions') }}</q-tooltip>
<q-menu cover auto-close>
<q-list>
<q-item clickable>
<q-item-section avatar>
<q-icon name="add" />
</q-item-section>
<q-item-section>Add a note</q-item-section>
</q-item>
<q-item clickable>
<q-item-section avatar>
<q-icon name="history" />
</q-item-section>
<q-item-section>Display customer history</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
<style lang="scss" scoped>
.card {
width: 100%;
max-width: 60em;
}
</style>
<q-btn flat round color="orange" icon="arrow_circle_right" @click="navigate(row.id)">
<q-tooltip>{{ t('components.smartCard.openCard') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="preview" @click="showPreview(row.id)">
<q-tooltip>{{ t('components.smartCard.openSummary') }}</q-tooltip>
</q-btn>
<q-btn flat round color="grey-7" icon="vn:ticket">
<q-tooltip>{{ t('customer.list.customerOrders') }}</q-tooltip>
</q-btn>
</template>
</smart-card>
</q-page>
<!-- <q-dialog v-model="preview.shown">
<customer-summary :customer-id="preview.data.customerId" />
</q-dialog> -->
</template>

View File

@ -0,0 +1,85 @@
import { RouterView } from 'vue-router';
export default {
name: 'Claim',
path: '/claim',
meta: {
title: 'claims',
icon: 'vn:claims'
},
component: RouterView,
redirect: { name: 'ClaimMain' },
children: [
{
name: 'ClaimMain',
path: '',
component: () => import('src/pages/Claim/ClaimMain.vue'),
redirect: { name: 'ClaimList' },
children: [
{
name: 'ClaimList',
path: 'list',
meta: {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/Claim/ClaimList.vue'),
},
{
name: 'ClaimRmaList',
path: 'rma',
meta: {
title: 'rmaList',
icon: 'vn:barcode',
roles: ['claimManager']
},
component: () => import('src/pages/Claim/ClaimRmaList.vue'),
},
{
name: 'ClaimCreate',
path: 'create',
meta: {
title: 'createClaim',
icon: 'vn:addperson',
},
component: () => import('src/pages/Claim/ClaimCreate.vue'),
}
]
},
{
name: 'ClaimCard',
path: ':id',
component: () => import('src/pages/Claim/Card/ClaimCard.vue'),
redirect: { name: 'ClaimSummary' },
children: [
{
name: 'ClaimSummary',
path: 'summary',
meta: {
title: 'summary'
},
component: () => import('src/pages/Claim/Card/ClaimSummary.vue'),
},
{
name: 'ClaimBasicData',
path: 'basic-data',
meta: {
title: 'basicData',
roles: ['salesPerson']
},
component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'),
},
{
name: 'ClaimRma',
path: 'rma',
meta: {
title: 'rma',
roles: ['claimManager']
},
component: () => import('src/pages/Claim/Card/ClaimRma.vue'),
props: { claim: true }
}
]
},
]
};

View File

@ -4,7 +4,6 @@ export default {
path: '/customer',
name: 'Customer',
meta: {
roles: ['developer'],
title: 'customers',
icon: 'vn:client'
},
@ -38,10 +37,19 @@ export default {
]
},
{
name: 'CustomerCard',
path: ':id',
component: () => import('src/pages/Customer/Card/CustomerCard.vue'),
redirect: { name: 'CustomerBasicData' },
redirect: { name: 'CustomerSummary' },
children: [
{
name: 'CustomerSummary',
path: 'summary',
meta: {
title: 'summary'
},
component: () => import('src/pages/Customer/Card/CustomerSummary.vue'),
},
{
path: 'basic-data',
name: 'CustomerBasicData',

View File

@ -1,10 +1,9 @@
import { RouterView } from 'vue-router';
export default {
path: '/ticket',
name: 'Ticket',
path: '/ticket',
meta: {
roles: ['developer'],
title: 'tickets',
icon: 'vn:ticket'
},
@ -12,14 +11,14 @@ export default {
redirect: { name: 'TicketMain' },
children: [
{
path: '',
name: 'TicketMain',
path: '',
component: () => import('src/pages/Ticket/TicketMain.vue'),
redirect: { name: 'TicketList' },
children: [
{
path: 'list',
name: 'TicketList',
path: 'list',
meta: {
title: 'list',
icon: 'view_list',
@ -27,8 +26,8 @@ export default {
component: () => import('src/pages/Ticket/TicketList.vue'),
},
{
path: 'create',
name: 'TicketCreate',
path: 'create',
meta: {
title: 'createTicket',
icon: 'vn:ticketAdd',
@ -40,13 +39,22 @@ export default {
]
},
{
name: 'TicketCard',
path: ':id',
component: () => import('src/pages/Ticket/Card/TicketCard.vue'),
redirect: { name: 'TicketBasicData' },
redirect: { name: 'TicketSummary' },
children: [
{
path: 'basic-data',
name: 'TicketSummary',
path: 'summary',
meta: {
title: 'summary'
},
component: () => import('src/pages/Ticket/Card/TicketSummary.vue'),
},
{
name: 'TicketBasicData',
path: 'basic-data',
meta: {
title: 'basicData'
},

View File

@ -1,5 +1,6 @@
import customer from './modules/customer';
import ticket from './modules/ticket';
import claim from './modules/claim';
const routes = [
{
@ -23,6 +24,7 @@ const routes = [
// Module routes
customer,
ticket,
claim,
],
},
{