salix-front/src/components/common/VnLog.vue

1072 lines
37 KiB
Vue
Raw Normal View History

<script setup>
2023-09-18 12:35:21 +00:00
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
2023-09-18 12:35:21 +00:00
import axios from 'axios';
import { date } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toRelativeDate } from 'src/filters';
2023-09-18 12:35:21 +00:00
import { useColor } from 'src/composables/useColor';
import { useFirstUpper } from 'src/composables/useFirstUpper';
2023-09-28 11:45:14 +00:00
import { useValidator } from 'src/composables/useValidator';
2023-09-18 12:35:21 +00:00
import VnAvatar from '../ui/VnAvatar.vue';
import VnJsonValue from '../common/VnJsonValue.vue';
import FetchData from '../FetchData.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
2023-10-13 10:26:37 +00:00
import VnSelectFilter from './VnSelectFilter.vue';
const stateStore = useStateStore();
2023-09-28 11:45:14 +00:00
const validationsStore = useValidator();
const { models } = validationsStore;
const route = useRoute();
const { t } = useI18n();
const props = defineProps({
model: {
type: String,
default: null,
},
});
2023-09-18 12:35:21 +00:00
const filter = {
fields: [
'id',
'originFk',
'userFk',
'action',
'changedModel',
'oldInstance',
'newInstance',
'creationDate',
'changedModel',
'changedModelId',
'changedModelValue',
'description',
],
include: [
{
relation: 'user',
scope: {
fields: ['nickname', 'name', 'image'],
include: {
relation: 'worker',
scope: {
fields: ['id'],
},
},
},
},
],
};
2023-09-18 12:35:21 +00:00
const workers = ref();
const actions = ref();
const changeInput = ref();
const searchInput = ref();
const userRadio = ref();
const userSelect = ref();
const dateFrom = ref();
const dateFromDialog = ref(false);
2023-09-18 12:35:21 +00:00
const dateTo = ref();
const dateToDialog = ref(false);
const selectedFilters = ref({});
const userTypes = [
{ label: 'All', value: undefined },
{ label: 'User', value: { neq: null } },
{ label: 'System', value: null },
];
const checkboxOptions = ref({
insert: {
label: 'Creates',
selected: false,
},
2023-09-18 12:35:21 +00:00
update: {
label: 'Edits',
selected: false,
},
2023-09-18 12:35:21 +00:00
delete: {
label: 'Deletes',
selected: false,
},
2023-09-18 12:35:21 +00:00
select: {
label: 'Accesses',
selected: false,
},
});
2023-09-28 11:45:14 +00:00
let validations = models;
2023-09-18 12:35:21 +00:00
let pointRecord = ref(null);
let byRecord = ref(false);
const logTree = ref([]);
2023-09-18 12:35:21 +00:00
const actionsText = {
insert: 'Creates',
update: 'Edits',
delete: 'Deletes',
select: 'Accesses',
};
const actionsClass = {
insert: 'success',
update: 'warning',
delete: 'alert',
select: 'notice',
};
const actionsIcon = {
insert: 'add',
update: 'edit',
delete: 'remove',
select: 'visibility',
};
const validDate = new RegExp(
/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])/.source +
/T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/.source
);
function castJsonValue(value) {
return typeof value === 'string' && validDate.test(value) ? new Date(value) : value;
}
function parseProps(propNames, locale, vals, olds) {
const props = [];
for (const prop of propNames) {
if (prop.endsWith('$')) continue;
props.push({
name: prop,
nameI18n: useFirstUpper(locale.columns?.[prop]) || prop,
val: getVal(vals, prop),
old: olds && getVal(olds, prop),
});
}
2023-09-18 12:35:21 +00:00
props.sort((a, b) => a.nameI18n.localeCompare(b.nameI18n));
function getVal(vals, prop) {
let val;
let id;
const showProp = `${prop}$`;
if (vals[showProp] != null) {
val = vals[showProp];
id = vals[prop];
} else val = vals[prop];
2023-09-18 12:35:21 +00:00
return { val: castJsonValue(val), id };
}
2023-09-18 12:35:21 +00:00
return props;
}
function getLogs(data) {
const logs = [];
let originLog = null;
let userLog = null;
let modelLog = null;
let prevLog;
2023-09-18 12:35:21 +00:00
let nLogs;
2023-10-04 13:52:40 +00:00
2023-09-18 12:35:21 +00:00
data.forEach((log) => {
const locale = validations[log.changedModel]?.locale || {};
2023-09-18 12:35:21 +00:00
// Origin
const originChanged = !prevLog || log.originFk != prevLog.originFk;
if (originChanged) {
2023-09-18 12:35:21 +00:00
logs.push((originLog = { originFk: log.originFk, logs: [] }));
prevLog = log;
}
// User
const userChanged = originChanged || log.userFk != prevLog.userFk;
if (userChanged) {
2023-09-18 12:35:21 +00:00
originLog.logs.push(
(userLog = {
user: log.user,
userFk: log.userFk,
logs: [],
})
);
}
// Model
const modelChanged =
userChanged ||
log.changedModel != prevLog.changedModel ||
log.changedModelId != prevLog.changedModelId ||
nLogs >= 6;
if (modelChanged) {
2023-09-18 12:35:21 +00:00
userLog.logs.push(
(modelLog = {
model: log.changedModel,
modelI18n: useFirstUpper(locale.name) || log.changedModel,
id: log.changedModelId,
showValue: log.changedModelValue,
logs: [],
})
);
nLogs = 0;
}
nLogs++;
modelLog.logs.push(log);
// Changes
const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || {};
const vals = (notDelete ? log.newInstance : log.oldInstance) || {};
let propNames = Object.keys(olds).concat(Object.keys(vals));
propNames = [...new Set(propNames)];
log.props = parseProps(propNames, locale, vals, olds);
});
return logs;
}
async function openPointRecord(id, modelLog) {
pointRecord.value = null;
const { data } = await axios.get(`${props.model}Logs/${id}/pitInstance`);
const propNames = Object.keys(data);
const locale = validations[modelLog.model]?.locale || {};
2023-09-18 12:35:21 +00:00
pointRecord.value = parseProps(propNames, locale, data);
}
async function setLogTree() {
filter.where = { and: [{ originFk: route.params.id }] };
const { data } = await axios.get(`${props.model}Logs`, {
params: { filter: JSON.stringify(filter) },
});
logTree.value = getLogs(data);
}
function filterByRecord(modelLog) {
byRecord.value = true;
const { id, model } = modelLog;
2023-09-18 12:35:21 +00:00
searchInput.value = id;
selectedFilters.value.changedModelId = id;
selectedFilters.value.changedModel = model;
applyFilter();
}
2023-09-18 12:35:21 +00:00
async function applyFilter() {
filter.where = { and: [] };
if (
!selectedFilters.value.changedModel ||
(!selectedFilters.value.changedModelValue &&
!selectedFilters.value.changedModelId)
)
byRecord.value = false;
if (!byRecord.value) filter.where.and.push({ originFk: route.params.id });
if (Object.keys(selectedFilters.value).length) {
filter.where.and.push(selectedFilters.value);
}
const { data } = await axios.get(`${props.model}Logs`, {
params: { filter: JSON.stringify(filter) },
});
logTree.value = getLogs(data);
}
function setDate(type) {
let from = dateFrom.value
? date.formatDate(dateFrom.value.split('-').reverse().join('-'), 'YYYY-MM-DD')
2023-09-18 12:35:21 +00:00
: undefined;
from = date.adjustDate(from, { hour: 0, minute: 0, second: 0, millisecond: 0 }, true);
let to = dateTo.value
? date.formatDate(dateTo.value.split('-').reverse().join('-'), 'YYYY-MM-DD')
: date.formatDate(dateFrom.value.split('-').reverse().join('-'), 'YYYY-MM-DD');
to = date.adjustDate(
to,
{ hour: 21, minute: 59, second: 59, millisecond: 999 },
true
);
2023-09-18 12:35:21 +00:00
switch (type) {
case 'from':
return { between: [from, to] };
case 'to': {
if (dateFrom.value) {
2023-09-18 12:35:21 +00:00
return {
between: [from, to],
};
} else {
return { lte: to };
}
}
}
}
function selectFilter(type, dateType) {
const filter = {};
const actions = { inq: [] };
let reload = true;
if (type === 'search') {
if (/^\s*[0-9]+\s*$/.test(searchInput.value) || props.byRecord) {
selectedFilters.value.changedModelId = searchInput.value.trim();
} else if (!searchInput.value) {
selectedFilters.value.changedModelId = undefined;
selectedFilters.value.changedModelValue = undefined;
} else {
selectedFilters.value.changedModelValue = { like: `%${searchInput.value}%` };
}
}
if (type === 'action' && selectedFilters.value.changedModel === null) {
selectedFilters.value.changedModel = undefined;
reload = false;
}
if (type === 'userRadio') {
selectedFilters.value.userFk = userRadio.value;
}
if (type === 'change') {
if (changeInput.value)
selectedFilters.value.or = [
{ oldJson: { like: `%${changeInput.value}%` } },
{ newJson: { like: `%${changeInput.value}%` } },
{ description: { like: `%${changeInput.value}%` } },
];
else selectedFilters.value.or = undefined;
}
if (type === 'userSelect') {
selectedFilters.value.userFk =
userSelect.value !== null ? userSelect.value : undefined;
}
if (type === 'date') {
if (!dateFrom.value && !dateTo.value) {
2023-09-18 12:35:21 +00:00
selectedFilters.value.creationDate = undefined;
} else if (dateType === 'to') {
selectedFilters.value.creationDate = setDate('to');
} else if (dateType === 'from') {
selectedFilters.value.creationDate = setDate('from');
}
}
Object.keys(checkboxOptions.value).forEach((key) => {
if (checkboxOptions.value[key].selected) actions.inq.push(key);
});
selectedFilters.value.action = actions.inq.length ? actions : undefined;
Object.keys(selectedFilters.value).forEach((key) => {
if (selectedFilters.value[key]) filter[key] = selectedFilters.value[key];
});
if (reload) applyFilter(filter);
}
async function clearFilter() {
selectedFilters.value = {};
byRecord.value = false;
userSelect.value = undefined;
searchInput.value = undefined;
changeInput.value = undefined;
dateFrom.value = undefined;
2023-09-18 12:35:21 +00:00
dateTo.value = undefined;
2023-10-13 10:26:37 +00:00
userRadio.value = undefined;
2023-09-18 12:35:21 +00:00
Object.keys(checkboxOptions.value).forEach(
(opt) => (checkboxOptions.value[opt].selected = false)
);
await applyFilter();
}
setLogTree();
</script>
<template>
2023-09-18 12:35:21 +00:00
<FetchData
:url="`${props.model}Logs/${route.params.id}/editors`"
:filter="{
fields: ['id', 'nickname', 'name', 'image'],
order: 'nickname',
limit: 30,
}"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
:url="`${props.model}Logs/${route.params.id}/models`"
:filter="{ order: ['changedModel'] }"
2023-10-13 10:26:37 +00:00
@on-fetch="
(data) =>
(actions = data.map((item) => {
return {
locale: useFirstUpper(validations[item.changedModel].locale.name),
value: item.changedModel,
};
}))
"
2023-09-18 12:35:21 +00:00
auto-load
/>
<div
class="column items-center logs origin-log"
v-for="(originLog, originLogIndex) in logTree"
:key="originLogIndex"
>
<QItem class="origin-info items-center q-my-md" v-if="logTree.length > 1">
<h6 class="origin-id text-grey">
{{ useFirstUpper(validations[props.model].locale.name) }}
2023-09-18 12:35:21 +00:00
#{{ originLog.originFk }}
</h6>
<div class="line bg-grey"></div>
2023-09-18 12:35:21 +00:00
</QItem>
<div
class="user-log q-mb-sm row"
v-for="(userLog, userIndex) in originLog.logs"
:key="userIndex"
>
<div class="timeline">
<div class="user-avatar">
<VnAvatar
class="cursor-pointer"
:worker="userLog.user.id"
:title="userLog.user.nickname"
/>
<WorkerDescriptorProxy
v-if="userLog.user.image"
:id="userLog.user.id"
/>
</div>
<div class="arrow bg-panel" v-if="byRecord"></div>
<div class="line"></div>
</div>
<QList class="user-changes" v-if="userLog">
<QItem
class="model-log column q-px-none q-py-xs"
v-for="(modelLog, modelLogIndex) in userLog.logs"
:key="modelLogIndex"
>
<QItemSection>
<QItemLabel class="model-info q-mb-xs" v-if="!byRecord">
<QChip
dense
2023-09-18 12:35:21 +00:00
size="md"
class="model-name q-mr-xs text-white"
v-if="
!(modelLog.changedModel && modelLog.changedModelId) &&
modelLog.model
"
:style="{
backgroundColor: useColor(modelLog.model),
}"
:title="modelLog.model"
>
{{ t(modelLog.modelI18n) }}
</QChip>
<span class="model-id" v-if="modelLog.id"
>#{{ modelLog.id }}</span
>
<span class="model-value" :title="modelLog.showValue">
{{ modelLog.showValue }}
</span>
<QBtn
flat
2023-09-18 12:35:21 +00:00
round
color="grey"
class="q-mr-xs q-ml-auto"
size="sm"
icon="filter_alt"
:title="t('recordChanges')"
@click.stop="filterByRecord(modelLog)"
/>
</QItemLabel>
</QItemSection>
<QItemSection>
<QCard
class="changes-log q-py-none"
v-for="(log, logIndex) in modelLog.logs"
:key="logIndex"
>
<QCardSection class="change-info q-pa-none">
<QItem
class="q-px-sm q-py-xs justify-between items-center"
>
<div
class="date text-grey text-caption q-mr-sm"
:title="
date.formatDate(
log.creationDate,
'DD/MM/YYYY hh:mm:ss'
) ?? `date:'dd/MM/yyyy HH:mm:ss'`
2023-09-18 12:35:21 +00:00
"
>
{{ toRelativeDate(log.creationDate) }}
</div>
<div>
<QBtn
color="grey"
class="pit"
icon="preview"
flat
round
:title="t('pointRecord')"
padding="none"
2023-09-18 12:35:21 +00:00
v-if="log.action != 'insert'"
@click.stop="
openPointRecord(log.id, modelLog)
"
>
<QPopupProxy>
<QCard v-if="pointRecord">
<div
class="header q-px-sm q-py-xs q-ma-none text-white text-bold bg-primary"
>
{{ modelLog.modelI18n }}
<span v-if="modelLog.id"
>#{{ modelLog.id }}</span
>
</div>
<QCardSection
class="change-detail q-pa-sm"
>
<QItem
v-for="(
value, index
) in pointRecord"
:key="index"
class="q-pa-none"
>
<span
class="json-field q-mr-xs text-grey"
:title="value.name"
>
{{ value.nameI18n }}:
</span>
<VnJsonValue
:value="value.val.val"
/>
</QItem>
</QCardSection>
</QCard>
</QPopupProxy>
</QBtn>
<QIcon
class="action q-ml-xs"
:class="actionsClass[log.action]"
:name="actionsIcon[log.action]"
:title="
t(`actions.${actionsText[log.action]}`)
"
/>
</div>
</QItem>
</QCardSection>
<QCardSection
class="change-detail q-px-sm q-py-xs"
:class="{ expanded: log.expand }"
v-if="log.props.length || log.description"
>
2023-09-18 12:35:21 +00:00
<QIcon
class="cursor-pointer q-mr-md"
color="grey"
name="expand_more"
:title="t('globals.details')"
size="sm"
@click="log.expand = !log.expand"
/>
<span v-if="log.props.length" class="attributes">
<span v-if="!log.expand" class="q-pa-none text-grey">
2023-09-18 12:35:21 +00:00
<span
v-for="(prop, propIndex) in log.props"
:key="propIndex"
class="basic-json"
>
<span class="json-field" :title="prop.name">
{{ prop.nameI18n }}:
</span>
<VnJsonValue :value="prop.val.val" />
<span v-if="propIndex < log.props.length - 1"
>,&nbsp;
2023-09-18 12:35:21 +00:00
</span>
</span>
</span>
<span
2023-09-18 12:35:21 +00:00
v-if="log.expand"
class="expanded-json column q-pa-none"
>
<div
v-for="(prop, prop2Index) in log.props"
:key="prop2Index"
class="q-pa-none text-grey"
>
2023-09-18 12:35:21 +00:00
<span class="json-field" :title="prop.name">
{{ prop.nameI18n }}:
</span>
<VnJsonValue :value="prop.val.val" />
<span v-if="prop.val.id" class="id-value">
#{{ prop.val.id }}
</span>
<span v-if="log.action == 'update'">
<VnJsonValue :value="prop.old.val" />
<span v-if="prop.old.id" class="id-value">
#{{ prop.old.id }}
</span>
</span>
</div>
</span>
</span>
2023-09-18 12:35:21 +00:00
<span v-if="!log.props.length" class="description">
{{ log.description }}
</span>
</QCardSection>
</QCard>
</QItemSection>
</QItem>
</QList>
</div>
</div>
<Teleport v-if="stateStore.isHeaderMounted()" to="#actions-append">
<div class="row q-gutter-x-sm">
2023-09-18 12:35:21 +00:00
<QBtn
flat
@click.stop="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
<QDrawer v-model="stateStore.rightDrawer" show-if-above side="right" :width="300">
<QScrollArea class="fit text-grey-8">
2023-09-18 12:35:21 +00:00
<QList dense>
<QSeparator />
<QItem class="q-mt-sm">
<QInput
:label="t('globals.search')"
v-model="searchInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="() => selectFilter('search')"
@focusout="() => selectFilter('search')"
@clear="() => selectFilter('search')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{ t('tooltips.search') }}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
2023-10-13 10:26:37 +00:00
<QItem>
<VnSelectFilter
2023-09-18 12:35:21 +00:00
class="full-width"
2023-10-13 10:26:37 +00:00
:label="t('globals.entity')"
2023-09-18 12:35:21 +00:00
v-model="selectedFilters.changedModel"
2023-10-13 10:26:37 +00:00
option-label="locale"
:options="actions"
2023-09-18 12:35:21 +00:00
@update:model-value="selectFilter('action')"
@clear="() => selectFilter('action')"
2023-10-13 10:26:37 +00:00
/>
2023-09-18 12:35:21 +00:00
</QItem>
<QItem class="q-mt-sm">
<QOptionGroup
size="sm"
v-model="userRadio"
:options="userTypes"
color="primary"
@update:model-value="selectFilter('userRadio')"
right-label
>
<template #label="{ label }">
{{ t(`Users.${label}`) }}
</template>
</QOptionGroup>
</QItem>
<QItem class="q-mt-sm">
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
2023-10-13 10:26:37 +00:00
<QItemSection v-if="workers && userRadio !== null">
<VnSelectFilter
class="full-width"
2023-09-18 12:35:21 +00:00
:label="t('globals.user')"
v-model="userSelect"
option-label="name"
2023-10-13 10:26:37 +00:00
option-value="id"
:options="workers"
@update:model-value="selectFilter('userSelect')"
2023-09-18 12:35:21 +00:00
>
<template #option="{ opt, itemProps }">
<QItem
v-bind="itemProps"
class="q-pa-xs row items-center"
>
<QItemSection class="col-3 items-center">
<VnAvatar :worker="opt.id" />
</QItemSection>
<QItemSection class="col-9 justify-center">
<span>{{ opt.name }}</span>
<span class="text-grey">{{ opt.nickname }}</span>
</QItemSection>
</QItem>
</template>
2023-10-13 10:26:37 +00:00
</VnSelectFilter>
2023-09-18 12:35:21 +00:00
</QItemSection>
</QItem>
<QItem class="q-mt-sm">
<QInput
:label="t('globals.changes')"
v-model="changeInput"
class="full-width"
clearable
clear-icon="close"
@keyup.enter="selectFilter('change')"
@focusout="selectFilter('change')"
@clear="selectFilter('change')"
>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip max-width="250px">{{
t('tooltips.changes')
}}</QTooltip>
</QIcon>
</template>
</QInput>
</QItem>
<QItem
:class="index == 'create' ? 'q-mt-md' : 'q-mt-xs'"
v-for="(checkboxOption, index) in checkboxOptions"
:key="index"
>
<QCheckbox
size="sm"
v-model="checkboxOption.selected"
:label="t(`actions.${checkboxOption.label}`)"
@update:model-value="selectFilter"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('globals.date')"
@click="dateFromDialog = true"
2023-09-18 12:35:21 +00:00
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'to')"
v-model="dateFrom"
2023-09-18 12:35:21 +00:00
clearable
clear-icon="close"
/>
</QItem>
<QItem class="q-mt-sm">
<QInput
class="full-width"
:label="t('to')"
@click="dateToDialog = true"
@focus="(evt) => evt.target.blur()"
@clear="selectFilter('date', 'from')"
v-model="dateTo"
clearable
clear-icon="close"
/>
</QItem>
</QList>
</QScrollArea>
</QDrawer>
<QDialog v-model="dateFromDialog">
2023-09-18 12:35:21 +00:00
<QDate
:years-in-month-view="false"
v-model="dateFrom"
2023-09-18 12:35:21 +00:00
dense
flat
minimal
@update:model-value="
(value) => {
dateFromDialog = false;
dateFrom = date.formatDate(value, 'DD-MM-YYYY');
2023-09-18 12:35:21 +00:00
selectFilter('date', 'from');
}
"
/>
</QDialog>
<QDialog v-model="dateToDialog">
<QDate
v-model="dateTo"
dense
flat
minimal
@update:model-value="
(value) => {
dateToDialog = false;
dateTo = date.formatDate(value, 'DD-MM-YYYY');
2023-09-18 12:35:21 +00:00
selectFilter('date', 'to');
}
"
/>
</QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
v-if="Object.values(selectedFilters).some((filter) => filter !== undefined)"
color="primary"
icon="filter_alt_off"
size="md"
round
@click="clearFilter"
/>
</QPageSticky>
</template>
<style lang="scss" scoped>
2023-09-18 12:35:21 +00:00
.q-card {
background-color: var(--vn-gray);
}
.q-item {
min-height: 0px;
}
.origin-log {
&:first-child > .origin-info {
margin-top: 0;
}
& > .origin-info {
width: 100%;
max-width: 42em;
margin-top: 28px;
2023-09-18 12:35:21 +00:00
gap: 6px;
& > .origin-id {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0;
}
& > .line {
flex-grow: 1;
height: 2px;
}
}
}
.user-log {
width: 100%;
max-width: 40em;
2023-09-18 12:35:21 +00:00
& > .timeline {
position: relative;
padding-right: 5px;
width: 50px;
min-width: 38px;
& > .arrow {
height: 8px;
width: 8px;
position: absolute;
transform: rotateY(0deg) rotate(45deg);
top: 15px;
right: -4px;
z-index: 1;
}
& > .user-avatar {
padding: 8px 0;
margin-top: -8px;
position: sticky;
top: 64px;
}
& > .line {
position: absolute;
background-color: $primary;
width: 2px;
left: 23px;
z-index: -1;
top: 0;
bottom: -8px;
}
}
&:last-child > .timeline > .line {
display: none;
}
& > .user-changes {
flex-grow: 1;
overflow: hidden;
}
}
.model-log {
.model-info {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.model-value {
font-style: italic;
}
.model-id {
color: var(--vn-label);
font-size: 0.9rem;
}
.q-btn {
visibility: hidden;
2023-09-18 12:35:21 +00:00
float: right;
}
}
&:hover {
.model-info {
.q-btn {
visibility: visible;
}
}
}
2023-09-18 12:35:21 +00:00
}
.changes-log {
width: 100%;
2023-09-18 12:35:21 +00:00
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
.change-info {
overflow: hidden;
2023-09-28 11:56:21 +00:00
background-color: var(--vn-dark);
2023-09-18 12:35:21 +00:00
& > .date {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& > div {
white-space: nowrap;
.action {
color: black;
border-radius: 50%;
padding: 3px;
font-size: 18px;
&.notice {
background-color: $info;
}
&.success {
background-color: $positive;
}
&.warning {
background-color: $warning;
}
&.alert {
background-color: $negative;
}
}
}
.q-btn.pit {
visibility: hidden;
}
&:hover .q-btn.pit {
visibility: visible;
}
2023-09-18 12:35:21 +00:00
}
& > .change-detail {
position: relative;
2023-09-18 12:35:21 +00:00
overflow: hidden;
text-overflow: ellipsis;
background-color: var(--vn-gray);
2023-09-18 12:35:21 +00:00
white-space: nowrap;
box-sizing: border-box;
& > .q-icon {
float: right;
transition-property: transform, background-color;
transition-duration: 150ms;
margin: 0;
}
&.expanded {
text-overflow: initial;
white-space: initial;
& > .q-icon {
transform: rotate(180deg);
}
}
& > .no-changes {
font-style: italic;
}
}
}
.q-menu {
display: block;
& > .loading {
display: flex;
justify-content: center;
}
& > .q-card {
min-width: 180px;
max-width: 400px;
& > .header {
color: $dark;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
</style>
<i18n>
en:
2023-09-18 12:35:21 +00:00
to: To
pointRecord: View record at this point in time
recordChanges: show all record changes
tooltips:
search: Search by id or concept
changes: Search by changes
actions:
2023-09-18 12:35:21 +00:00
Creates: Creates
Edits: Edits
Deletes: Deletes
Accesses: Accesses
Users:
User: Usuario
All: Todo
System: Sistema
properties:
id: ID
claimFk: Claim ID
saleFk: Sale ID
quantity: Quantity
observation: Observation
ticketCreated: Created
created: Created
isChargedToMana: Charged to mana
hasToPickUp: Has to pick Up
dmsFk: Document ID
text: Description
claimStateFk: Claim State
workerFk: Worker
clientFk: Customer
rma: RMA
responsibility: Responsibility
packages: Packages
es:
2023-09-18 12:35:21 +00:00
to: Hasta
pointRecord: Ver el registro en este punto
recordChanges: Mostrar todos los cambios realizados en el registro
tooltips:
search: Buscar por identificador o concepto
changes: Buscar por cambios. Los atributos deben buscarse por su nombre interno, para obtenerlo situar el cursor sobre el atributo.
Audit logs: Registros de auditoría
Property: Propiedad
Before: Antes
After: Después
Yes: Si
Nothing: Nada
actions:
2023-09-18 12:35:21 +00:00
Creates: Crea
Edits: Modifica
Deletes: Elimina
Accesses: Accede
Users:
User: Usuario
All: Todo
System: Sistema
properties:
id: ID
claimFk: ID reclamación
saleFk: ID linea de venta
quantity: Cantidad
observation: Observación
ticketCreated: Creado
created: Creado
isChargedToMana: Cargado a maná
hasToPickUp: Se debe recoger
dmsFk: ID documento
text: Descripción
claimStateFk: Estado de la reclamación
workerFk: Trabajador
clientFk: Cliente
rma: RMA
responsibility: Responsabilidad
packages: Bultos
</i18n>