diff --git a/.eslintrc.js b/.eslintrc.js
index 09dc09c1e..c8bdecb1a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -64,7 +64,7 @@ module.exports = {
     },
     overrides: [
         {
-            files: ['test/cypress/**/*.spec.{js,ts}'],
+            files: ['test/cypress/**/*.*'],
             extends: [
                 // Add Cypress-specific lint rules, globals and Cypress plugin
                 // See https://github.com/cypress-io/eslint-plugin-cypress#rules
diff --git a/cypress.config.js b/cypress.config.js
index 31aad6a86..2b5b40d08 100644
--- a/cypress.config.js
+++ b/cypress.config.js
@@ -7,7 +7,7 @@ module.exports = defineConfig({
         screenshotsFolder: 'test/cypress/screenshots',
         supportFile: 'test/cypress/support/index.js',
         videosFolder: 'test/cypress/videos',
-        video: true,
+        video: false,
         specPattern: 'test/cypress/integration/*.spec.js',
         experimentalRunAllSpecs: true,
         component: {
diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue
new file mode 100644
index 000000000..ed869a565
--- /dev/null
+++ b/src/components/CrudModel.vue
@@ -0,0 +1,322 @@
+<script setup>
+import axios from 'axios';
+import { computed, ref, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useQuasar } from 'quasar';
+import { useValidator } from 'src/composables/useValidator';
+import { useStateStore } from 'stores/useStateStore';
+import VnPaginate from 'components/ui/VnPaginate.vue';
+import VnConfirm from 'components/ui/VnConfirm.vue';
+import SkeletonTable from 'components/ui/SkeletonTable.vue';
+
+const quasar = useQuasar();
+const stateStore = useStateStore();
+const { t } = useI18n();
+const { validate } = useValidator();
+
+const $props = defineProps({
+    model: {
+        type: String,
+        default: '',
+    },
+    url: {
+        type: String,
+        default: '',
+    },
+    saveUrl: {
+        type: String,
+        default: null,
+    },
+    primaryKey: {
+        type: String,
+        default: 'id',
+    },
+    dataRequired: {
+        type: Object,
+        default: () => {},
+    },
+    defaultSave: {
+        type: Boolean,
+        default: true,
+    },
+    defaultReset: {
+        type: Boolean,
+        default: true,
+    },
+    defaultRemove: {
+        type: Boolean,
+        default: true,
+    },
+    selected: {
+        type: Object,
+        default: null,
+    },
+    saveFn: {
+        type: Function,
+        default: null,
+    },
+});
+
+const isLoading = ref(false);
+const hasChanges = ref(false);
+const originalData = ref();
+const vnPaginateRef = ref();
+const formData = ref();
+const formUrl = computed(() => $props.url);
+
+const emit = defineEmits(['onFetch', 'update:selected']);
+
+defineExpose({
+    reload,
+    insert,
+    remove,
+    onSubmit,
+    reset,
+    hasChanges,
+});
+
+function tMobile(...args) {
+    if (!quasar.platform.is.mobile) return t(...args);
+}
+
+async function fetch(data) {
+    if (data && Array.isArray(data)) {
+        let $index = 0;
+        data.map((d) => (d.$index = $index++));
+    }
+
+    originalData.value = data && JSON.parse(JSON.stringify(data));
+    formData.value = data && JSON.parse(JSON.stringify(data));
+    watch(formData, () => (hasChanges.value = true), { deep: true });
+
+    emit('onFetch', data);
+}
+
+async function reset() {
+    await fetch(originalData.value);
+    hasChanges.value = false;
+}
+// eslint-disable-next-line vue/no-dupe-keys
+function filter(value, update, filterOptions) {
+    update(
+        () => {
+            const { options, filterFn, field } = filterOptions;
+
+            options.value = filterFn(options, value, field);
+        },
+        (ref) => {
+            ref.setOptionIndex(-1);
+            ref.moveOptionSelection(1, true);
+        }
+    );
+}
+
+async function onSubmit() {
+    if (!hasChanges.value) {
+        return quasar.notify({
+            type: 'negative',
+            message: t('globals.noChanges'),
+        });
+    }
+    isLoading.value = true;
+    await saveChanges();
+}
+
+async function saveChanges(data) {
+    if ($props.saveFn) return $props.saveFn(data, getChanges);
+    const changes = data || getChanges();
+    try {
+        await axios.post($props.saveUrl || $props.url + '/crud', changes);
+    } catch (e) {
+        return (isLoading.value = false);
+    }
+    originalData.value = JSON.parse(JSON.stringify(formData.value));
+    if (changes.creates?.length) await vnPaginateRef.value.fetch();
+
+    hasChanges.value = false;
+    isLoading.value = false;
+}
+
+async function insert() {
+    const $index = formData.value.length
+        ? formData.value[formData.value.length - 1].$index + 1
+        : 0;
+    formData.value.push(Object.assign({ $index }, $props.dataRequired));
+    hasChanges.value = true;
+}
+
+async function remove(data) {
+    if (!data.length)
+        return quasar.notify({
+            type: 'warning',
+            message: t('globals.noChanges'),
+        });
+
+    const pk = $props.primaryKey;
+    let ids = data.map((d) => d[pk]).filter(Boolean);
+    let preRemove = data.map((d) => (d[pk] ? null : d.$index)).filter(Boolean);
+    let newData = formData.value;
+
+    if (preRemove.length) {
+        newData = newData.filter(
+            (form) => !preRemove.some((index) => index == form.$index)
+        );
+        const changes = getChanges();
+        if (!changes.creates?.length && !changes.updates?.length)
+            hasChanges.value = false;
+        fetch(newData);
+    }
+    if (ids.length) {
+        quasar
+            .dialog({
+                component: VnConfirm,
+                componentProps: {
+                    title: t('confirmDeletion'),
+                    message: t('confirmDeletionMessage'),
+                    newData,
+                    ids,
+                },
+            })
+            .onOk(async () => {
+                await saveChanges({ deletes: ids });
+                newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
+                fetch(newData);
+            });
+    }
+    emit('update:selected', []);
+}
+
+function getChanges() {
+    const updates = [];
+    const creates = [];
+
+    const pk = $props.primaryKey;
+
+    for (const [i, row] of formData.value.entries()) {
+        if (!row[pk]) {
+            creates.push(row);
+        } else if (originalData.value) {
+            const data = getDifferences(originalData.value[i], row);
+            if (!isEmpty(data)) {
+                updates.push({
+                    data,
+                    where: { [pk]: row[pk] },
+                });
+            }
+        }
+    }
+    const changes = { updates, creates };
+
+    for (let prop in changes) {
+        if (changes[prop].length === 0) changes[prop] = undefined;
+    }
+
+    return changes;
+}
+
+function getDifferences(obj1, obj2) {
+    let diff = {};
+    delete obj1.$index;
+    delete obj2.$index;
+
+    for (let key in obj1) {
+        if (obj2[key] && obj1[key] !== obj2[key]) {
+            diff[key] = obj2[key];
+        }
+    }
+    for (let key in obj2) {
+        if (obj1[key] === undefined || obj1[key] !== obj2[key]) {
+            diff[key] = obj2[key];
+        }
+    }
+    return diff;
+}
+
+function isEmpty(obj) {
+    if (obj == null) return true;
+    if (obj === undefined) return true;
+    if (Object.keys(obj).length === 0) return true;
+
+    if (obj.length > 0) return false;
+}
+
+async function reload() {
+    vnPaginateRef.value.fetch();
+}
+
+watch(formUrl, async () => {
+    originalData.value = null;
+    reset();
+});
+</script>
+<template>
+    <VnPaginate
+        :url="url"
+        v-bind="$attrs"
+        @on-fetch="fetch"
+        :skeleton="false"
+        ref="vnPaginateRef"
+    >
+        <template #body v-if="formData">
+            <slot
+                name="body"
+                :rows="formData"
+                :validate="validate"
+                :filter="filter"
+            ></slot>
+        </template>
+    </VnPaginate>
+    <SkeletonTable v-if="!formData" />
+    <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
+        <QBtnGroup push class="q-gutter-x-sm">
+            <slot name="moreActions" />
+            <QBtn
+                :label="tMobile('globals.remove')"
+                color="primary"
+                icon="delete"
+                flat
+                @click="remove(selected)"
+                :disable="!selected?.length"
+                :title="t('globals.remove')"
+                v-if="$props.defaultRemove"
+            />
+            <QBtn
+                :label="tMobile('globals.reset')"
+                color="primary"
+                icon="restart_alt"
+                flat
+                @click="reset"
+                :disable="!hasChanges"
+                :title="t('globals.reset')"
+                v-if="$props.defaultReset"
+            />
+            <QBtn
+                :label="tMobile('globals.save')"
+                color="primary"
+                icon="save"
+                @click="onSubmit"
+                :disable="!hasChanges"
+                :title="t('globals.save')"
+                v-if="$props.defaultSave"
+            />
+        </QBtnGroup>
+    </Teleport>
+    <QInnerLoading
+        :showing="isLoading"
+        :label="t && t('globals.pleaseWait')"
+        color="primary"
+    />
+</template>
+
+<i18n>
+    {
+        "en": {
+            "confirmDeletion": "Confirm deletion",
+            "confirmDeletionMessage": "Are you sure you want to delete this?"
+        },
+        "es": {
+            "confirmDeletion": "Confirmar eliminación",
+            "confirmDeletionMessage": "Seguro que quieres eliminar?"
+        }
+    }
+</i18n>
diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue
index 9d0916a8e..540c37d01 100644
--- a/src/components/FormModel.vue
+++ b/src/components/FormModel.vue
@@ -4,12 +4,14 @@ import { onMounted, onUnmounted, computed, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useQuasar } from 'quasar';
 import { useState } from 'src/composables/useState';
+import { useStateStore } from 'stores/useStateStore';
 import { useValidator } from 'src/composables/useValidator';
 import SkeletonForm from 'components/ui/SkeletonForm.vue';
 
 const quasar = useQuasar();
-const { t } = useI18n();
 const state = useState();
+const stateStore = useStateStore();
+const { t } = useI18n();
 const { validate } = useValidator();
 
 const $props = defineProps({
@@ -29,6 +31,10 @@ const $props = defineProps({
         type: String,
         default: null,
     },
+    defaultActions: {
+        type: Boolean,
+        default: true,
+    },
 });
 
 const emit = defineEmits(['onFetch']);
@@ -45,17 +51,21 @@ onUnmounted(() => {
 
 const isLoading = ref(false);
 const hasChanges = ref(false);
-const formData = computed(() => state.get($props.model));
 const originalData = ref();
+const formData = computed(() => state.get($props.model));
 const formUrl = computed(() => $props.url);
 
+function tMobile(...args) {
+    if (!quasar.platform.is.mobile) return t(...args);
+}
+
 async function fetch() {
     const { data } = await axios.get($props.url, {
         params: { filter: $props.filter },
     });
 
     state.set($props.model, data);
-    originalData.value = Object.assign({}, data);
+    originalData.value = data && JSON.parse(JSON.stringify(data));
 
     watch(formData.value, () => (hasChanges.value = true));
 
@@ -72,13 +82,18 @@ async function save() {
     isLoading.value = true;
     await axios.patch($props.urlUpdate || $props.url, formData.value);
 
-    originalData.value = formData.value;
+    originalData.value = JSON.parse(JSON.stringify(formData.value));
     hasChanges.value = false;
     isLoading.value = false;
 }
 
 function reset() {
     state.set($props.model, originalData.value);
+    originalData.value = JSON.parse(JSON.stringify(originalData.value));
+
+    watch(formData.value, () => (hasChanges.value = true));
+
+    emit('onFetch', state.get($props.model));
     hasChanges.value = false;
 }
 // eslint-disable-next-line vue/no-dupe-keys
@@ -109,20 +124,31 @@ watch(formUrl, async () => {
     </QBanner>
     <QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md">
         <slot name="form" :data="formData" :validate="validate" :filter="filter"></slot>
-        <div class="q-mt-lg">
-            <slot name="actions">
-                <QBtn :label="t('globals.save')" type="submit" color="primary" />
-                <QBtn
-                    :label="t('globals.reset')"
-                    type="reset"
-                    class="q-ml-sm"
-                    color="primary"
-                    flat
-                    :disable="!hasChanges"
-                />
-            </slot>
-        </div>
     </QForm>
+    <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
+        <div v-if="$props.defaultActions">
+            <QBtnGroup push class="q-gutter-x-sm">
+                <slot name="moreActions" />
+                <QBtn
+                    :label="tMobile('globals.reset')"
+                    color="primary"
+                    icon="restart_alt"
+                    flat
+                    @click="reset"
+                    :disable="!hasChanges"
+                    :title="t('globals.reset')"
+                />
+                <QBtn
+                    :label="tMobile('globals.save')"
+                    color="primary"
+                    icon="save"
+                    @click="save"
+                    :disable="!hasChanges"
+                    :title="t('globals.save')"
+                />
+            </QBtnGroup>
+        </div>
+    </Teleport>
     <SkeletonForm v-if="!formData" />
     <QInnerLoading
         :showing="isLoading"
diff --git a/src/components/common/VnSelectFilter.vue b/src/components/common/VnSelectFilter.vue
new file mode 100644
index 000000000..9ccb94e33
--- /dev/null
+++ b/src/components/common/VnSelectFilter.vue
@@ -0,0 +1,76 @@
+<script setup>
+import { ref, toRefs, watch, computed } from 'vue';
+const emit = defineEmits(['update:modelValue', 'update:options']);
+
+const $props = defineProps({
+    modelValue: {
+        type: [String, Number],
+        default: null,
+    },
+    options: {
+        type: Array,
+        default: () => [],
+    },
+    optionLabel: {
+        type: String,
+        default: '',
+    },
+});
+const { optionLabel, options } = toRefs($props);
+const myOptions = ref([]);
+const myOptionsOriginal = ref([]);
+function setOptions(data) {
+    myOptions.value = JSON.parse(JSON.stringify(data));
+    myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
+}
+setOptions(options.value);
+
+const filter = (val, options) => {
+    const search = val.toLowerCase();
+
+    if (val === '') return options;
+    return options.filter((row) => {
+        const id = row.id;
+        const name = row[$props.optionLabel].toLowerCase();
+
+        const idMatches = id == search;
+        const nameMatches = name.indexOf(search) > -1;
+
+        return idMatches || nameMatches;
+    });
+};
+
+const filterHandler = (val, update) => {
+    update(() => {
+        myOptions.value = filter(val, myOptionsOriginal.value);
+    });
+};
+
+watch(options, (newValue) => {
+    setOptions(newValue);
+});
+
+const value = computed({
+    get() {
+        return $props.modelValue;
+    },
+    set(value) {
+        emit('update:modelValue', value);
+    },
+});
+</script>
+
+<template>
+    <QSelect
+        v-model="value"
+        :options="myOptions"
+        :option-label="optionLabel"
+        v-bind="$attrs"
+        emit-value
+        map-options
+        use-input
+        @filter="filterHandler"
+        clearable
+        clear-icon="close"
+    />
+</template>
diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue
index 65a89ee28..f63b75de6 100644
--- a/src/components/ui/CardDescriptor.vue
+++ b/src/components/ui/CardDescriptor.vue
@@ -29,12 +29,14 @@ const $props = defineProps({
 
 const slots = useSlots();
 const { t } = useI18n();
+const entity = ref();
 
-onMounted(() => fetch());
+onMounted(async () => {
+    await fetch();
+});
 
 const emit = defineEmits(['onFetch']);
 
-const entity = ref();
 async function fetch() {
     const params = {};
 
diff --git a/src/components/ui/SkeletonTable.vue b/src/components/ui/SkeletonTable.vue
new file mode 100644
index 000000000..d58253f90
--- /dev/null
+++ b/src/components/ui/SkeletonTable.vue
@@ -0,0 +1,50 @@
+<template>
+    <div class="q-pa-md w">
+        <div class="row q-gutter-md q-mb-md">
+            <div class="col-1">
+                <QSkeleton type="rect" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="rect" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="rect" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="rect" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="rect" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="rect" square />
+            </div>
+        </div>
+
+        <div class="row q-gutter-md q-mb-md" v-for="n in 5" :key="n">
+            <div class="col-1">
+                <QSkeleton type="QInput" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="QInput" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="QInput" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="QInput" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="QInput" square />
+            </div>
+            <div class="col">
+                <QSkeleton type="QInput" square />
+            </div>
+        </div>
+    </div>
+</template>
+<style lang="scss" scoped>
+.w {
+    width: 80vw;
+}
+</style>
diff --git a/src/components/ui/VnPaginate.vue b/src/components/ui/VnPaginate.vue
index 434ebf232..2475f56e9 100644
--- a/src/components/ui/VnPaginate.vue
+++ b/src/components/ui/VnPaginate.vue
@@ -46,6 +46,10 @@ const props = defineProps({
         type: Number,
         default: 500,
     },
+    skeleton: {
+        type: Boolean,
+        default: true,
+    },
 });
 
 const emit = defineEmits(['onFetch', 'onPaginate']);
@@ -144,7 +148,10 @@ async function onLoad(...params) {
                 {{ t('No results found') }}
             </h5>
         </div>
-        <div v-if="props.autoLoad && !store.data" class="card-list q-gutter-y-md">
+        <div
+            v-if="props.skeleton && props.autoLoad && !store.data"
+            class="card-list q-gutter-y-md"
+        >
             <QCard class="card" v-for="$index in $props.limit" :key="$index">
                 <QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
                     <QItemSection class="q-pa-md">
@@ -164,7 +171,7 @@ async function onLoad(...params) {
             </QCard>
         </div>
     </div>
-    <QInfiniteScroll v-if="store.data" @load="onLoad" :offset="offset">
+    <QInfiniteScroll v-if="store.data" @load="onLoad" :offset="offset" class="full-width">
         <slot name="body" :rows="store.data"></slot>
         <div v-if="isLoading" class="info-row q-pa-md text-center">
             <QSpinner color="orange" size="md" />
diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js
index c7808f9a8..4535cde0f 100644
--- a/src/composables/useArrayData.js
+++ b/src/composables/useArrayData.js
@@ -38,11 +38,11 @@ export function useArrayData(key, userOptions) {
             'limit',
             'skip',
             'userParams',
-            'userFilter'
+            'userFilter',
         ];
         if (typeof userOptions === 'object') {
             for (const option in userOptions) {
-                const isEmpty = userOptions[option] == null || userOptions[option] == ''
+                const isEmpty = userOptions[option] == null || userOptions[option] == '';
                 if (isEmpty || !allowedOptions.includes(option)) continue;
 
                 if (Object.prototype.hasOwnProperty.call(store, option)) {
@@ -73,7 +73,7 @@ export function useArrayData(key, userOptions) {
 
         Object.assign(params, store.userParams);
 
-        store.isLoading = true
+        store.isLoading = true;
         const response = await axios.get(store.url, {
             signal: canceller.signal,
             params,
@@ -94,7 +94,7 @@ export function useArrayData(key, userOptions) {
             updateStateParams();
         }
 
-        store.isLoading = false
+        store.isLoading = false;
 
         canceller = null;
     }
@@ -153,8 +153,8 @@ export function useArrayData(key, userOptions) {
         });
     }
 
-    const totalRows = computed(() => store.data && store.data.length || 0);
-    const isLoading = computed(() => store.isLoading || false)
+    const totalRows = computed(() => (store.data && store.data.length) || 0);
+    const isLoading = computed(() => store.isLoading || false);
 
     return {
         fetch,
@@ -167,6 +167,6 @@ export function useArrayData(key, userOptions) {
         hasMoreData,
         totalRows,
         updateStateParams,
-        isLoading
+        isLoading,
     };
 }
diff --git a/src/composables/useValidator.js b/src/composables/useValidator.js
index ef2dcbd90..bc48332a2 100644
--- a/src/composables/useValidator.js
+++ b/src/composables/useValidator.js
@@ -3,15 +3,13 @@ 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)
+        axios.get('Schemas/ModelInfo').then((response) => (models.value = response.data));
     }
 
     function validate(propertyRule) {
@@ -38,19 +36,18 @@ export function useValidator() {
 
     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
+                    message = t(validation.message) || validation.message;
 
-                return !validator.isEmpty(value ? String(value) : '') || message
+                return !validator.isEmpty(value ? String(value) : '') || message;
             },
             length: (value) => {
                 const options = {
                     min: validation.min || validation.is,
-                    max: validation.max || validation.is
+                    max: validation.max || validation.is,
                 };
 
                 value = String(value);
@@ -69,14 +66,14 @@ export function useValidator() {
             },
             numericality: (value) => {
                 if (validation.int)
-                    return validator.isInt(value) || 'Value should be integer'
-                return validator.isNumeric(value) || 'Value should be a number'
+                    return validator.isInt(value) || 'Value should be integer';
+                return validator.isNumeric(value) || 'Value should be a number';
             },
-            custom: (value) => validation.bindedFunction(value) || 'Invalid value'
+            custom: (value) => validation.bindedFunction(value) || 'Invalid value',
         };
     };
 
     return {
-        validate
+        validate,
     };
-}
\ No newline at end of file
+}
diff --git a/src/css/app.scss b/src/css/app.scss
index 3c8cc50b6..0f04c9ad8 100644
--- a/src/css/app.scss
+++ b/src/css/app.scss
@@ -32,10 +32,16 @@ body.body--light {
     --vn-text: #000000;
     --vn-gray: #f5f5f5;
     --vn-label: #5f5f5f;
+    --vn-dark: white;
 }
 
 body.body--dark {
     --vn-text: #ffffff;
     --vn-gray: #313131;
     --vn-label: #a8a8a8;
+    --vn-dark: #292929;
+}
+
+.bg-vn-dark {
+    background-color: var(--vn-dark);
 }
diff --git a/src/i18n/en/index.js b/src/i18n/en/index.js
index 62704bf8c..cfd20716b 100644
--- a/src/i18n/en/index.js
+++ b/src/i18n/en/index.js
@@ -266,6 +266,7 @@ export default {
             lines: 'Lines',
             rma: 'RMA',
             photos: 'Photos',
+            development: 'Development',
             log: 'Audit logs',
             notes: 'Notes',
         },
diff --git a/src/i18n/es/index.js b/src/i18n/es/index.js
index 1cef961db..532c1bb3b 100644
--- a/src/i18n/es/index.js
+++ b/src/i18n/es/index.js
@@ -264,6 +264,7 @@ export default {
             basicData: 'Datos básicos',
             lines: 'Líneas',
             rma: 'RMA',
+            development: 'Trazabilidad',
             photos: 'Fotos',
             log: 'Registros de auditoría',
             notes: 'Notas',
diff --git a/src/pages/Claim/Card/ClaimCard.vue b/src/pages/Claim/Card/ClaimCard.vue
index 9f1ecc416..03b9889f0 100644
--- a/src/pages/Claim/Card/ClaimCard.vue
+++ b/src/pages/Claim/Card/ClaimCard.vue
@@ -44,17 +44,6 @@ onMounted(async () => {
             <LeftMenu source="card" />
             <QSeparator />
             <QList>
-                <QItem
-                    active-class="text-primary"
-                    clickable
-                    v-ripple
-                    :href="`${salixUrl}/development`"
-                >
-                    <QItemSection avatar>
-                        <QIcon name="vn:traceability"></QIcon>
-                    </QItemSection>
-                    <QItemSection>{{ t('Development') }}</QItemSection>
-                </QItem>
                 <QItem
                     active-class="text-primary"
                     clickable
@@ -68,8 +57,13 @@ onMounted(async () => {
         </QScrollArea>
     </QDrawer>
     <QPageContainer>
-        <QPage class="q-pa-md">
-            <RouterView></RouterView>
+        <QPage>
+            <QToolbar class="bg-vn-dark justify-end">
+                <div id="st-data"></div>
+                <QSpace />
+                <div id="st-actions"></div>
+            </QToolbar>
+            <div class="q-pa-md"><RouterView></RouterView></div>
         </QPage>
     </QPageContainer>
 </template>
@@ -80,6 +74,5 @@ es:
     You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
     Details: Detalles
     Notes: Notas
-    Development: Trazabilidad
     Action: Acción
 </i18n>
diff --git a/src/pages/Claim/Card/ClaimDescriptor.vue b/src/pages/Claim/Card/ClaimDescriptor.vue
index 914de2eb2..af7e84d38 100644
--- a/src/pages/Claim/Card/ClaimDescriptor.vue
+++ b/src/pages/Claim/Card/ClaimDescriptor.vue
@@ -3,6 +3,8 @@ import { ref, computed } from 'vue';
 import { useRoute } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { toDate } from 'src/filters';
+import { useState } from 'src/composables/useState';
+
 import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
 import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
 import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
@@ -19,6 +21,7 @@ const $props = defineProps({
 });
 
 const route = useRoute();
+const state = useState();
 const { t } = useI18n();
 
 const entityId = computed(() => {
@@ -67,6 +70,7 @@ function stateColor(code) {
 const data = ref(useCardDescription());
 const setData = (entity) => {
     data.value = useCardDescription(entity.client.name, entity.id);
+    state.set('ClaimDescriptor', entity);
 };
 </script>
 
diff --git a/src/pages/Claim/Card/ClaimDevelopment.vue b/src/pages/Claim/Card/ClaimDevelopment.vue
new file mode 100644
index 000000000..ea4b178b5
--- /dev/null
+++ b/src/pages/Claim/Card/ClaimDevelopment.vue
@@ -0,0 +1,203 @@
+<script setup>
+import { ref, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useRoute } from 'vue-router';
+import CrudModel from 'components/CrudModel.vue';
+import FetchData from 'components/FetchData.vue';
+import VnSelectFilter from 'components/common/VnSelectFilter.vue';
+
+const route = useRoute();
+const { t } = useI18n();
+
+const claimDevelopmentForm = ref();
+const claimReasons = ref([]);
+const claimResults = ref([]);
+const claimResponsibles = ref([]);
+const claimRedeliveries = ref([]);
+const workers = ref([]);
+const selected = ref([]);
+
+const developmentsFilter = {
+    fields: [
+        'id',
+        'claimFk',
+        'claimReasonFk',
+        'claimResultFk',
+        'claimResponsibleFk',
+        'workerFk',
+        'claimRedeliveryFk',
+    ],
+    where: {
+        claimFk: route.params.id,
+    },
+};
+
+const columns = computed(() => [
+    {
+        name: 'claimReason',
+        label: t('Reason'),
+        field: (row) => row.claimReasonFk,
+        sortable: true,
+        options: claimReasons.value,
+        required: true,
+        model: 'claimReasonFk',
+        optionValue: 'id',
+        optionLabel: 'description',
+    },
+    {
+        name: 'claimResult',
+        label: t('Result'),
+        field: (row) => row.claimResultFk,
+        sortable: true,
+        options: claimResults.value,
+        required: true,
+        model: 'claimResultFk',
+        optionValue: 'id',
+        optionLabel: 'description',
+    },
+    {
+        name: 'claimResponsible',
+        label: t('Responsible'),
+        field: (row) => row.claimResponsibleFk,
+        sortable: true,
+        options: claimResponsibles.value,
+        required: true,
+        model: 'claimResponsibleFk',
+        optionValue: 'id',
+        optionLabel: 'description',
+    },
+    {
+        name: 'worker',
+        label: t('Worker'),
+        field: (row) => row.workerFk,
+        sortable: true,
+        options: workers.value,
+        model: 'workerFk',
+        optionValue: 'id',
+        optionLabel: 'nickname',
+    },
+    {
+        name: 'claimRedelivery',
+        label: t('Redelivery'),
+        field: (row) => row.claimRedeliveryFk,
+        sortable: true,
+        options: claimRedeliveries.value,
+        required: true,
+        model: 'claimRedeliveryFk',
+        optionValue: 'id',
+        optionLabel: 'description',
+    },
+]);
+</script>
+<template>
+    <FetchData
+        url="ClaimReasons"
+        order="description"
+        @on-fetch="(data) => (claimReasons = data)"
+        auto-load
+    />
+    <FetchData
+        url="ClaimResults"
+        order="description"
+        @on-fetch="(data) => (claimResults = data)"
+        auto-load
+    />
+    <FetchData
+        url="ClaimResponsibles"
+        order="description"
+        @on-fetch="(data) => (claimResponsibles = data)"
+        auto-load
+    />
+    <FetchData
+        url="ClaimRedeliveries"
+        order="description"
+        @on-fetch="(data) => (claimRedeliveries = data)"
+        auto-load
+    />
+    <FetchData
+        url="Workers/activeWithInheritedRole"
+        :where="{ role: 'employee' }"
+        @on-fetch="(data) => (workers = data)"
+        auto-load
+    />
+    <CrudModel
+        data-key="ClaimDevelopments"
+        url="ClaimDevelopments"
+        model="claimDevelopment"
+        :filter="developmentsFilter"
+        ref="claimDevelopmentForm"
+        :data-required="{ claimFk: route.params.id }"
+        v-model:selected="selected"
+        auto-load
+    >
+        <template #body="{ rows }">
+            <QTable
+                :columns="columns"
+                :rows="rows"
+                :pagination="{ rowsPerPage: 0 }"
+                row-key="$index"
+                selection="multiple"
+                hide-pagination
+                v-model:selected="selected"
+                :grid="$q.screen.lt.md"
+            >
+                <template #body-cell="{ row, col }">
+                    <QTd auto-width>
+                        <VnSelectFilter
+                            :label="col.label"
+                            v-model="row[col.model]"
+                            :options="col.options"
+                            :option-value="col.optionValue"
+                            :option-label="col.optionLabel"
+                        />
+                    </QTd>
+                </template>
+                <template #item="props">
+                    <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
+                        <QCard bordered flat>
+                            <QCardSection>
+                                <QCheckbox v-model="props.selected" dense />
+                            </QCardSection>
+                            <QSeparator />
+                            <QList dense>
+                                <QItem v-for="col in props.cols" :key="col.name">
+                                    <QItemSection>
+                                        <VnSelectFilter
+                                            :label="col.label"
+                                            v-model="props.row[col.model]"
+                                            :options="col.options"
+                                            :option-value="col.optionValue"
+                                            :option-label="col.optionLabel"
+                                            dense
+                                        />
+                                    </QItemSection>
+                                </QItem>
+                            </QList>
+                        </QCard>
+                    </div>
+                </template>
+            </QTable>
+        </template>
+    </CrudModel>
+    <QPageSticky position="bottom-right" :offset="[25, 25]">
+        <QBtn fab color="primary" icon="add" @click="claimDevelopmentForm.insert()" />
+    </QPageSticky>
+</template>
+
+<style lang="scss" scoped>
+.grid-style-transition {
+    transition: transform 0.28s, background-color 0.28s;
+}
+.maxwidth {
+    width: 100%;
+}
+</style>
+
+<i18n>
+es:
+    Reason: Motivo
+    Result: Consecuencia
+    Responsible: Responsable
+    Worker: Trabajador
+    Redelivery: Devolución
+</i18n>
diff --git a/src/pages/Claim/Card/ClaimLines.vue b/src/pages/Claim/Card/ClaimLines.vue
index 9d2a12804..8680ff922 100644
--- a/src/pages/Claim/Card/ClaimLines.vue
+++ b/src/pages/Claim/Card/ClaimLines.vue
@@ -6,9 +6,8 @@ import { useQuasar } from 'quasar';
 import { useRoute } from 'vue-router';
 import { useArrayData } from 'composables/useArrayData';
 import { useStateStore } from 'stores/useStateStore';
-import VnPaginate from 'components/ui/VnPaginate.vue';
+import CrudModel from 'components/CrudModel.vue';
 import FetchData from 'components/FetchData.vue';
-import VnConfirm from 'components/ui/VnConfirm.vue';
 
 import { toDate, toCurrency, toPercentage } from 'filters/index';
 import VnDiscount from 'components/common/vnDiscount.vue';
@@ -17,6 +16,7 @@ import ClaimLinesImport from './ClaimLinesImport.vue';
 const quasar = useQuasar();
 const route = useRoute();
 const { t } = useI18n();
+
 const stateStore = useStateStore();
 const arrayData = useArrayData('ClaimLines');
 const store = arrayData.store;
@@ -36,6 +36,7 @@ const linesFilter = {
     },
 };
 
+const claimLinesForm = ref();
 const claim = ref(null);
 async function onFetchClaim(data) {
     claim.value = data;
@@ -46,6 +47,7 @@ async function onFetchClaim(data) {
 const amount = ref(0);
 const amountClaimed = ref(0);
 async function onFetch(rows) {
+    if (!rows || rows.length) return;
     amount.value = rows.reduce(
         (acumulator, { sale }) => acumulator + sale.price * sale.quantity,
         0
@@ -141,47 +143,6 @@ function onUpdateDiscount(response) {
     });
 }
 
-async function confirmRemove() {
-    const rows = selected.value;
-    const count = rows.length;
-
-    if (count === 0) {
-        return quasar.notify({
-            message: 'You must select at least one row',
-            type: 'warning',
-        });
-    }
-
-    quasar
-        .dialog({
-            component: VnConfirm,
-            componentProps: {
-                title: t('Delete claimed sales'),
-                message: t('You are about to remove {count} rows', count, { count }),
-                data: { rows },
-                promise: remove,
-            },
-        })
-        .onOk(() => {
-            for (const row of rows) {
-                const orgData = store.data;
-                const index = orgData.findIndex((item) => item.id === row.id);
-                store.data.splice(index, 1);
-                selected.value = [];
-            }
-        });
-}
-
-async function remove({ rows }) {
-    if (!rows.length) return;
-    const body = { deletes: rows.map((row) => row.id) };
-    await axios.post(`ClaimBeginnings/crud`, body);
-    quasar.notify({
-        type: 'positive',
-        message: t('globals.rowRemoved'),
-    });
-}
-
 function showImportDialog() {
     quasar
         .dialog({
@@ -191,10 +152,8 @@ function showImportDialog() {
 }
 </script>
 <template>
-    <QPageSticky position="top" :offset="[0, 0]" expand>
+    <Teleport to="#st-data" v-if="stateStore.isSubToolbarShown()">
         <QToolbar class="bg-dark text-white">
-            <QToolbarTitle> {{ t('Claimed lines') }} </QToolbarTitle>
-            <QSpace />
             <div class="row q-gutter-md">
                 <div>
                     {{ t('Amount') }}
@@ -211,7 +170,7 @@ function showImportDialog() {
                 </div>
             </div>
         </QToolbar>
-    </QPageSticky>
+    </Teleport>
 
     <FetchData
         :url="`Claims/${route.params.id}`"
@@ -221,11 +180,16 @@ function showImportDialog() {
     />
     <div class="column items-center">
         <div class="list">
-            <VnPaginate
+            <CrudModel
                 data-key="ClaimLines"
+                ref="claimLinesForm"
                 :url="`Claims/${route.params.id}/lines`"
+                save-url="ClaimBeginnings/crud"
                 :filter="linesFilter"
                 @on-fetch="onFetch"
+                v-model:selected="selected"
+                :default-save="false"
+                :default-reset="false"
                 auto-load
             >
                 <template #body="{ rows }">
@@ -361,46 +325,12 @@ function showImportDialog() {
                         </template>
                     </QTable>
                 </template>
-            </VnPaginate>
+            </CrudModel>
         </div>
     </div>
 
-    <Teleport
-        v-if="stateStore.isHeaderMounted() && !$q.screen.lt.sm"
-        to="#actions-prepend"
-    >
-        <div class="row q-gutter-x-sm">
-            <QBtn
-                v-if="selected.length > 0"
-                @click="confirmRemove"
-                icon="delete"
-                color="primary"
-                flat
-                dense
-                rounded
-            >
-                <QTooltip bottom> {{ t('globals.remove') }} </QTooltip>
-            </QBtn>
-            <QBtn @click="showImportDialog" icon="add" color="primary" flat dense rounded>
-                <QTooltip bottom> {{ t('globals.add') }} </QTooltip>
-            </QBtn>
-            <QSeparator vertical />
-        </div>
-    </Teleport>
-    <!-- v-if="quasar.platform.is.mobile" -->
-    <QPageSticky v-if="$q.screen.lt.sm" position="bottom" :offset="[0, 0]" expand>
-        <QToolbar class="bg-primary text-white q-pa-none">
-            <QTabs class="full-width" align="justify" inline-label narrow-indicator>
-                <QTab @click="showImportDialog" icon="add" :label="t('globals.add')" />
-                <QSeparator vertical inset />
-                <QTab
-                    @click="confirmRemove"
-                    icon="delete"
-                    :label="t('globals.remove')"
-                    :disable="selected.length === 0"
-                />
-            </QTabs>
-        </QToolbar>
+    <QPageSticky position="bottom-right" :offset="[25, 25]">
+        <QBtn fab color="primary" icon="add" @click="showImportDialog()" />
     </QPageSticky>
 </template>
 
@@ -421,7 +351,6 @@ en:
         You are about to remove <strong>{count}</strong> row |
         You are about to remove  <strong>{count}</strong> rows'
 es:
-    Claimed lines: Líneas reclamadas
     Delivered: Entregado
     Quantity: Cantidad
     Claimed: Reclamada
diff --git a/src/pages/Claim/Card/ClaimNotes.vue b/src/pages/Claim/Card/ClaimNotes.vue
index 94b53c8a2..84d350e38 100644
--- a/src/pages/Claim/Card/ClaimNotes.vue
+++ b/src/pages/Claim/Card/ClaimNotes.vue
@@ -25,7 +25,7 @@ const body = {
 };
 </script>
 <template>
-    <div class="col items-center">
+    <div class="column items-center">
         <VnNotes
             :add-note="true"
             :id="id"
diff --git a/src/pages/Claim/Card/ClaimPhoto.vue b/src/pages/Claim/Card/ClaimPhoto.vue
index 839fbefc9..483dbffc1 100644
--- a/src/pages/Claim/Card/ClaimPhoto.vue
+++ b/src/pages/Claim/Card/ClaimPhoto.vue
@@ -4,7 +4,6 @@ import { ref, computed } from 'vue';
 import { useQuasar } from 'quasar';
 import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
-import { useStateStore } from 'stores/useStateStore';
 import { useSession } from 'composables/useSession';
 import VnConfirm from 'components/ui/VnConfirm.vue';
 import FetchData from 'components/FetchData.vue';
@@ -12,7 +11,6 @@ import FetchData from 'components/FetchData.vue';
 const router = useRouter();
 const quasar = useQuasar();
 const { t } = useI18n();
-const stateStore = useStateStore();
 const session = useSession();
 const token = session.getToken();
 
@@ -237,59 +235,20 @@ function onDrag() {
         </div>
     </div>
 
-    <Teleport
-        v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
-        to="#actions-prepend"
-    >
-        <div class="row q-gutter-x-sm">
-            <label for="fileInput">
-                <QBtn
-                    @click="inputFile.nativeEl.click()"
-                    icon="add"
-                    color="primary"
-                    dense
-                    rounded
-                >
-                    <QInput
-                        ref="inputFile"
-                        type="file"
-                        style="display: none"
-                        multiple
-                        v-model="files"
-                        @update:model-value="create()"
-                    />
-                    <QTooltip bottom> {{ t('globals.add') }} </QTooltip>
-                </QBtn>
-            </label>
-            <QSeparator vertical />
-        </div>
-    </Teleport>
-
-    <QPageSticky
-        v-if="quasar.platform.is.mobile"
-        position="bottom"
-        :offset="[0, 0]"
-        expand
-    >
-        <QToolbar class="bg-primary text-white q-pa-none">
-            <QTabs class="full-width" align="justify" inline-label narrow-indicator>
-                <QTab
-                    @click="inputFile.nativeEl.click()"
-                    icon="add_circle"
-                    :label="t('globals.add')"
-                >
-                    <QInput
-                        ref="inputFile"
-                        type="file"
-                        style="display: none"
-                        multiple
-                        v-model="files"
-                        @update:model-value="create()"
-                    />
-                    <QTooltip bottom> {{ t('globals.add') }} </QTooltip>
-                </QTab>
-            </QTabs>
-        </QToolbar>
+    <QPageSticky position="bottom-right" :offset="[25, 25]">
+        <label for="fileInput">
+            <QBtn fab @click="inputFile.nativeEl.click()" icon="add" color="primary">
+                <QInput
+                    ref="inputFile"
+                    type="file"
+                    style="display: none"
+                    multiple
+                    v-model="files"
+                    @update:model-value="create()"
+                />
+                <QTooltip bottom> {{ t('globals.add') }} </QTooltip>
+            </QBtn>
+        </label>
     </QPageSticky>
 
     <!-- MULTIMEDIA DIALOG START-->
diff --git a/src/pages/Claim/Card/ClaimRma.vue b/src/pages/Claim/Card/ClaimRma.vue
index c59c11845..bba901d7b 100644
--- a/src/pages/Claim/Card/ClaimRma.vue
+++ b/src/pages/Claim/Card/ClaimRma.vue
@@ -1,48 +1,34 @@
 <script setup>
 import axios from 'axios';
-import { ref } from 'vue';
+import { watch, ref, computed, onUnmounted, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useQuasar } from 'quasar';
-import { useRoute } from 'vue-router';
-import { useArrayData } from 'src/composables/useArrayData';
-import { useStateStore } from 'stores/useStateStore';
-import VnPaginate from 'src/components/ui/VnPaginate.vue';
-import FetchData from 'components/FetchData.vue';
-import VnConfirm from 'src/components/ui/VnConfirm.vue';
+import CrudModel from 'components/CrudModel.vue';
+import { useState } from 'src/composables/useState';
 
 import { toDate } from 'src/filters';
 
 const quasar = useQuasar();
-const route = useRoute();
+const state = useState();
 const { t } = useI18n();
-const stateStore = useStateStore();
-const arrayData = useArrayData('ClaimRma');
+const selected = ref([]);
+const claimRmaRef = ref();
+const claim = computed(() => state.get('ClaimDescriptor'));
 
-const claim = ref();
-const claimFilter = {
-    fields: ['rma'],
-};
-
-async function onFetch(data) {
-    claim.value = data;
-
-    const filter = {
-        include: {
-            relation: 'worker',
-            scope: {
-                include: {
-                    relation: 'user',
-                },
+const claimRmaFilter = {
+    include: {
+        relation: 'worker',
+        scope: {
+            include: {
+                relation: 'user',
             },
         },
-        order: 'created DESC',
-        where: {
-            code: claim.value.rma,
-        },
-    };
-
-    arrayData.applyFilter({ filter });
-}
+    },
+    order: 'created DESC',
+    where: {
+        code: claim.value?.rma,
+    },
+};
 
 async function addRow() {
     if (!claim.value.rma) {
@@ -56,7 +42,7 @@ async function addRow() {
     };
 
     await axios.post(`ClaimRmas`, formData);
-    await arrayData.refresh();
+    await claimRmaRef.value.reload();
 
     quasar.notify({
         type: 'positive',
@@ -65,38 +51,33 @@ async function addRow() {
     });
 }
 
-function confirmRemove(id) {
-    quasar
-        .dialog({
-            component: VnConfirm,
-            componentProps: {
-                data: { id },
-                promise: remove,
-            },
-        })
-        .onOk(async () => await arrayData.refresh());
-}
-
-async function remove({ id }) {
-    await axios.delete(`ClaimRmas/${id}`);
-    quasar.notify({
-        type: 'positive',
-        message: t('globals.rowRemoved'),
-    });
-}
+onMounted(() => {
+    if (claim.value) claimRmaRef.value.reload();
+});
+watch(
+    claim,
+    () => {
+        claimRmaRef.value.reload();
+    },
+    { deep: true }
+);
 </script>
 <template>
-    <FetchData
-        :url="`Claims/${route.params.id}`"
-        :filter="claimFilter"
-        @on-fetch="onFetch"
-        auto-load
-    />
     <div class="column items-center">
         <div class="list">
-            <VnPaginate data-key="ClaimRma" url="ClaimRmas">
+            <CrudModel
+                data-key="ClaimRma"
+                url="ClaimRmas"
+                model="ClaimRma"
+                :filter="claimRmaFilter"
+                v-model:selected="selected"
+                ref="claimRmaRef"
+                :default-save="false"
+                :default-reset="false"
+                :default-remove="false"
+            >
                 <template #body="{ rows }">
-                    <QCard class="card">
+                    <QCard>
                         <template v-for="(row, index) of rows" :key="row.id">
                             <QItem class="q-pa-none items-start">
                                 <QItemSection class="q-pa-md">
@@ -107,7 +88,7 @@ async function remove({ id }) {
                                                     {{ t('claim.rma.user') }}
                                                 </QItemLabel>
                                                 <QItemLabel>
-                                                    {{ row.worker.user.name }}
+                                                    {{ row?.worker?.user?.name }}
                                                 </QItemLabel>
                                             </QItemSection>
                                         </QItem>
@@ -133,7 +114,7 @@ async function remove({ id }) {
                                         round
                                         color="orange"
                                         icon="vn:bin"
-                                        @click="confirmRemove(row.id)"
+                                        @click="claimRmaRef.remove([row])"
                                     >
                                         <QTooltip>{{ t('globals.remove') }}</QTooltip>
                                     </QBtn>
@@ -143,33 +124,11 @@ async function remove({ id }) {
                         </template>
                     </QCard>
                 </template>
-            </VnPaginate>
+            </CrudModel>
         </div>
     </div>
-
-    <Teleport
-        v-if="stateStore.isHeaderMounted() && !quasar.platform.is.mobile"
-        to="#actions-prepend"
-    >
-        <div class="row q-gutter-x-sm">
-            <QBtn @click="addRow()" icon="add" color="primary" dense rounded>
-                <QTooltip bottom> {{ t('globals.add') }} </QTooltip>
-            </QBtn>
-            <QSeparator vertical />
-        </div>
-    </Teleport>
-
-    <QPageSticky
-        v-if="quasar.platform.is.mobile"
-        position="bottom"
-        :offset="[0, 0]"
-        expand
-    >
-        <QToolbar class="bg-primary text-white q-pa-none">
-            <QTabs class="full-width" align="justify" inline-label narrow-indicator>
-                <QTab @click="addRow()" icon="add_circle" :label="t('globals.add')" />
-            </QTabs>
-        </QToolbar>
+    <QPageSticky position="bottom-right" :offset="[25, 25]">
+        <QBtn fab color="primary" icon="add" @click="addRow()" />
     </QPageSticky>
 </template>
 
@@ -178,16 +137,6 @@ async function remove({ id }) {
     width: 100%;
     max-width: 60em;
 }
-.q-toolbar {
-    background-color: $grey-9;
-}
-.sticky-page {
-    padding-top: 66px;
-}
-
-.q-page-sticky {
-    z-index: 2998;
-}
 </style>
 
 <i18n>
diff --git a/src/pages/Customer/Card/CustomerCard.vue b/src/pages/Customer/Card/CustomerCard.vue
index 3a1c28d18..c833d0cf9 100644
--- a/src/pages/Customer/Card/CustomerCard.vue
+++ b/src/pages/Customer/Card/CustomerCard.vue
@@ -1,11 +1,13 @@
 <script setup>
 import { useI18n } from 'vue-i18n';
 import { useStateStore } from 'stores/useStateStore';
+import { useRoute } from 'vue-router';
 import CustomerDescriptor from './CustomerDescriptor.vue';
 import LeftMenu from 'components/LeftMenu.vue';
 import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
 
 const stateStore = useStateStore();
+const route = useRoute();
 const { t } = useI18n();
 </script>
 <template>
@@ -25,8 +27,13 @@ const { t } = useI18n();
         </QScrollArea>
     </QDrawer>
     <QPageContainer>
-        <QPage class="q-pa-md">
-            <RouterView></RouterView>
+        <QPage>
+            <QToolbar class="bg-vn-dark justify-end">
+                <div id="st-data"></div>
+                <QSpace />
+                <div id="st-actions"></div>
+            </QToolbar>
+            <div class="q-pa-md"><RouterView></RouterView></div>
         </QPage>
     </QPageContainer>
 </template>
diff --git a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue
index 5cbe72396..d24eb3ef2 100644
--- a/src/pages/InvoiceOut/Card/InvoiceOutCard.vue
+++ b/src/pages/InvoiceOut/Card/InvoiceOutCard.vue
@@ -25,8 +25,13 @@ const { t } = useI18n();
         </QScrollArea>
     </QDrawer>
     <QPageContainer>
-        <QPage class="q-pa-md">
-            <RouterView></RouterView>
+        <QPage>
+            <QToolbar class="bg-vn-dark justify-end">
+                <div id="st-data"></div>
+                <QSpace />
+                <div id="st-actions"></div>
+            </QToolbar>
+            <div class="q-pa-md"><RouterView></RouterView></div>
         </QPage>
     </QPageContainer>
 </template>
diff --git a/src/pages/Ticket/Card/TicketCard.vue b/src/pages/Ticket/Card/TicketCard.vue
index e0ad5054d..91921f827 100644
--- a/src/pages/Ticket/Card/TicketCard.vue
+++ b/src/pages/Ticket/Card/TicketCard.vue
@@ -25,8 +25,13 @@ const { t } = useI18n();
         </QScrollArea>
     </QDrawer>
     <QPageContainer>
-        <QPage class="q-pa-md">
-            <RouterView></RouterView>
+        <QPage>
+            <QToolbar class="bg-vn-dark justify-end">
+                <div id="st-data"></div>
+                <QSpace />
+                <div id="st-actions"></div>
+            </QToolbar>
+            <div class="q-pa-md"><RouterView></RouterView></div>
         </QPage>
     </QPageContainer>
 </template>
diff --git a/src/pages/Wagon/WagonCreate.vue b/src/pages/Wagon/WagonCreate.vue
index 3f7824975..123e01d36 100644
--- a/src/pages/Wagon/WagonCreate.vue
+++ b/src/pages/Wagon/WagonCreate.vue
@@ -20,7 +20,7 @@ const $props = defineProps({
 });
 const entityId = computed(() => $props.id || route.params.id);
 
-let wagonTypes;
+let wagonTypes = [];
 let originalData = {};
 const wagon = ref({});
 const filteredWagonTypes = ref(wagonTypes);
diff --git a/src/pages/Worker/Card/WorkerCard.vue b/src/pages/Worker/Card/WorkerCard.vue
index 3d6b46e11..972eb52ec 100644
--- a/src/pages/Worker/Card/WorkerCard.vue
+++ b/src/pages/Worker/Card/WorkerCard.vue
@@ -25,8 +25,13 @@ const { t } = useI18n();
         </QScrollArea>
     </QDrawer>
     <QPageContainer>
-        <QPage class="q-pa-md">
-            <RouterView></RouterView>
+        <QPage>
+            <QToolbar class="bg-vn-dark justify-end">
+                <div id="st-data"></div>
+                <QSpace />
+                <div id="st-actions"></div>
+            </QToolbar>
+            <div class="q-pa-md"><RouterView></RouterView></div>
         </QPage>
     </QPageContainer>
 </template>
diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue
index 05ccdc373..7c8accc5d 100644
--- a/src/pages/Worker/Card/WorkerSummary.vue
+++ b/src/pages/Worker/Card/WorkerSummary.vue
@@ -1,6 +1,5 @@
 <script setup>
-import axios from 'axios';
-import { ref, onMounted, computed, onUpdated } from 'vue';
+import { ref, onMounted, computed } from 'vue';
 import { useRoute } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import CardSummary from 'components/ui/CardSummary.vue';
diff --git a/src/router/modules/claim.js b/src/router/modules/claim.js
index 40aaef73b..9df1dd64e 100644
--- a/src/router/modules/claim.js
+++ b/src/router/modules/claim.js
@@ -18,6 +18,7 @@ export default {
             'ClaimPhotos',
             'ClaimLog',
             'ClaimNotes',
+            'ClaimDevelopment',
         ],
     },
     children: [
@@ -101,6 +102,16 @@ export default {
                     },
                     component: () => import('src/pages/Claim/Card/ClaimPhoto.vue'),
                 },
+                {
+                    name: 'ClaimDevelopment',
+                    path: 'development',
+                    meta: {
+                        title: 'development',
+                        icon: 'vn:traceability',
+                        roles: ['claimManager'],
+                    },
+                    component: () => import('src/pages/Claim/Card/ClaimDevelopment.vue'),
+                },
                 {
                     name: 'ClaimLog',
                     path: 'log',
diff --git a/src/stores/useStateStore.js b/src/stores/useStateStore.js
index 8704c46e4..74b65e71f 100644
--- a/src/stores/useStateStore.js
+++ b/src/stores/useStateStore.js
@@ -30,6 +30,13 @@ export const useStateStore = defineStore('stateStore', () => {
         return rightDrawer.value;
     }
 
+    function isSubToolbarShown() {
+        return (
+            !!document.querySelector('#st-data') &&
+            !!document.querySelector('#st-actions')
+        );
+    }
+
     return {
         leftDrawer,
         rightDrawer,
@@ -39,5 +46,6 @@ export const useStateStore = defineStore('stateStore', () => {
         toggleRightDrawer,
         isLeftDrawerShown,
         isRightDrawerShown,
+        isSubToolbarShown,
     };
 });
diff --git a/test/cypress/integration/claimDevelopment.spec.js b/test/cypress/integration/claimDevelopment.spec.js
new file mode 100755
index 000000000..77ce2bb45
--- /dev/null
+++ b/test/cypress/integration/claimDevelopment.spec.js
@@ -0,0 +1,53 @@
+/// <reference types="cypress" />
+describe('ClaimDevelopment', () => {
+    const claimId = 1;
+    const firstLineReason = 'tbody > :nth-child(1) > :nth-child(2)';
+    const thirdRow = 'tbody > :nth-child(3)';
+
+    beforeEach(() => {
+        cy.viewport(1920, 1080);
+        cy.login('developer');
+        cy.visit(`/#/claim/${claimId}/development`);
+    });
+
+    it('should reset line', () => {
+        cy.selectOption(firstLineReason, 'Novato');
+        cy.resetCard();
+        cy.getValue(firstLineReason).should('have.text', 'Prisas');
+    });
+
+    it('should edit line', () => {
+        cy.selectOption(firstLineReason, 'Novato');
+        cy.saveCard();
+
+        cy.reload();
+        cy.getValue(firstLineReason).should('have.text', 'Novato');
+
+        //Restart data
+        cy.selectOption(firstLineReason, 'Prisas');
+        cy.saveCard();
+    });
+
+    it('should add and remove new line', () => {
+        //add row
+        cy.addCard();
+        cy.get(thirdRow).should('exist');
+
+        const rowData = [false, 'Novato', 'Roces', 'Compradores', 'employeeNick', 'Tour'];
+        cy.fillRow(thirdRow, rowData);
+        cy.saveCard();
+        cy.validateRow(thirdRow, rowData);
+
+        cy.reload();
+        cy.validateRow(thirdRow, rowData);
+
+        //remove row
+        cy.fillRow(thirdRow, [true]);
+        cy.removeCard();
+        cy.clickConfirm();
+        cy.get(thirdRow).should('not.exist');
+
+        cy.reload();
+        cy.get(thirdRow).should('not.exist');
+    });
+});
diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js
index a3a61c423..2eb14e463 100755
--- a/test/cypress/support/commands.js
+++ b/test/cypress/support/commands.js
@@ -40,4 +40,91 @@ Cypress.Commands.add('login', (user) => {
         window.localStorage.setItem('token', response.body.token);
     });
 });
+
+Cypress.Commands.add('waitForElement', (element) => {
+    cy.get(element, { timeout: 2000 }).should('be.visible');
+});
+
+Cypress.Commands.add('getValue', (selector) => {
+    cy.get(selector).then(($el) => {
+        if ($el.find('.q-checkbox__inner').length > 0) {
+            return cy.get(selector + '.q-checkbox__inner');
+        }
+        // Si es un QSelect
+        else if ($el.find('.q-select__dropdown-icon').length) {
+            return cy.get(
+                selector +
+                    '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > span'
+            );
+        } else {
+            // Puedes añadir un log o lanzar un error si el elemento no es reconocido
+            cy.log('Elemento no soportado');
+        }
+    });
+});
+
+// Fill Inputs
+Cypress.Commands.add('selectOption', (selector, option) => {
+    cy.get(selector).find('.q-select__dropdown-icon').click();
+    cy.get('.q-menu .q-item').contains(option).click();
+});
+
+Cypress.Commands.add('checkOption', (selector) => {
+    cy.wrap(selector).find('.q-checkbox__inner').click();
+});
+
+// Global buttons
+Cypress.Commands.add('saveCard', () => {
+    cy.get('[title="Save"]').click();
+    cy.get('[title="Save"]').should('have.class', 'disabled');
+});
+Cypress.Commands.add('resetCard', () => {
+    cy.get('[title="Reset"]').click();
+});
+Cypress.Commands.add('removeCard', () => {
+    cy.get('[title="Remove"]').click();
+});
+Cypress.Commands.add('addCard', () => {
+    cy.waitForElement('tbody');
+    cy.get('.q-page-sticky > div > .q-btn').click();
+});
+Cypress.Commands.add('clickConfirm', () => {
+    cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
+});
+
+Cypress.Commands.add('fillRow', (rowSelector, data) => {
+    // Usar el selector proporcionado para obtener la fila deseada
+    cy.waitForElement('tbody');
+    cy.get(rowSelector).as('currentRow');
+
+    data.forEach((value, index) => {
+        if (value === null) return;
+        cy.get('@currentRow')
+            .find('td')
+            .eq(index)
+            .then((td) => {
+                if (td.find('.q-select__dropdown-icon').length) {
+                    cy.selectOption(td, value);
+                }
+                if (td.find('.q-checkbox__inner').length && value) {
+                    cy.checkOption(td);
+                }
+            });
+    });
+});
+
+Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
+    cy.waitForElement('tbody');
+    cy.get(rowSelector).within(() => {
+        for (const [index, value] of expectedValues.entries()) {
+            cy.log('CHECKING ', index, value);
+            if (typeof value == 'boolean') {
+                const prefix = value ? '' : 'not.';
+                cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`);
+                continue;
+            }
+            cy.getValue(`:nth-child(${index + 1})`).should('have.text', value);
+        }
+    });
+});
 // registerCommands();
diff --git a/test/vitest/__tests__/components/common/CrudModel.spec.js b/test/vitest/__tests__/components/common/CrudModel.spec.js
new file mode 100644
index 000000000..6ce93e59c
--- /dev/null
+++ b/test/vitest/__tests__/components/common/CrudModel.spec.js
@@ -0,0 +1,120 @@
+import { createWrapper } from 'app/test/vitest/helper';
+import CrudModel from 'components/CrudModel.vue';
+import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';
+
+describe('CrudModel', () => {
+    let vm;
+    beforeAll(() => {
+        vm = createWrapper(CrudModel, {
+            global: {
+                stubs: [
+                    'vnPaginate',
+                    'useState',
+                    'arrayData',
+                    'useStateStore',
+                    'vue-i18n',
+                ],
+                mocks: {
+                    validate: vi.fn(),
+                },
+            },
+            propsData: {
+                dataRequired: {
+                    fk: 1,
+                },
+                dataKey: 'crudModelKey',
+                model: 'crudModel',
+                url: 'crudModelUrl',
+            },
+        }).vm;
+    });
+
+    beforeEach(() => {
+        vm.fetch([]);
+    });
+
+    afterEach(() => {
+        vi.clearAllMocks();
+    });
+
+    describe('insert()', () => {
+        it('should new element in list with index 0 if formData not has data', () => {
+            vm.insert();
+
+            expect(vm.formData.length).toEqual(1);
+            expect(vm.formData[0].fk).toEqual(1);
+            expect(vm.formData[0].$index).toEqual(0);
+        });
+    });
+
+    describe('getChanges()', () => {
+        it('should return correct updates and creates', async () => {
+            vm.fetch([
+                { id: 1, name: 'New name one' },
+                { id: 2, name: 'New name two' },
+                { id: 3, name: 'Bruce Wayne' },
+            ]);
+
+            vm.originalData = [
+                { id: 1, name: 'Tony Starks' },
+                { id: 2, name: 'Jessica Jones' },
+                { id: 3, name: 'Bruce Wayne' },
+            ];
+
+            vm.insert();
+            const result = vm.getChanges();
+
+            const expected = {
+                creates: [
+                    {
+                        $index: 3,
+                        fk: 1,
+                    },
+                ],
+                updates: [
+                    {
+                        data: {
+                            name: 'New name one',
+                        },
+                        where: {
+                            id: 1,
+                        },
+                    },
+                    {
+                        data: {
+                            name: 'New name two',
+                        },
+                        where: {
+                            id: 2,
+                        },
+                    },
+                ],
+            };
+
+            expect(result).toEqual(expected);
+        });
+    });
+
+    describe('getDifferences()', () => {
+        it('should return the differences between two objects', async () => {
+            const obj1 = {
+                a: 1,
+                b: 2,
+                c: 3,
+            };
+            const obj2 = {
+                a: null,
+                b: 4,
+                d: 5,
+            };
+
+            const result = vm.getDifferences(obj1, obj2);
+
+            expect(result).toEqual({
+                a: null,
+                b: 4,
+                d: 5,
+            });
+        });
+    });
+});
diff --git a/test/vitest/__tests__/pages/Claims/ClaimLines.spec.js b/test/vitest/__tests__/pages/Claims/ClaimLines.spec.js
index dda59a98d..6dd6b89e6 100644
--- a/test/vitest/__tests__/pages/Claims/ClaimLines.spec.js
+++ b/test/vitest/__tests__/pages/Claims/ClaimLines.spec.js
@@ -5,7 +5,6 @@ import ClaimLines from 'pages/Claim/Card/ClaimLines.vue';
 describe('ClaimLines', () => {
     let vm;
 
-
     beforeAll(() => {
         vm = createWrapper(ClaimLines, {
             global: {
@@ -13,25 +12,26 @@ describe('ClaimLines', () => {
                 mocks: {
                     fetch: vi.fn(),
                 },
-            }
+            },
         }).vm;
     });
 
     beforeEach(() => {
         vm.claim = {
             id: 1,
-            ticketFk: 1
-        }
+            ticketFk: 1,
+        };
         vm.store.data = [
             {
                 id: 1,
                 quantity: 10,
                 sale: {
-                    id: 1, discount: 0
-                }
-            }
-        ]
-    })
+                    id: 1,
+                    discount: 0,
+                },
+            },
+        ];
+    });
 
     afterEach(() => {
         vi.clearAllMocks();
@@ -42,13 +42,17 @@ describe('ClaimLines', () => {
             vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
             vi.spyOn(vm.quasar, 'notify');
 
-            const canceller = new AbortController()
+            const canceller = new AbortController();
             await vm.updateDiscount({ saleFk: 1, discount: 5, canceller });
 
-            const expectedData = { salesIds: [1], newDiscount: 5 }
-            expect(axios.post).toHaveBeenCalledWith('Tickets/1/updateDiscount', expectedData, {
-                signal: canceller.signal
-            })
+            const expectedData = { salesIds: [1], newDiscount: 5 };
+            expect(axios.post).toHaveBeenCalledWith(
+                'Tickets/1/updateDiscount',
+                expectedData,
+                {
+                    signal: canceller.signal,
+                }
+            );
         });
     });
 
@@ -56,37 +60,14 @@ describe('ClaimLines', () => {
         it('should make a POST request and then set the discount on the original row', async () => {
             vi.spyOn(vm.quasar, 'notify');
 
-
             vm.onUpdateDiscount({ discount: 5, rowIndex: 0 });
-            const firstRow = vm.store.data[0]
+            const firstRow = vm.store.data[0];
 
-            expect(firstRow.sale.discount).toEqual(5)
+            expect(firstRow.sale.discount).toEqual(5);
             expect(vm.quasar.notify).toHaveBeenCalledWith(
                 expect.objectContaining({
                     message: 'Discount updated',
-                    type: 'positive'
-                })
-            );
-        });
-    });
-
-    describe('remove()', () => {
-        it('should make a POST request and then call to the quasar notify() method', async () => {
-            vi.spyOn(axios, 'post').mockResolvedValue({ data: true });
-            vi.spyOn(vm.quasar, 'notify');
-
-            await vm.remove({
-                rows: [
-                    { id: 1 }
-                ]
-            });
-            const expectedData = { deletes: [1] }
-
-            expect(axios.post).toHaveBeenCalledWith('ClaimBeginnings/crud', expectedData)
-            expect(vm.quasar.notify).toHaveBeenCalledWith(
-                expect.objectContaining({
-                    message: 'Row removed',
-                    type: 'positive'
+                    type: 'positive',
                 })
             );
         });
diff --git a/test/vitest/__tests__/pages/Wagons/WagonCreate.spec.js b/test/vitest/__tests__/pages/Wagons/WagonCreate.spec.js
index bc6b92639..f195c183f 100644
--- a/test/vitest/__tests__/pages/Wagons/WagonCreate.spec.js
+++ b/test/vitest/__tests__/pages/Wagons/WagonCreate.spec.js
@@ -7,9 +7,11 @@ describe('WagonCreate', () => {
     const entityId = 1;
 
     beforeAll(() => {
-        vmEdit = createWrapper(WagonCreate, {propsData: {
+        vmEdit = createWrapper(WagonCreate, {
+            propsData: {
                 id: entityId,
-            }}).vm;
+            },
+        }).vm;
         vmCreate = createWrapper(WagonCreate).vm;
     });
 
@@ -29,9 +31,7 @@ describe('WagonCreate', () => {
 
             await vmCreate.onSubmit();
 
-            expect(axios.patch).toHaveBeenCalledWith(
-                `Wagons`, vmCreate.wagon
-            );
+            expect(axios.patch).toHaveBeenCalledWith(`Wagons`, vmCreate.wagon);
         });
 
         it('should update a wagon', async () => {
@@ -46,9 +46,7 @@ describe('WagonCreate', () => {
 
             await vmEdit.onSubmit();
 
-            expect(axios.patch).toHaveBeenCalledWith(
-                `Wagons`, vmEdit.wagon
-            );
+            expect(axios.patch).toHaveBeenCalledWith(`Wagons`, vmEdit.wagon);
         });
     });
 
@@ -88,16 +86,12 @@ describe('WagonCreate', () => {
 
     describe('fetch()', () => {
         it('should fetch data', async () => {
-            vi.spyOn(axios, 'get').mockResolvedValue({ data: true });
+            vi.spyOn(axios, 'get').mockResolvedValue({ data: [] });
 
             await vmEdit.fetch();
 
-            expect(axios.get).toHaveBeenCalledWith(
-                `WagonTypes`
-            );
-            expect(axios.get).toHaveBeenCalledWith(
-                `Wagons/${entityId}`
-            );
+            expect(axios.get).toHaveBeenCalledWith(`WagonTypes`);
+            expect(axios.get).toHaveBeenCalledWith(`Wagons/${entityId}`);
         });
     });
 });
diff --git a/test/vitest/helper.js b/test/vitest/helper.js
index 8a6fb1415..8f4dc3221 100644
--- a/test/vitest/helper.js
+++ b/test/vitest/helper.js
@@ -5,6 +5,7 @@ import { vi } from 'vitest';
 import { i18n } from 'src/boot/i18n';
 import { Notify, Dialog } from 'quasar';
 import axios from 'axios';
+import * as useValidator from 'src/composables/useValidator';
 
 installQuasarPlugin({
     plugins: {
@@ -34,6 +35,10 @@ vi.mock('vue-router', () => ({
     }),
 }));
 
+vi.spyOn(useValidator, 'useValidator').mockImplementation(() => {
+    return { validate: vi.fn(), fetch: vi.fn() };
+});
+
 class FormDataMock {
     append() {
         vi.fn();
@@ -64,6 +69,10 @@ export function createWrapper(component, options) {
         global: {
             plugins: [i18n, pinia],
         },
+        mocks: {
+            t: (tKey) => tKey,
+            $t: (tKey) => tKey,
+        },
     };
 
     const mountOptions = Object.assign({}, defaultOptions);