Merge pull request 'dev/nicolas/employee-management' (#30) from dev/nicolas/employee-management into main
Reviewed-on: Targo/targo_frontend#30
This commit is contained in:
commit
0a87ce76f5
|
|
@ -41,6 +41,11 @@ export default defineConfigWithVueTs(
|
|||
'error',
|
||||
{ prefer: 'type-imports' }
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
||||
],
|
||||
}
|
||||
},
|
||||
// https://github.com/vuejs/eslint-config-typescript
|
||||
|
|
@ -63,15 +68,15 @@ export default defineConfigWithVueTs(
|
|||
}
|
||||
},
|
||||
|
||||
files: ['**/*.ts', '**/*.vue'],
|
||||
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
|
||||
// warn about unused but underscored variables
|
||||
'no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_' }
|
||||
],
|
||||
|
||||
'no-unused-vars': 'off',
|
||||
|
||||
// allow debugger during development only
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
|
|
|
|||
|
|
@ -29,11 +29,12 @@ export default defineConfig((ctx) => {
|
|||
// 'fontawesome-v6',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
// 'line-awesome',
|
||||
'line-awesome',
|
||||
'material-icons',
|
||||
'material-icons-outlined',
|
||||
|
||||
'roboto-font',
|
||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||
|
||||
'roboto-font', // optional, you are not bound to it
|
||||
'material-icons', // optional, you are not bound to it
|
||||
],
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
||||
|
|
|
|||
BIN
src/assets/en-CA.png
Normal file
BIN
src/assets/en-CA.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/assets/fr-FR.png
Normal file
BIN
src/assets/fr-FR.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 880 B |
|
|
@ -9,6 +9,43 @@ export default {
|
|||
role: "Role",
|
||||
supervisor: "Supervisor",
|
||||
company: "Company",
|
||||
is_supervisor: "is a supervisor",
|
||||
active: "active",
|
||||
inactive: "inactive",
|
||||
},
|
||||
},
|
||||
|
||||
employee_management: {
|
||||
add_employee: "Add employee",
|
||||
modify_employee: "Modify employee",
|
||||
access_label: "access",
|
||||
details_label: "details",
|
||||
schedule_label: "schedule",
|
||||
schedule_presets: {
|
||||
preset_list_placeholder: "Select a schedule",
|
||||
preset_name_placeholder: "schedule preset name",
|
||||
},
|
||||
module_access: {
|
||||
dashboard: "Dashboard",
|
||||
employee_list: "employee list",
|
||||
employee_management: "employee management",
|
||||
personal_profile: "profile",
|
||||
timesheets: "timesheets",
|
||||
timesheets_approval: "timesheet approval",
|
||||
user_access: "module access",
|
||||
by_role: "by role",
|
||||
by_module: "by module",
|
||||
preset_admin: "administrator",
|
||||
preset_employee: "employee",
|
||||
uncheck_all: "remove all",
|
||||
admin_description: "Check all modules",
|
||||
employee_description: "Only check modules that are relevant to standard employees with no management access",
|
||||
none_description: "Uncheck all modules",
|
||||
usage_description: "You can use roles to enable preset modules, add or remove modules individually, or both",
|
||||
},
|
||||
filter: {
|
||||
hide_terminated: "Hide inactive employees",
|
||||
sort_by_tags: "sort by tags",
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -58,6 +95,8 @@ export default {
|
|||
company: "company",
|
||||
supervisor: "supervisor",
|
||||
hired_date: "hiring date",
|
||||
fired_date: "departure date",
|
||||
bankroll_id: "payroll ID",
|
||||
},
|
||||
preferences: {
|
||||
tab_title: "preferences",
|
||||
|
|
@ -67,6 +106,7 @@ export default {
|
|||
'en-CA': "English",
|
||||
dark_mode: "dark",
|
||||
light_mode: "light",
|
||||
auto_mode: "auto",
|
||||
update_successful: "Preferences saved",
|
||||
update_failed: "Failed to save preferences",
|
||||
},
|
||||
|
|
@ -119,13 +159,13 @@ export default {
|
|||
remote: "remote work",
|
||||
},
|
||||
weekday: {
|
||||
sunday: "dimanche",
|
||||
monday: "lundi",
|
||||
tuesday: "mardi",
|
||||
wednesday: "mercredi",
|
||||
thursday: "jeudi",
|
||||
friday: "vendredi",
|
||||
saturday: "samedi",
|
||||
sun: "dimanche",
|
||||
mon: "lundi",
|
||||
tue: "mardi",
|
||||
wed: "mercredi",
|
||||
thu: "jeudi",
|
||||
fri: "vendredi",
|
||||
sat: "samedi",
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,43 @@ export default {
|
|||
role: "rôle",
|
||||
supervisor: "superviseur",
|
||||
company: "Compagnie",
|
||||
is_supervisor: "est un superviseur",
|
||||
active: "actif",
|
||||
inactive: "inactif",
|
||||
},
|
||||
},
|
||||
|
||||
employee_management: {
|
||||
add_employee: "Ajouter employé",
|
||||
modify_employee: "Modifier employé",
|
||||
access_label: "accès",
|
||||
details_label: "détails",
|
||||
schedule_label: "horaire",
|
||||
schedule_presets: {
|
||||
preset_list_placeholder: "Sélectionner un horaire",
|
||||
preset_name_placeholder: "nom de l'horaire",
|
||||
},
|
||||
module_access: {
|
||||
dashboard: "Accueil",
|
||||
employee_list: "Répertoire du personnel",
|
||||
employee_management: "Gestion employés",
|
||||
personal_profile: "profil personnel",
|
||||
timesheets: "carte de temps",
|
||||
timesheets_approval: "validation cartes de temps",
|
||||
user_access: "module access",
|
||||
by_role: "par rôle",
|
||||
by_module: "par module",
|
||||
preset_admin: "administrateur",
|
||||
preset_employee: "employé",
|
||||
uncheck_all: "Tout enlever",
|
||||
admin_description: "Selectionner tous les modules",
|
||||
employee_description: "Selectionner seulement les modules qui sont pertinents aux employés sans accès spéciaux",
|
||||
none_description: "Enlever tous les accès",
|
||||
usage_description: "Vous pouvez utiliser les rôles pour sélectionner des modules prédéfinis, enlever ou ajouter des modules individuellement, ou les deux",
|
||||
},
|
||||
filter: {
|
||||
hide_terminated: "Cacher les employés inactifs",
|
||||
sort_by_tags: "filtrer par identifiants",
|
||||
},
|
||||
},
|
||||
|
||||
|
|
@ -58,6 +95,8 @@ export default {
|
|||
company: "compagnie",
|
||||
supervisor: "nom du superviseur",
|
||||
hired_date: "date d'embauche",
|
||||
fired_date: "date de départ",
|
||||
bankroll_id: "identifiant de paie",
|
||||
},
|
||||
preferences: {
|
||||
tab_title: "préférences",
|
||||
|
|
@ -67,6 +106,7 @@ export default {
|
|||
'en-CA': "Anglais",
|
||||
dark_mode: "sombre",
|
||||
light_mode: "clair",
|
||||
auto_mode: "automatique",
|
||||
update_successful: "Préférences enregistrées",
|
||||
update_failed: "Échec de sauvegarde",
|
||||
},
|
||||
|
|
@ -120,13 +160,13 @@ export default {
|
|||
remote: "télétravail",
|
||||
},
|
||||
weekday: {
|
||||
sunday: "dimanche",
|
||||
monday: "lundi",
|
||||
tuesday: "mardi",
|
||||
wednesday: "mercredi",
|
||||
thursday: "jeudi",
|
||||
friday: "vendredi",
|
||||
saturday: "samedi",
|
||||
sun: "dimanche",
|
||||
mon: "lundi",
|
||||
tue: "mardi",
|
||||
wed: "mercredi",
|
||||
thu: "jeudi",
|
||||
fri: "vendredi",
|
||||
sat: "samedi",
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
name="view_list"
|
||||
name="groups"
|
||||
color="accent"
|
||||
size="lg"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,20 +16,16 @@
|
|||
const user_preferences = ref(ui_store.user_preferences);
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('current preferences on load: ', ui_store.user_preferences);
|
||||
if (ui_store.user_preferences.id === -1) {
|
||||
console.log('fetching preferences');
|
||||
await ui_store.getUserPreferences();
|
||||
}
|
||||
});
|
||||
|
||||
watch(user_preferences, async () => {
|
||||
if (ui_store.user_preferences.id !== -1) {
|
||||
console.log('triggered watcher');
|
||||
await ui_store.updateUserPreferences(t);
|
||||
return
|
||||
}
|
||||
console.log('watcher triggered but store has no preferences')
|
||||
await ui_store.getUserPreferences();
|
||||
}, {deep: true});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { employee_access_options, type ModuleAccessPreset, type ModuleAccessName, employee_access_presets, getEmployeeAccessOptionIcon } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
const preset_preview = ref<ModuleAccessPreset>();
|
||||
|
||||
const toggleInSelected = (value: ModuleAccessName) => {
|
||||
const i = employee_store.employee.user_module_access.indexOf(value);
|
||||
if (i === -1) employee_store.employee.user_module_access.push(value);
|
||||
else employee_store.employee.user_module_access.splice(i, 1);
|
||||
}
|
||||
|
||||
const applyAccessPreset = (preset: ModuleAccessPreset) => {
|
||||
employee_store.employee.user_module_access = unwrapAndClone(employee_access_presets[preset]);
|
||||
}
|
||||
|
||||
|
||||
const getPreviewBackgroundColor = (name: ModuleAccessName) => {
|
||||
if (employee_access_presets[preset_preview.value!].includes(name)) {
|
||||
if (!employee_store.employee.user_module_access.includes(name)) return 'bg-info text-white';
|
||||
|
||||
return 'bg-accent text-white';
|
||||
}
|
||||
|
||||
if (employee_store.employee.user_module_access.includes(name)) return 'bg-negative text-white';
|
||||
|
||||
return 'bg-dark';
|
||||
};
|
||||
|
||||
const getBackgroundColor = (name: ModuleAccessName) => {
|
||||
if (employee_store.employee.user_module_access.includes(name)) return 'bg-accent text-white';
|
||||
|
||||
return 'bg-dark';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row full-width items-start content-start overflow-hidden-y">
|
||||
<div class="col-12 row flex-center q-px-sm q-py-xs no-wrap">
|
||||
<q-icon
|
||||
name="info_outline"
|
||||
size="sm"
|
||||
color="accent"
|
||||
class="col-auto q-mr-sm"
|
||||
/>
|
||||
|
||||
<q-item-label
|
||||
caption
|
||||
class="col-auto text-weight-medium"
|
||||
>{{ $t('employee_management.module_access.usage_description') }}</q-item-label>
|
||||
</div>
|
||||
|
||||
<div class="column col-3 overflow-hidden-y">
|
||||
<span class="text-uppercase text-weight-medium q-mx-sm">
|
||||
{{ $t('employee_management.module_access.by_role') }}
|
||||
</span>
|
||||
|
||||
<q-separator
|
||||
size="2px"
|
||||
color="accent"
|
||||
class="q-mx-sm"
|
||||
style="transform: translateY(-4px);"
|
||||
/>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
class="shadow-2 rounded-5 q-ma-sm bg-dark"
|
||||
@click="applyAccessPreset('admin')"
|
||||
@mouseover="preset_preview = 'admin'"
|
||||
@mouseleave="preset_preview = undefined"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">
|
||||
{{ $t('employee_management.module_access.preset_admin') }}
|
||||
</q-item-label>
|
||||
|
||||
<q-item-label caption>
|
||||
{{ $t('employee_management.module_access.admin_description') }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
class="shadow-2 rounded-5 q-ma-sm bg-dark"
|
||||
@click="applyAccessPreset('employee')"
|
||||
@mouseover="preset_preview = 'employee'"
|
||||
@mouseleave="preset_preview = undefined"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-uppercase text-weight-bold">
|
||||
{{ $t('employee_management.module_access.preset_employee') }}
|
||||
</q-item-label>
|
||||
|
||||
<q-item-label caption>
|
||||
{{ $t('employee_management.module_access.employee_description') }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
color="negative"
|
||||
icon="clear"
|
||||
size="md"
|
||||
align="left"
|
||||
:label="$t('employee_management.module_access.uncheck_all')"
|
||||
class="q-ma-sm q-px-xs rounded-5"
|
||||
@click="applyAccessPreset('none')"
|
||||
@mouseover="preset_preview = 'none'"
|
||||
@mouseleave="preset_preview = undefined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-0 col-sm-1"></div>
|
||||
|
||||
<div class="row col items-start content-start">
|
||||
<div class="col-12 q-mb-xs">
|
||||
<span class="text-uppercase text-weight-medium q-mx-sm">
|
||||
{{ $t('employee_management.module_access.by_module') }}
|
||||
</span>
|
||||
|
||||
<q-separator
|
||||
size="2px"
|
||||
color="accent"
|
||||
class="q-mx-sm"
|
||||
style="transform: translateY(-4px);"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="option in employee_access_options"
|
||||
:key="option.label"
|
||||
class="col-lg-6 col-sm-12 col-xs-12 q-pa-xs"
|
||||
>
|
||||
<div
|
||||
class="row full-width cursor-pointer flex-center q-pa-sm rounded-5 no-wrap shadow-5"
|
||||
:class="preset_preview !== undefined ? getPreviewBackgroundColor(option.value) : getBackgroundColor(option.value)"
|
||||
@click="toggleInSelected(option.value)"
|
||||
>
|
||||
<q-icon :name="getEmployeeAccessOptionIcon(option.value)" size="sm" class="q-mr-sm"/>
|
||||
<span class="text-uppercase text-weight-bold non-selectable">
|
||||
{{ $t('employee_management.module_access.' + option.value) }}
|
||||
</span>
|
||||
<q-space />
|
||||
<q-icon
|
||||
:name="employee_store.employee.user_module_access.includes(option.value) ? 'check' : ''"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
314
src/modules/employee-list/components/add-modify-dialog-form.vue
Normal file
314
src/modules/employee-list/components/add-modify-dialog-form.vue
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
const last_work_day = computed(() => employee_store.employee.last_work_day ?? '---');
|
||||
const is_first_day_picker_open = ref(false);
|
||||
const is_last_day_picker_open = ref(false);
|
||||
|
||||
const company_options = [
|
||||
{ label: 'Targo', value: 'Targo' },
|
||||
{ label: 'Solucom', value: 'Solucom' },
|
||||
]
|
||||
|
||||
const supervisor_options = computed(() => {
|
||||
const supervisors = employee_store.employee_list.filter(employee => employee.is_supervisor === true && employee.last_work_day === null);
|
||||
return supervisors.map(supervisor => supervisor.first_name + ' ' + supervisor.last_name);
|
||||
})
|
||||
|
||||
const setLastWorkDay = (date: string | number | null) => {
|
||||
if (typeof date === 'string' && date.length > 0) {
|
||||
employee_store.employee.last_work_day = date;
|
||||
}
|
||||
employee_store.employee.last_work_day = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-form>
|
||||
<div class="row flex-center">
|
||||
<transition
|
||||
enter-active-class="animated pulse fast"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-checkbox
|
||||
v-model="employee_store.employee.is_supervisor"
|
||||
:key="employee_store.employee.is_supervisor ? '1' : '0'"
|
||||
dense
|
||||
left-label
|
||||
:label="$t('employee_list.table.is_supervisor')"
|
||||
size="lg"
|
||||
color="accent"
|
||||
class="col-auto text-uppercase q-py-xs q-px-lg q-ma-xs rounded-25"
|
||||
:class="employee_store.employee.is_supervisor ? 'bg-accent text-white text-weight-bold' : ''"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="q-ma-xs"
|
||||
:class="$q.screen.lt.sm ? 'column' : 'row'"
|
||||
>
|
||||
<q-input
|
||||
v-model="employee_store.employee.first_name"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
class="col q-mx-md"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.personal.first_name') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="employee_store.employee.last_name"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
class="col q-mx-md"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.personal.last_name') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="q-ma-xs"
|
||||
:class="$q.screen.lt.sm ? 'column' : 'row'"
|
||||
>
|
||||
<q-input
|
||||
v-model="employee_store.employee.email"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
class="col q-mx-md"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.employee.email') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="employee_store.employee.phone_number"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
mask="(###) ### - ####"
|
||||
class="col q-mx-md"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.personal.phone_number') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="q-ma-xs"
|
||||
:class="$q.screen.lt.sm ? 'column' : 'row'"
|
||||
>
|
||||
<q-input
|
||||
v-model="employee_store.employee.job_title"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
class="col q-mx-md"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.employee.job_title') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-select
|
||||
v-model="employee_store.employee.company_name"
|
||||
color="accent"
|
||||
:options="company_options"
|
||||
stack-label
|
||||
emit-value
|
||||
label-slot
|
||||
class="col q-mx-md"
|
||||
popup-content-class="text-uppercase text-weight-medium rounded-20"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:menu-offset="[0, 10]"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.employee.company') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="q-ma-xs"
|
||||
:class="$q.screen.lt.sm ? 'column' : 'row'"
|
||||
>
|
||||
<q-select
|
||||
v-model="employee_store.employee.supervisor_full_name"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
:options="supervisor_options"
|
||||
options-selected-class="text-white text-bold bg-accent"
|
||||
class="col q-mx-md"
|
||||
popup-content-class="text-uppercase text-weight-medium rounded-20"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:menu-offset="[0, 10]"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.employee.supervisor') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-input
|
||||
v-if="employee_store.management_mode === 'modify_employee'"
|
||||
v-model="employee_store.employee.external_payroll_id"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
class="col q-mx-md"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.employee.bankroll_id') }}
|
||||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="q-ma-xs"
|
||||
:class="$q.screen.lt.md ? 'column' : 'row'"
|
||||
>
|
||||
<q-input
|
||||
v-model="employee_store.employee.first_work_day"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
mask="####-##-##"
|
||||
class="col q-mx-md"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.employee.hired_date') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="lg"
|
||||
icon="calendar_month"
|
||||
color="accent"
|
||||
@click="is_first_day_picker_open = true"
|
||||
>
|
||||
<q-dialog
|
||||
v-model="is_first_day_picker_open"
|
||||
backdrop-filter="none"
|
||||
>
|
||||
<q-date
|
||||
v-model="employee_store.employee.first_work_day"
|
||||
mask="YYYY-MM-DD"
|
||||
color="accent"
|
||||
@update:model-value="is_first_day_picker_open = false"
|
||||
/>
|
||||
</q-dialog>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input
|
||||
v-model="last_work_day"
|
||||
color="accent"
|
||||
stack-label
|
||||
label-slot
|
||||
mask="####-##-##"
|
||||
class="col q-mx-md"
|
||||
@update:model-value="setLastWorkDay"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
class="text-weight-bolder text-uppercase"
|
||||
style="font-size: 0.85em;"
|
||||
>
|
||||
{{ $t('profile.employee.fired_date') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="lg"
|
||||
icon="calendar_month"
|
||||
color="accent"
|
||||
@click="is_last_day_picker_open = true"
|
||||
>
|
||||
<q-dialog
|
||||
v-model="is_last_day_picker_open"
|
||||
backdrop-filter="none"
|
||||
>
|
||||
<q-date
|
||||
v-model="employee_store.employee.last_work_day"
|
||||
mask="YYYY-MM-DD"
|
||||
color="accent"
|
||||
@update:model-value="is_last_day_picker_open = false"
|
||||
/>
|
||||
</q-dialog>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</q-form>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { date } from 'quasar';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
|
||||
defineProps<{
|
||||
currentPresetId: number;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row flex-center fit">
|
||||
<div
|
||||
v-if="currentPresetId > 0"
|
||||
class="col column fit flex-center q-pa-md"
|
||||
>
|
||||
<div
|
||||
v-for="weekday in schedule_preset_store.current_schedule_preset.weekdays"
|
||||
:key="weekday.day"
|
||||
class="col row justify-center q-py-xs full-width"
|
||||
>
|
||||
<div class="col-10 row items-center bg-dark q-px-md shadow-10 rounded-10">
|
||||
<span class="col-2 text-weight-bolder text-accent text-uppercase text-overline" style="font-size: 1.3em;">{{
|
||||
$t(`shared.weekday.${weekday.day.toLowerCase()}`) }}</span>
|
||||
<div
|
||||
v-for="shift, index in weekday.shifts.sort((a, b) => date.extractDate(a.start_time, 'HH:mm').getTime() - date.extractDate(b.start_time, 'HH:mm').getTime())"
|
||||
:key="index"
|
||||
class="col q-px-md q-py-xs"
|
||||
>
|
||||
<div class="row flex-center rounded-5" style="border: 1px solid var(--q-accent);">
|
||||
<div class="col bg-accent text-white text-uppercase text-weight-bolder text-center">
|
||||
{{ $t(`shared.shift_type.${shift.type.toLowerCase()}`) }}
|
||||
</div>
|
||||
<div class="col text-center text-bold">{{ shift.start_time }}</div>
|
||||
<q-icon name="las la-chevron-right" color="accent" class="col-auto"></q-icon>
|
||||
<div class="col text-center text-bold">{{ shift.end_time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="col row justify-center"
|
||||
>
|
||||
<q-icon
|
||||
name="las la-calendar-week"
|
||||
size="20em"
|
||||
color="accent"
|
||||
style="opacity: 0.3;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import HorizontalSlideTransition from 'src/modules/shared/components/horizontal-slide-transition.vue';
|
||||
import SchedulePresetsDialog from 'src/modules/employee-list/components/schedule_presets_dialog.vue';
|
||||
import AddModifyDialogSchedulePreview from './add-modify-dialog-schedule-preview.vue';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useEmployeeListApi } from '../composables/use-employee-api';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
||||
const preset_options = ref<{ label: string, value: number }[]>([]);
|
||||
const current_preset = ref<{ label: string | undefined, value: number }>({ label: undefined, value: -1 });
|
||||
|
||||
const getPresetOptions = (): { label: string, value: number }[] => {
|
||||
const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
|
||||
options.push({ label: '', value: -1 });
|
||||
return options;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
preset_options.value = getPresetOptions();
|
||||
const current_option = preset_options.value.find(option => option.value === employee_store.employee.preset_id);
|
||||
current_preset.value = current_option ?? { label: undefined, value: -1 };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :key="schedule_preset_store.is_manager_open === false ? '0' : '1'" class="column full-width flex-center items-start">
|
||||
<SchedulePresetsDialog />
|
||||
|
||||
<div class="col row justify-center full-width no-wrap">
|
||||
<q-select
|
||||
v-model="current_preset"
|
||||
standout="bg-accent"
|
||||
dense
|
||||
options-dense
|
||||
rounded
|
||||
color="accent"
|
||||
:options="getPresetOptions()"
|
||||
class="col-xs-10 col-md-7"
|
||||
popup-content-class="text-uppercase text-weight-medium rounded-20"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:menu-offset="[0, 10]"
|
||||
@update:modelValue="option => employee_list_api.setSchedulePreset(option.value)"
|
||||
>
|
||||
<template #selected>
|
||||
<span
|
||||
class="text-uppercase text-center text-weight-bold full-width"
|
||||
:style="current_preset.label === undefined ? 'opacity: 0.5;' : ''"
|
||||
>
|
||||
{{ current_preset.label === undefined ?
|
||||
$t('employee_management.schedule_presets.preset_list_placeholder') :
|
||||
current_preset.label }}
|
||||
</span>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-btn
|
||||
push
|
||||
dense
|
||||
rounded
|
||||
icon="add"
|
||||
color="accent"
|
||||
class="col-auto q-px-sm q-ml-sm"
|
||||
@click="schedule_preset_store.openSchedulePresetManager(-1)"
|
||||
/>
|
||||
|
||||
<HorizontalSlideTransition :show="current_preset !== undefined && current_preset?.value !== -1">
|
||||
<transition
|
||||
enter-active-class="animated zoomIn"
|
||||
leave-active-class="animated zoomOut"
|
||||
mode="out-in"
|
||||
>
|
||||
<q-btn
|
||||
v-if="current_preset !== undefined && current_preset?.value !== -1"
|
||||
push
|
||||
dense
|
||||
rounded
|
||||
icon="edit"
|
||||
color="accent"
|
||||
class="col-auto q-px-sm q-ml-sm full-height"
|
||||
@click="schedule_preset_store.openSchedulePresetManager(current_preset.value)"
|
||||
/>
|
||||
</transition>
|
||||
</HorizontalSlideTransition>
|
||||
|
||||
</div>
|
||||
<AddModifyDialogSchedulePreview :current-preset-id="current_preset.value" />
|
||||
</div>
|
||||
</template>
|
||||
118
src/modules/employee-list/components/add-modify-dialog.vue
Normal file
118
src/modules/employee-list/components/add-modify-dialog.vue
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import AddModifyDialogForm from 'src/modules/employee-list/components/add-modify-dialog-form.vue';
|
||||
import AddModifyDialogAccess from 'src/modules/employee-list/components/add-modify-dialog-access.vue';
|
||||
import AddModifyDialogSchedule from 'src/modules/employee-list/components/add-modify-dialog-schedule.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
const current_step = ref<'form' | 'access' | 'schedule'>('form');
|
||||
const initial_employee_profile = ref(new EmployeeProfile)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="employee_store.is_add_modify_dialog_open"
|
||||
full-width
|
||||
@beforeShow="current_step = 'form'"
|
||||
@show="Object.assign(initial_employee_profile, employee_store.employee)"
|
||||
class="shadow-24"
|
||||
>
|
||||
<div
|
||||
class="column bg-secondary rounded-10 no-wrap"
|
||||
:class="$q.dark.isActive ? 'shadow-24' : 'shadow-10'"
|
||||
:style="($q.screen.lt.md ? ' ' : 'max-width: 60vw !important; height: 60vh') +
|
||||
($q.dark.isActive ? 'border: 2px solid var(--q-accent)' : '')"
|
||||
>
|
||||
<div class="row col-auto text-white bg-primary flex-center shadow-5">
|
||||
<div class="q-py-sm text-uppercase text-weight-bolder text-h5 ">
|
||||
{{ $t('employee_management.' + employee_store.management_mode) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="employee_store.employee.first_name.length > 0"
|
||||
class="text-uppercase text-weight-light text-h6 q-ml-sm"
|
||||
>
|
||||
{{ `${employee_store.employee.first_name} ${employee_store.employee.last_name}` }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col column q-pa-md no-wrap scroll">
|
||||
<q-tabs
|
||||
v-model="current_step"
|
||||
dense
|
||||
inline-label
|
||||
align="justify"
|
||||
indicator-color="transparent"
|
||||
active-class="text-white bg-accent"
|
||||
class="q-mb-sm"
|
||||
>
|
||||
<q-tab
|
||||
name="form"
|
||||
icon="las la-id-card"
|
||||
:label="$q.screen.lt.sm ? '' : $t('employee_management.details_label')"
|
||||
class="rounded-25 q-ma-xs"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
/>
|
||||
<q-tab
|
||||
name="access"
|
||||
icon="las la-key"
|
||||
:label="$q.screen.lt.sm ? '' : $t('employee_management.access_label')"
|
||||
class="rounded-25 q-ma-xs"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
/>
|
||||
<q-tab
|
||||
name="schedule"
|
||||
icon="calendar_month"
|
||||
:label="$q.screen.lt.sm ? '' : $t('employee_management.schedule_label')"
|
||||
class="rounded-25 q-ma-xs"
|
||||
style="border: 2px solid var(--q-accent);"
|
||||
/>
|
||||
</q-tabs>
|
||||
|
||||
<q-tab-panels
|
||||
v-model="current_step"
|
||||
animated
|
||||
:transition-prev="$q.screen.lt.sm ? 'jump-down' : 'jump-left'"
|
||||
:transition-next="$q.screen.lt.sm ? 'jump-up' : 'jump-right'"
|
||||
class="bg-transparent full-height"
|
||||
>
|
||||
<q-tab-panel
|
||||
name="form"
|
||||
class="q-pa-xs"
|
||||
>
|
||||
<AddModifyDialogForm />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
name="access"
|
||||
class="q-pa-xs"
|
||||
>
|
||||
<AddModifyDialogAccess />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
name="schedule"
|
||||
class="q-pa-xs"
|
||||
>
|
||||
<AddModifyDialogSchedule />
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
square
|
||||
color="accent"
|
||||
:label="employee_store.management_mode === 'add_employee' ? $t('shared.label.save') : $t('shared.label.update')"
|
||||
class="col-auto q-py-sm shadow-up-5"
|
||||
@click="employee_store.createOrUpdateEmployee(employee_store.employee)"
|
||||
/>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
// return first_name.charAt(0) + last_name.charAt(0);
|
||||
// };
|
||||
|
||||
const { row, index = 0 } = defineProps<{
|
||||
const { row, index = -1 } = defineProps<{
|
||||
row: EmployeeProfile
|
||||
index?: number
|
||||
}>()
|
||||
|
|
@ -19,23 +19,21 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInUp slow"
|
||||
mode="out-in"
|
||||
<div
|
||||
class="col-xs-6 col-sm-4 col-md-3 col-lg-3 col-xl-2 q-pa-sm row flex-center"
|
||||
:style="`animation-delay: ${index / 25}s;`"
|
||||
>
|
||||
<q-card
|
||||
v-ripple
|
||||
class="column col-xs-6 col-sm-4 col-md-3 col-lg-2 no-wrap rounded-15 cursor-pointer q-ma-sm"
|
||||
style="max-width: 230px;"
|
||||
:style="`animation-delay: ${index / 25}s;`"
|
||||
<div
|
||||
class="column col no-wrap cursor-pointer bg-dark rounded-15 shadow-12"
|
||||
style="max-width: 230px; height: 275px;"
|
||||
:style="(row.last_work_day === null ? ' ' : 'opacity: 0.6; ') + ($q.dark.isActive ? ' border: 2px solid var(--q-accent)' : '')"
|
||||
@click="emit('onProfileClick', row.email)"
|
||||
>
|
||||
<q-card-section class="col-6 text-center">
|
||||
<div class="col-auto column flex-center q-pt-md">
|
||||
<q-avatar
|
||||
:color="row.last_work_day === undefined ? 'accent' : 'negative'"
|
||||
:color="row.last_work_day === null ? 'accent' : 'negative'"
|
||||
size="8em"
|
||||
class="shadow-3 q-mb-md"
|
||||
class="col-auto shadow-3"
|
||||
>
|
||||
<img
|
||||
src="src/assets/targo-default-avatar.png"
|
||||
|
|
@ -43,23 +41,29 @@
|
|||
class="q-pa-xs"
|
||||
>
|
||||
</q-avatar>
|
||||
</q-card-section>
|
||||
</div>
|
||||
|
||||
<q-card-section
|
||||
class="col-grow text-center text-h6 text-weight-medium text-uppercase q-pb-none"
|
||||
style="line-height: 0.8em;"
|
||||
<div
|
||||
class="col column items-center justify-start text-center text-weight-medium text-uppercase q-pa-sm no-wrap"
|
||||
style="line-height: 1.2em; font-size: 1.3em;"
|
||||
>
|
||||
<div class="ellipsis" :class="row.last_work_day === undefined ? 'text-accent' : 'text-negative'"> {{ row.first_name }} {{ row.last_name }} </div>
|
||||
<q-separator
|
||||
color="accent"
|
||||
class="q-mx-sm q-mt-xs"
|
||||
/>
|
||||
<div class=" ellipsis-2-lines text-caption"> {{ row.job_title }} </div>
|
||||
</q-card-section>
|
||||
<div
|
||||
class="ellipsis-2-lines"
|
||||
:class="row.last_work_day === null ? 'text-accent' : 'text-negative'"
|
||||
>
|
||||
{{ row.first_name }} {{ row.last_name }}
|
||||
|
||||
<q-card-section class="bg-primary text-white text-caption text-center q-py-none col-2 content-center">
|
||||
<div> {{ row.email }} </div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</transition>
|
||||
<q-separator class="q-mb-xs q-mx-md" />
|
||||
</div>
|
||||
<div class=" ellipsis-2-lines text-caption no-wrap">{{ row.job_title }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-auto bg-primary text-white text-caption text-center text-weight-medium q-py-sm"
|
||||
style="border-radius: 0 0 15px 15px;"
|
||||
>
|
||||
{{ row.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -5,38 +5,76 @@
|
|||
import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { date, type QTableColumn } from 'quasar';
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||
import { employee_list_columns, getCompanyName } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { employee_list_columns, type EmployeeProfile, type EmployeeListFilters } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
import { animateFlip } from 'src/utils/table-grid-FLIP';
|
||||
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
const employee_store = useEmployeeStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const ui_store = useUiStore();
|
||||
const is_loading_list = ref<boolean>(true);
|
||||
const visible_columns = ref<(keyof EmployeeProfile)[]>(['first_name', 'email', 'company_name', 'supervisor_full_name', 'company_name', 'job_title', 'last_work_day']);
|
||||
|
||||
const filter = ref("");
|
||||
const table_grid_container = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
is_loading_list.value = true;
|
||||
await employee_list_api.getEmployeeList();
|
||||
is_loading_list.value = false;
|
||||
const filters = ref<EmployeeListFilters>({
|
||||
search_bar_string: '',
|
||||
hide_inactive_users: true,
|
||||
});
|
||||
|
||||
const filterEmployeeRows = (rows: readonly EmployeeProfile[], terms: EmployeeListFilters, _cols: readonly QTableColumn<EmployeeProfile>[]): EmployeeProfile[] => {
|
||||
let result = [...rows];
|
||||
|
||||
if (terms.hide_inactive_users) {
|
||||
const now = new Date();
|
||||
result = result.filter(row => {
|
||||
if (!row.last_work_day) return true;
|
||||
const inactiveDate = date.extractDate(row.last_work_day, 'YYYY-MM-DD');
|
||||
const limit = new Date(inactiveDate);
|
||||
limit.setDate(limit.getDate() + 14);
|
||||
return limit >= now;
|
||||
});
|
||||
}
|
||||
|
||||
if (terms.search_bar_string.trim().length > 0) {
|
||||
const searchTerms = terms.search_bar_string.split(' ').map(s => s.trim().toLowerCase());
|
||||
|
||||
result = result.filter(row => {
|
||||
const row_values = Object.values(row).map(v => String(v ?? '').toLowerCase());
|
||||
const row_values_without_emails = row_values.filter(value => !value.includes('@'));
|
||||
return searchTerms.every(term =>
|
||||
row_values_without_emails.some(value => value.includes(term))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
animateFlip(table_grid_container);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
table_grid_container.value = document.querySelector(".q-table__grid-content") as HTMLElement;
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-pa-lg">
|
||||
<q-table
|
||||
:key="filters.hide_inactive_users ? '1' : '0'"
|
||||
dense
|
||||
hide-pagination
|
||||
virtual-scroll
|
||||
title=" "
|
||||
card-style="max-height: 70vh;"
|
||||
:rows="employee_store.employee_list"
|
||||
:columns="employee_list_columns"
|
||||
row-key="name"
|
||||
row-key="email"
|
||||
:rows-per-page-options="[0]"
|
||||
:filter="filter"
|
||||
:pagination="{ sortBy: 'last_work_day', descending: true, }"
|
||||
:filter="filters"
|
||||
:filter-method="filterEmployeeRows"
|
||||
class="bg-transparent no-shadow sticky-header-table"
|
||||
:style="$q.screen.lt.md ? '' : 'width: 80vw;'"
|
||||
:table-class="$q.dark.isActive ? 'q-py-none q-mx-md rounded-10 bg-dark shadow-10' : 'q-py-none q-mx-md rounded-10 bg-white shadow-10'"
|
||||
|
|
@ -44,44 +82,21 @@
|
|||
table-header-class="text-accent text-uppercase"
|
||||
card-container-class="justify-center"
|
||||
:grid="ui_store.user_preferences.is_employee_list_grid"
|
||||
:loading="is_loading_list"
|
||||
:loading="employee_store.is_loading"
|
||||
:no-data-label="$t('shared.error.no_data_found')"
|
||||
:no-results-label="$t('shared.error.no_search_results')"
|
||||
:loading-label="$t('shared.label.loading')"
|
||||
@row-click="() => console.log('click!')"
|
||||
:visible-columns="visible_columns"
|
||||
>
|
||||
<template #header="props">
|
||||
<q-tr
|
||||
:props="props"
|
||||
class="bg-accent"
|
||||
>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
<span class="text-uppercase text-weight-bolder text-white">
|
||||
{{ $t(col.label) }}
|
||||
</span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="props">
|
||||
<EmployeeListTableItem
|
||||
:row="props.row"
|
||||
:index="props.rowIndex"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:top>
|
||||
<template #top>
|
||||
<div class="row full-width q-mb-sm">
|
||||
<q-btn
|
||||
push
|
||||
color="accent"
|
||||
icon="person_add"
|
||||
icon="las la-user-edit"
|
||||
:label="$t('shared.label.add')"
|
||||
class="text-uppercase"
|
||||
@click.stop="_evt => employee_store.openAddModifyDialog()"
|
||||
/>
|
||||
|
||||
<q-space />
|
||||
|
|
@ -100,13 +115,14 @@
|
|||
]"
|
||||
/>
|
||||
<q-input
|
||||
v-model="filter"
|
||||
v-model="filters.search_bar_string"
|
||||
outlined
|
||||
dense
|
||||
rounded
|
||||
color="accent"
|
||||
bg-color="white"
|
||||
label-color="accent"
|
||||
debounce="300"
|
||||
:label="$t('shared.label.search')"
|
||||
>
|
||||
<template v-slot:append>
|
||||
|
|
@ -117,20 +133,98 @@
|
|||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<q-space />
|
||||
<q-checkbox
|
||||
v-model="filters.hide_inactive_users"
|
||||
color="accent"
|
||||
:label="$t('employee_management.filter.hide_terminated')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
>
|
||||
<span
|
||||
class="text-uppercase text-weight-bolder text-white"
|
||||
style="font-size: 1.2em;"
|
||||
>
|
||||
{{ $t(col.label) }}
|
||||
</span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<template #item="props">
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated zoomIn fast"
|
||||
leave-active-class="animated zoomOut fast"
|
||||
mode="out-in"
|
||||
>
|
||||
<EmployeeListTableItem
|
||||
:key="props.rowIndex"
|
||||
:row="props.row"
|
||||
:index="props.rowIndex"
|
||||
@on-profile-click="employee_store.openAddModifyDialog"
|
||||
/>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<template #body-cell="scope">
|
||||
<q-td
|
||||
:props="scope"
|
||||
class="text-weight-medium"
|
||||
@click="employee_store.openAddModifyDialog(scope.row.email)"
|
||||
>
|
||||
<span v-if="scope.col.name === 'company_name'"> {{ getCompanyName(scope.value) }}</span>
|
||||
<span v-else>{{ scope.value }}</span>
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeInUp fast"
|
||||
leave-active-class="animated fadeOutDown fast"
|
||||
mode="out-in"
|
||||
>
|
||||
<div
|
||||
:key="scope.rowIndex + (timesheet_store.pay_period?.pay_period_no ?? 0)"
|
||||
class="rounded-5 cursor-pointer"
|
||||
style="font-size: 1.2em;"
|
||||
:style="`animation-delay: ${scope.rowIndex / 30}s; ` + (scope.row.last_work_day === null ? '' : 'opacity: 0.5;')"
|
||||
>
|
||||
<div v-if="scope.col.name === 'first_name'">
|
||||
<span
|
||||
class="text-h5 text-uppercase q-mr-xs"
|
||||
:class="scope.row.last_work_day === null ? 'text-accent' : 'text-negative'"
|
||||
>{{ scope.value }}</span>
|
||||
<span class="text-uppercase text-weight-light">{{ scope.row.last_name }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="scope.col.name === 'last_work_day'">
|
||||
<q-badge
|
||||
:color="scope.row.last_work_day === null ? 'accent' : 'negative'"
|
||||
class="row rounded-50 q-px-sm self-center"
|
||||
>
|
||||
<span class="text-bold text-uppercase q-mr-sm">
|
||||
{{ scope.row.last_work_day === null ? $t('employee_list.table.active') :
|
||||
$t('employee_list.table.inactive') }}
|
||||
</span>
|
||||
<q-icon
|
||||
:name="scope.row.last_work_day === null ? 'check' : 'clear'"
|
||||
size="xs"
|
||||
/>
|
||||
</q-badge>
|
||||
</div>
|
||||
|
||||
<span v-else>{{ scope.value }}</span>
|
||||
</div>
|
||||
</transition>
|
||||
</q-td>
|
||||
</template>
|
||||
|
||||
<!-- Template for custome failed-to-load state -->
|
||||
<template v-slot:no-data="{ message, filter }">
|
||||
<template #no-data="{ message, filter }">
|
||||
<div class="full-width column items-center text-accent q-gutter-sm">
|
||||
<span class="text-h6 q-mt-xl">
|
||||
{{ message }}
|
||||
|
|
@ -145,21 +239,39 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.sticky-header-table
|
||||
thead tr:first-child th
|
||||
background-color: var(--q-accent)
|
||||
margin-top: none
|
||||
<style scoped>
|
||||
:deep(.q-table__card .q-table__sort-icon) {
|
||||
fill: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
thead tr th
|
||||
position: sticky
|
||||
z-index: 1
|
||||
thead tr:first-child th
|
||||
top: 0px
|
||||
:deep(.q-table--dense .q-table__sort-icon) {
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
&.q-table--loading thead tr:last-child th
|
||||
top: 48px
|
||||
.sticky-header-table thead tr:first-child th {
|
||||
background-color: var(--q-primary);
|
||||
margin-top: none;
|
||||
}
|
||||
|
||||
tbody
|
||||
scroll-margin-top: 48px
|
||||
thead tr th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
&.q-table--loading thead tr:last-child th {
|
||||
top: 48px;
|
||||
}
|
||||
|
||||
tbody {
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
|
||||
:deep(.q-table) {
|
||||
transition: height 0.25s ease;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog v-model="employee_store.isShowingEmployeeAddModifyWindow">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
LOL
|
||||
</q-card-section>
|
||||
<q-inner-loading :showing="employee_store.is_loading"/>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { ShiftOption } from 'src/modules/timesheets/models/shift.models';
|
||||
import type { SchedulePresetShift } from '../models/schedule-presets.models';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const SHIFT_OPTIONS: ShiftOption[] = [
|
||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: 'blue-grey-3' },
|
||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
||||
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
||||
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
||||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
||||
];
|
||||
|
||||
const shift = defineModel<SchedulePresetShift>('shift', { required: true });
|
||||
const shift_type_selected = ref(SHIFT_OPTIONS[0]);
|
||||
|
||||
defineEmits<{
|
||||
'click-delete': [void];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row col-auto flex-center">
|
||||
<div class="col q-pa-xs">
|
||||
<q-select
|
||||
ref="select"
|
||||
v-model="shift_type_selected"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
dense
|
||||
options-dense
|
||||
hide-dropdown-icon
|
||||
:menu-offset="[0, 10]"
|
||||
menu-anchor="bottom middle"
|
||||
menu-self="top middle"
|
||||
:options="SHIFT_OPTIONS"
|
||||
class="col rounded-5 bg-dark weekday-field"
|
||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||
popup-content-style="border: 2px solid var(--q-accent)"
|
||||
@update:model-value="option => shift.type = option.value"
|
||||
>
|
||||
<template #selected-item="scope">
|
||||
<div
|
||||
class="row flex-center text-uppercase q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||
:tabindex="scope.tabindex"
|
||||
>
|
||||
<q-icon
|
||||
:name="scope.opt.icon"
|
||||
:color="scope.opt.icon_color"
|
||||
class="col-auto q-mx-xs"
|
||||
/>
|
||||
<span
|
||||
style="line-height: 0.9em;"
|
||||
class="col-auto ellipsis"
|
||||
>{{ scope.opt.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col q-px-xs">
|
||||
<q-input
|
||||
v-model="shift.start_time"
|
||||
standout
|
||||
dense
|
||||
hide-bottom-space
|
||||
type="time"
|
||||
class="text-uppercase weekday-field"
|
||||
>
|
||||
<template #prepend>
|
||||
<div
|
||||
class="column text-uppercase text-accent text-weight-bold full-height"
|
||||
style="font-size: 0.5em; transform: translateY(4px);"
|
||||
>
|
||||
{{ $t('shared.misc.in') }} :
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col q-px-xs">
|
||||
<q-input
|
||||
v-model="shift.end_time"
|
||||
standout
|
||||
dense
|
||||
hide-bottom-space
|
||||
type="time"
|
||||
class="text-uppercase weekday-field"
|
||||
>
|
||||
<template #prepend>
|
||||
<div
|
||||
class="column text-uppercase text-accent text-weight-bold full-height"
|
||||
style="font-size: 0.5em; transform: translateY(4px);"
|
||||
>
|
||||
{{ $t('shared.misc.out') }} :
|
||||
</div>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<div class="col-auto q-px-xs">
|
||||
<q-btn
|
||||
dense
|
||||
push
|
||||
color="negative"
|
||||
icon="clear"
|
||||
size="sm"
|
||||
tabindex="-1"
|
||||
@click="$emit('click-delete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.q-field__native) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.weekday-field :deep(.q-field__control) {
|
||||
height: 25px;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
:deep(.q-field--auto-height.q-field--dense .q-field__native) {
|
||||
min-height: 25px;
|
||||
}
|
||||
</style>
|
||||
108
src/modules/employee-list/components/schedule_presets_dialog.vue
Normal file
108
src/modules/employee-list/components/schedule_presets_dialog.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import SchedulePresetsDialogRow from './schedule-presets-dialog-row.vue';
|
||||
|
||||
import { useEmployeeListApi } from '../composables/use-employee-api';
|
||||
import { SchedulePresetShift } from '../models/schedule-presets.models';
|
||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
v-model="schedule_preset_store.is_manager_open"
|
||||
full-width
|
||||
>
|
||||
<div
|
||||
class="column flex-center bg-secondary rounded-10 shadow-24"
|
||||
style="border: 2px solid var(--q-accent); width: 50vw !important;"
|
||||
>
|
||||
<div
|
||||
class="row col-auto flex-center bg-primary full-width"
|
||||
style="border-radius: 8px 8px 0 0;"
|
||||
>
|
||||
<span class="row col-auto text-uppercase text-weight-bold text-white q-py-sm">{{
|
||||
schedule_preset_store.current_schedule_preset.id === -1 ?
|
||||
$t('shared.label.add') :
|
||||
$t('shared.label.modify') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row col-auto q-px-sm flex-center full-width q-py-sm">
|
||||
<div class="col-8 bg-dark rounded-10 ellipsis">
|
||||
<q-input
|
||||
v-model="schedule_preset_store.current_schedule_preset.name"
|
||||
standout
|
||||
dense
|
||||
hide-bottom-space
|
||||
:placeholder="$t('employee_management.schedule_presets.preset_name_placeholder')"
|
||||
class="text-uppercase"
|
||||
input-class="text-weight-bold text-center"
|
||||
>
|
||||
<template #before>
|
||||
<q-icon
|
||||
name="edit"
|
||||
color="accent"
|
||||
class="q-ml-sm"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column col full-width q-py-sm q-px-lg">
|
||||
<div
|
||||
v-for="weekday of schedule_preset_store.current_schedule_preset.weekdays"
|
||||
:key="weekday.day"
|
||||
class="row col-auto items-center q-my-xs shadow-2 bg-dark rounded-10 ellipsis"
|
||||
style="min-height: 50px;"
|
||||
>
|
||||
<span class="col-2 text-uppercase text-weight-bold q-ml-sm ellipsis">{{
|
||||
$t(`shared.weekday.${weekday.day.toLowerCase()}`) }}
|
||||
</span>
|
||||
|
||||
<div class="col column">
|
||||
<div
|
||||
v-for="_shift, index in weekday.shifts"
|
||||
:key="index"
|
||||
>
|
||||
<SchedulePresetsDialogRow
|
||||
v-model:shift="weekday.shifts[index]!"
|
||||
@click-delete="weekday.shifts.splice(index, 1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto self-stretch">
|
||||
<q-btn
|
||||
square
|
||||
icon="more_time"
|
||||
color="accent"
|
||||
class="full-height q-ma-none q-px-sm"
|
||||
tabindex="-1"
|
||||
@click="weekday.shifts.push(new SchedulePresetShift(weekday.day))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto row self-end q-px-lg full-width">
|
||||
<q-space />
|
||||
<q-btn
|
||||
:disable="schedule_preset_store.current_schedule_preset.name === ''"
|
||||
push
|
||||
dense
|
||||
:color="schedule_preset_store.current_schedule_preset.name === '' ? 'grey-7' : 'accent'"
|
||||
icon="download"
|
||||
:label="$t('shared.label.save')"
|
||||
class="col-auto q-px-md q-mb-sm"
|
||||
@click="employee_list_api.saveSchedulePreset"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
|
@ -1,18 +1,51 @@
|
|||
import { useEmployeeStore } from "src/stores/employee-store";
|
||||
import { useSchedulePresetsStore } from "src/stores/schedule-presets.store";
|
||||
import { SchedulePreset } from "../models/schedule-presets.models";
|
||||
|
||||
export const useEmployeeListApi = () => {
|
||||
const employeeListStore = useEmployeeStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
|
||||
const getEmployeeList = (): Promise<void> => {
|
||||
return employeeListStore.getEmployeeList();
|
||||
const getEmployeeList = async (): Promise<void> => {
|
||||
employee_store.is_loading = true;
|
||||
|
||||
const success = await employee_store.getEmployeeList();
|
||||
if (success) await schedule_preset_store.findSchedulePresetList();
|
||||
|
||||
employee_store.is_loading = false;
|
||||
};
|
||||
|
||||
const getEmployeeDetails = (email: string): Promise<void> => {
|
||||
return employeeListStore.getEmployeeDetails(email);
|
||||
const getEmployeeDetails = async(email: string): Promise<void> => {
|
||||
const success = await employee_store.getEmployeeDetails(email);
|
||||
if (success && employee_store.employee.preset_id !== null) {
|
||||
schedule_preset_store.setCurrentSchedulePreset(employee_store.employee.preset_id ?? -1);
|
||||
}
|
||||
}
|
||||
|
||||
const setSchedulePreset = (preset_id: number) => {
|
||||
schedule_preset_store.setCurrentSchedulePreset(preset_id);
|
||||
employee_store.employee.preset_id = preset_id < 0 ? null : preset_id;
|
||||
}
|
||||
|
||||
const saveSchedulePreset = async() => {
|
||||
const preset = schedule_preset_store.current_schedule_preset;
|
||||
const preset_shifts = preset.weekdays.flatMap(weekday => weekday.shifts);
|
||||
const backend_preset = new SchedulePreset(preset.id, preset.name, preset.is_default, preset_shifts);
|
||||
let success = false;
|
||||
|
||||
if (preset.id === -1) success = await schedule_preset_store.createSchedulePreset(backend_preset);
|
||||
else success = await schedule_preset_store.updateSchedulePreset(backend_preset);
|
||||
|
||||
if (success) {
|
||||
schedule_preset_store.is_manager_open = false;
|
||||
await schedule_preset_store.findSchedulePresetList();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getEmployeeList,
|
||||
getEmployeeDetails,
|
||||
setSchedulePreset,
|
||||
saveSchedulePreset,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,76 +1,129 @@
|
|||
import type { QTableColumn } from "quasar";
|
||||
import type { QSelectOption, QTableColumn } from "quasar";
|
||||
|
||||
export type ModuleAccessName = 'dashboard' | 'employee_list' | 'employee_management' | 'personal_profile' | 'timesheets' | 'timesheets_approval';
|
||||
export type ModuleAccessPreset = 'admin' | 'supervisor' | 'employee' | 'none';
|
||||
export type CompanyNames = 'Targo' | 'Solucom';
|
||||
|
||||
export class EmployeeProfile {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
supervisor_full_name: string;
|
||||
company_name: number;
|
||||
company_name: CompanyNames;
|
||||
job_title: string;
|
||||
email: string;
|
||||
phone_number: string;
|
||||
first_work_day: string;
|
||||
last_work_day: string;
|
||||
last_work_day?: string | null;
|
||||
external_payroll_id: number;
|
||||
residence: string;
|
||||
birth_date: string;
|
||||
|
||||
is_supervisor: boolean;
|
||||
user_module_access: ModuleAccessName[];
|
||||
preset_id?: number | null;
|
||||
|
||||
constructor() {
|
||||
this.first_name = '';
|
||||
this.last_name = '';
|
||||
this.supervisor_full_name = '';
|
||||
this.company_name = 271583;
|
||||
this.company_name = 'Targo';
|
||||
this.job_title = '';
|
||||
this.email = '';
|
||||
this.phone_number = '';
|
||||
this.first_work_day = '';
|
||||
this.last_work_day = '';
|
||||
this.last_work_day = null;
|
||||
this.residence = '';
|
||||
this.birth_date = '';
|
||||
this.is_supervisor = false;
|
||||
this.external_payroll_id = -1;
|
||||
this.user_module_access = ['dashboard',];
|
||||
}
|
||||
}
|
||||
|
||||
export interface EmployeeListFilters {
|
||||
search_bar_string: string;
|
||||
hide_inactive_users: boolean;
|
||||
};
|
||||
|
||||
export const employee_list_columns: QTableColumn<EmployeeProfile>[] = [
|
||||
{
|
||||
name: 'first_name',
|
||||
label: 'employee_list.table.first_name',
|
||||
label: 'timesheet_approvals.table.full_name',
|
||||
field: 'first_name',
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'last_name',
|
||||
label: 'employee_list.table.last_name',
|
||||
field: 'last_name',
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'employee_list.table.email',
|
||||
field: 'email',
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'supervisor_full_name',
|
||||
label: 'employee_list.table.supervisor',
|
||||
field: 'supervisor_full_name',
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'company_name',
|
||||
label: 'employee_list.table.company',
|
||||
field: 'company_name',
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'job_title',
|
||||
label: 'employee_list.table.role',
|
||||
field: 'job_title',
|
||||
align: 'left'
|
||||
align: 'left',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'last_work_day',
|
||||
label: 'status',
|
||||
field: 'last_work_day',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
sort: (a: string | null, b: string | null) => {
|
||||
if (a === null && b === null) return 0;
|
||||
else if (a === null && b !== null) return 1;
|
||||
else return -1;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const getCompanyName = (company_code: number) => {
|
||||
switch (company_code) {
|
||||
case 271583: return 'Targo';
|
||||
case 271585: return 'Solucom';
|
||||
default: return 'N / A';
|
||||
export const employee_access_options: QSelectOption<ModuleAccessName>[] = [
|
||||
{ label: 'dashboard', value: 'dashboard' },
|
||||
{ label: 'employee_list', value: 'employee_list' },
|
||||
{ label: 'personal_profile', value: 'personal_profile' },
|
||||
{ label: 'timesheets', value: 'timesheets' },
|
||||
{ label: 'employee_management', value: 'employee_management' },
|
||||
{ label: 'timesheets_approval', value: 'timesheets_approval' },
|
||||
]
|
||||
|
||||
export const employee_access_presets: Record<ModuleAccessPreset, ModuleAccessName[]> = {
|
||||
'admin' : ['dashboard', 'employee_list', 'employee_management', 'personal_profile', 'timesheets', 'timesheets_approval'],
|
||||
'supervisor' : ['dashboard', 'employee_list', 'personal_profile', 'timesheets', 'timesheets_approval'],
|
||||
'employee' : ['dashboard', 'timesheets', 'personal_profile', 'employee_list'],
|
||||
'none' : [],
|
||||
}
|
||||
|
||||
export const getEmployeeAccessOptionIcon = (module: ModuleAccessName): string => {
|
||||
switch (module) {
|
||||
case 'dashboard': return 'home';
|
||||
case 'employee_list' : return 'groups';
|
||||
case 'employee_management': return 'las la-user-edit';
|
||||
case 'personal_profile': return 'las la-id-card';
|
||||
case 'timesheets': return 'punch_clock';
|
||||
case 'timesheets_approval': return 'event_available';
|
||||
}
|
||||
}
|
||||
59
src/modules/employee-list/models/schedule-presets.models.ts
Normal file
59
src/modules/employee-list/models/schedule-presets.models.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { ShiftType } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export type Weekday = 'SUN' | 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT';
|
||||
|
||||
export const WEEKDAYS: Weekday[] = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
|
||||
|
||||
export class SchedulePreset {
|
||||
id: number;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
shifts: SchedulePresetShift[];
|
||||
|
||||
constructor(id?: number, name?: string, is_default?: boolean, shifts?: SchedulePresetShift[]) {
|
||||
this.id = id ?? -1;
|
||||
this.name = name ?? 'default';
|
||||
this.is_default = is_default ?? false;
|
||||
this.shifts = shifts ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
export class SchedulePresetShift {
|
||||
preset_id: number;
|
||||
week_day: Weekday;
|
||||
type: ShiftType;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
is_remote: boolean;
|
||||
|
||||
constructor(weekday: Weekday) {
|
||||
this.preset_id = -1;
|
||||
this.week_day = weekday;
|
||||
this.type = 'REGULAR';
|
||||
this.start_time = '00:00';
|
||||
this.end_time = '00:00';
|
||||
this.is_remote = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class SchedulePresetFrontend {
|
||||
id: number;
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
weekdays: WeekdayPresetShifts[];
|
||||
|
||||
constructor(schedule_preset?: SchedulePreset) {
|
||||
this.id = schedule_preset?.id ?? -1;
|
||||
this.name = schedule_preset?.name ?? '';
|
||||
this.is_default = schedule_preset?.is_default ?? false;
|
||||
this.weekdays = WEEKDAYS.map(day => ({
|
||||
day,
|
||||
shifts: schedule_preset !== undefined ? schedule_preset?.shifts.filter(shift => shift.week_day === day) : [],
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export interface WeekdayPresetShifts {
|
||||
day: Weekday;
|
||||
shifts: SchedulePresetShift[];
|
||||
}
|
||||
|
|
@ -1,19 +1,31 @@
|
|||
import { api } from 'src/boot/axios';
|
||||
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
import type { BackendResponse } from 'src/modules/shared/models/backend-response.models';
|
||||
|
||||
export const EmployeeListService = {
|
||||
getEmployeeList: async (): Promise<EmployeeProfile[]> => {
|
||||
const response = await api.get<{success: boolean, data: EmployeeProfile[], error?: string }>('/employees/employee-list')
|
||||
return response.data.data;
|
||||
getEmployeeList: async (): Promise<BackendResponse<EmployeeProfile[]>> => {
|
||||
const response = await api.get<BackendResponse<EmployeeProfile[]>>('/employees/employee-list')
|
||||
return response.data;
|
||||
|
||||
},
|
||||
|
||||
getEmployeeDetails: async (): Promise<EmployeeProfile> => {
|
||||
const response = await api.get<{success: boolean, data: EmployeeProfile, error?: string}>('employees/profile');
|
||||
return response.data.data;
|
||||
getEmployeeDetails: async (): Promise<BackendResponse<EmployeeProfile>> => {
|
||||
const response = await api.get('employees/profile');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getEmployeeDetailsWithEmployeeEmail: async (employee_email: string): Promise<EmployeeProfile> => {
|
||||
const response = await api.get<{success: boolean, data: EmployeeProfile, error?: string}>(`employees/profile?employee_email=${employee_email}`);
|
||||
return response.data.data;
|
||||
}
|
||||
getEmployeeDetailsWithEmployeeEmail: async (employee_email: string): Promise<BackendResponse<EmployeeProfile>> => {
|
||||
const response = await api.get(`employees/profile?employee_email=${employee_email}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createNewEmployee: async (profile: Omit<EmployeeProfile, 'last_work_day' | 'birth_date' | 'external_payroll_id'>): Promise<BackendResponse<EmployeeProfile>> => {
|
||||
const response = await api.post('employees/create', profile);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateEmployee: async (profile: EmployeeProfile): Promise<BackendResponse<EmployeeProfile>> => {
|
||||
const response = await api.patch('employees/update', profile);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { SchedulePreset } from "src/modules/profile/models/schedule-presets.models";
|
||||
import type { SchedulePreset } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||
|
||||
export const SchedulePresetsService = {
|
||||
createSchedulePresets: async (new_schedule: SchedulePreset) => {
|
||||
const response = await api.post(`/schedule-presets/create/`, new_schedule);
|
||||
createSchedulePresets: async (preset: SchedulePreset) => {
|
||||
const response = await api.post(`/schedule-presets/create/`, preset);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateSchedulePresets: async (preset_id: number, dto: Partial<SchedulePreset>) => {
|
||||
const response = await api.patch(`/schedule-presets/update/${preset_id}`, dto);
|
||||
updateSchedulePresets: async (preset: SchedulePreset): Promise<BackendResponse<boolean>> => {
|
||||
const response = await api.patch(`/schedule-presets/update`, preset);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ export const SchedulePresetsService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
findListOfSchedulePresets: async () => {
|
||||
getSchedulePresetsList: async (): Promise<BackendResponse<SchedulePreset[]>> => {
|
||||
const response = await api.get(`/schedule-presets/find-list`);
|
||||
return response.data;
|
||||
},
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
import MenuPanelPersonal from 'src/modules/profile/components/employee/menu-panel-personal.vue';
|
||||
import MenuPanelEmployee from 'src/modules/profile/components/employee/menu-panel-employee.vue';
|
||||
import MenuPanelPreferences from 'src/modules/profile/components/shared/menu-panel-preferences.vue';
|
||||
import MenuPanelSchedulePresets from 'src/modules/profile/components/shared/menu-panel-schedule-presets.vue';
|
||||
import MenuTemplate from 'src/modules/profile/components/shared/menu-template.vue';
|
||||
import { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
|
|
@ -16,7 +15,6 @@
|
|||
PERSONAL_INFO: 'personal_info',
|
||||
EMPLOYEE_INFO: 'employee_info',
|
||||
PREFERENCES: 'references',
|
||||
SCHEDULE_PRESETS: 'schedule_presets',
|
||||
};
|
||||
|
||||
const employee_profile = defineModel<EmployeeProfile>({ default: new EmployeeProfile });
|
||||
|
|
@ -24,66 +22,57 @@
|
|||
|
||||
|
||||
<template>
|
||||
<q-card
|
||||
flat
|
||||
class="rounded-5 bg-transparent q-pa-none"
|
||||
>
|
||||
<MenuTemplate
|
||||
:first-name="employee_profile.first_name === '' ? auth_store.user?.first_name ?? '' : employee_profile.first_name"
|
||||
:last-name="employee_profile.last_name === '' ? auth_store.user?.last_name ?? '' : employee_profile.last_name"
|
||||
:initial-menu="PanelNames.PERSONAL_INFO"
|
||||
<div>
|
||||
<q-card
|
||||
flat
|
||||
class="rounded-5 bg-transparent q-pa-none"
|
||||
>
|
||||
<template #tabs>
|
||||
<q-tab
|
||||
:name='PanelNames.PERSONAL_INFO'
|
||||
icon='person_outline'
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
|
||||
/>
|
||||
<q-tab
|
||||
:name="PanelNames.EMPLOYEE_INFO"
|
||||
icon="work_outline"
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
|
||||
/>
|
||||
<q-tab
|
||||
:name="PanelNames.PREFERENCES"
|
||||
icon="display_settings"
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
|
||||
/>
|
||||
<q-tab
|
||||
:name="PanelNames.SCHEDULE_PRESETS"
|
||||
icon="list_alt"
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.schedule_presets.tab_title')"
|
||||
/>
|
||||
</template>
|
||||
<MenuTemplate
|
||||
:first-name="employee_profile.first_name === '' ? auth_store.user?.first_name ?? '' : employee_profile.first_name"
|
||||
:last-name="employee_profile.last_name === '' ? auth_store.user?.last_name ?? '' : employee_profile.last_name"
|
||||
:initial-menu="PanelNames.PERSONAL_INFO"
|
||||
>
|
||||
<template #tabs>
|
||||
<q-tab
|
||||
:name='PanelNames.PERSONAL_INFO'
|
||||
icon='person_outline'
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
|
||||
/>
|
||||
<q-tab
|
||||
:name="PanelNames.EMPLOYEE_INFO"
|
||||
icon="work_outline"
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
|
||||
/>
|
||||
<q-tab
|
||||
:name="PanelNames.PREFERENCES"
|
||||
icon="display_settings"
|
||||
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #panels>
|
||||
<q-tab-panel
|
||||
:name="PanelNames.PERSONAL_INFO"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelPersonal v-model="employee_profile" />
|
||||
</q-tab-panel>
|
||||
<template #panels>
|
||||
<q-tab-panel
|
||||
:name="PanelNames.PERSONAL_INFO"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelPersonal v-model="employee_profile" />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
:name="PanelNames.EMPLOYEE_INFO"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelEmployee v-model="employee_profile" />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel
|
||||
:name="PanelNames.EMPLOYEE_INFO"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelEmployee v-model="employee_profile" />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel
|
||||
:name="PanelNames.PREFERENCES"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelPreferences />
|
||||
</q-tab-panel>
|
||||
<q-tab-panel
|
||||
:name="PanelNames.SCHEDULE_PRESETS"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelSchedulePresets />
|
||||
</q-tab-panel>
|
||||
</template>
|
||||
</MenuTemplate>
|
||||
</q-card>
|
||||
<q-tab-panel
|
||||
:name="PanelNames.PREFERENCES"
|
||||
class="q-pa-none"
|
||||
>
|
||||
<MenuPanelPreferences />
|
||||
</q-tab-panel>
|
||||
</template>
|
||||
</MenuTemplate>
|
||||
</q-card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -3,102 +3,42 @@
|
|||
lang="ts"
|
||||
>
|
||||
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
|
||||
import MenuPanelSelectField from 'src/modules/profile/components/shared/menu-panel-select-field.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { deepEqual } from 'src/utils/deep-equal';
|
||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
const COMPANY_OPTIONS = [
|
||||
{ label: 'Targo', value: 271583 },
|
||||
{ label: 'Solucom', value: 271585 }
|
||||
];
|
||||
const SUPERVISOR_OPTIONS = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
|
||||
|
||||
const auth_store = useAuthStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
|
||||
const is_editing = ref<boolean>(false);
|
||||
const current_company_option = ref(COMPANY_OPTIONS.find(option => option.value === employee_store.employee.company_name) ?? { label: '', value: 0 })
|
||||
let initial_info: EmployeeProfile = unwrapAndClone(employee_store.employee);
|
||||
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!is_editing.value) {
|
||||
is_editing.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
is_editing.value = false;
|
||||
initial_info = unwrapAndClone(employee_store.employee); // update initial value for future possible resets
|
||||
employee_store.employee.company_name = current_company_option.value.value;
|
||||
|
||||
if (!deepEqual(employee_store.employee, initial_info)) {
|
||||
// save the new data here
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
employee_store.employee = unwrapAndClone(initial_info);
|
||||
is_editing.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form
|
||||
class="q-pa-md full-height"
|
||||
@submit="onSubmit"
|
||||
@reset="onReset"
|
||||
>
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<div class="column q-pa-md full-height">
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.job_title"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.employee.job_title')"
|
||||
/>
|
||||
<MenuPanelSelectField
|
||||
v-model="current_company_option"
|
||||
:options="COMPANY_OPTIONS"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.company_name"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.employee.company')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MenuPanelSelectField
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.supervisor_full_name"
|
||||
:options="SUPERVISOR_OPTIONS"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.employee.supervisor')"
|
||||
:is-editing="is_editing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.email"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.employee.email')"
|
||||
/>
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.first_work_day"
|
||||
readonly
|
||||
class="col-auto"
|
||||
type="date"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.employee.hired_date')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
<!-- DEV NOTE: May revisit later for changes by employee that will need approval -->
|
||||
<!-- <div
|
||||
v-if="CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST')"
|
||||
class="absolute-bottom"
|
||||
:class="$q.screen.lt.md ? 'column' : 'row'"
|
||||
|
|
@ -121,6 +61,6 @@
|
|||
class="q-ma-sm q-py-xs"
|
||||
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -4,90 +4,44 @@
|
|||
>
|
||||
import MenuPanelInputField from 'src/modules/profile/components/shared/menu-panel-input-field.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { deepEqual } from 'src/utils/deep-equal';
|
||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||
|
||||
const employee_store = useEmployeeStore();
|
||||
const is_editing = ref<boolean>(false);
|
||||
const initial_info = ref<EmployeeProfile>(unwrapAndClone(employee_store.employee));
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!is_editing.value) {
|
||||
is_editing.value = true;
|
||||
initial_info.value = unwrapAndClone(employee_store.employee);
|
||||
return;
|
||||
}
|
||||
|
||||
is_editing.value = false;
|
||||
initial_info.value = unwrapAndClone(employee_store.employee); // update initial value for future possible resets
|
||||
|
||||
if (!deepEqual(employee_store.employee, initial_info)) {
|
||||
// save the new data here
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onReset = () => {
|
||||
employee_store.employee = unwrapAndClone(initial_info.value);
|
||||
is_editing.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form
|
||||
class="q-pa-md full-height"
|
||||
@submit="onSubmit"
|
||||
@reset="onReset"
|
||||
>
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.first_name"
|
||||
type="text"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.first_name')"
|
||||
/>
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.last_name"
|
||||
class="col"
|
||||
type="text"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.last_name')"
|
||||
/>
|
||||
</div>
|
||||
<div class="column q-pa-md full-height">
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.first_name"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.personal.first_name')"
|
||||
/>
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.last_name"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.personal.last_name')"
|
||||
/>
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.phone_number"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.personal.phone_number')"
|
||||
/>
|
||||
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.phone_number"
|
||||
class="col"
|
||||
type="text"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.phone_number')"
|
||||
/>
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.birth_date"
|
||||
class="col"
|
||||
mask="#### / ## / ##"
|
||||
hint="ex: 1970 / 01 / 01"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.birthdate')"
|
||||
/>
|
||||
</div>
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.birth_date"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.personal.birthdate')"
|
||||
/>
|
||||
|
||||
<div :class="$q.screen.lt.md ? 'column' : 'row'">
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.residence"
|
||||
class="col"
|
||||
:is-editing="is_editing"
|
||||
:label-string="$t('profile.personal.address')"
|
||||
:hint="$t('profile.personal.address_hint')"
|
||||
/>
|
||||
</div>
|
||||
<MenuPanelInputField
|
||||
v-model="employee_store.employee.residence"
|
||||
class="col-auto"
|
||||
:label-string="$t('profile.personal.address')"
|
||||
/>
|
||||
|
||||
<div
|
||||
|
||||
<!-- DEV NOTE: May revisit later for changes by employee that will need approval -->
|
||||
<!-- <div
|
||||
class="absolute-bottom"
|
||||
:class="$q.screen.lt.md ? 'column' : 'row'"
|
||||
>
|
||||
|
|
@ -109,6 +63,6 @@
|
|||
class="q-ma-sm q-py-xs"
|
||||
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
|
||||
/>
|
||||
</div>
|
||||
</q-form>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -12,8 +12,7 @@
|
|||
<q-img
|
||||
src="src/assets/profile_header_default.png"
|
||||
height="15vh"
|
||||
:width="$q.screen.lt.md ? '80vw' : '40vw'"
|
||||
class="rounded-5 q-mb-md shadow-2 col-auto"
|
||||
class="rounded-5 q-mb-md shadow-2"
|
||||
fit="cover"
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -2,43 +2,46 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import type { ValidationRule } from 'quasar';
|
||||
|
||||
const model = defineModel<string | number | undefined>({ required: true });
|
||||
|
||||
const { readonly = false, hint = '' } = defineProps<{
|
||||
defineProps<{
|
||||
labelString: string;
|
||||
isEditing: boolean;
|
||||
readonly?: boolean;
|
||||
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
|
||||
hint?: string;
|
||||
mask?: string;
|
||||
rules?: ValidationRule[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-input
|
||||
v-model="model"
|
||||
readonly
|
||||
dense
|
||||
:stack-label="!isEditing"
|
||||
stack-label
|
||||
autogrow
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
debounce="500"
|
||||
outlined
|
||||
label-color="accent"
|
||||
label-slot
|
||||
class="q-ma-xs"
|
||||
class="q-mx-xs q-my-sm"
|
||||
input-class="text-weight-light"
|
||||
input-style="font-size: 1.2em"
|
||||
:hide-hint="hint === ''"
|
||||
:hint="isEditing ? hint : ''"
|
||||
:mask="mask"
|
||||
:readonly="readonly || !isEditing"
|
||||
:type="type"
|
||||
:rules="rules"
|
||||
input-style="font-size: 1.5em"
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-weight-bolder text-accent text-uppercase">{{ labelString }}</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.q-field__label) {
|
||||
background-color: var(--q-dark);
|
||||
padding: 0 5px;
|
||||
transform: translateY(-60%) scale(0.75) !important;
|
||||
}
|
||||
|
||||
:deep(.q-field__control-container) {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
:deep(.q-field__control::before) {
|
||||
border: 1px solid var(--q-accent) !important;
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,66 +2,72 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { dark_mode_options } from 'src/modules/profile/models/preferences.models';
|
||||
import LanguageSwitch from 'src/modules/shared/components/language-switch.vue';
|
||||
import { ref } from 'vue';
|
||||
import { Dark } from 'quasar';
|
||||
|
||||
import { useUiStore } from 'src/stores/ui-store';
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const initial_dark_mode_value = Dark.isActive;
|
||||
const is_dark_mode = ref<boolean>(initial_dark_mode_value);
|
||||
|
||||
const toggle_dark_mode = (value: boolean) => {
|
||||
if (ui_store.user_preferences) ui_store.user_preferences.is_dark_mode = value;
|
||||
Dark.set(value);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form class="q-pa-md column fit">
|
||||
<q-card
|
||||
flat
|
||||
class="col-auto justify-center content-center q-mb-lg q-pa-none"
|
||||
:style="$q.dark.isActive ? 'background-color: #FFF1;' : 'background-color: #0001;'"
|
||||
<div class="q-pa-md column fit">
|
||||
<div
|
||||
class="col-auto"
|
||||
style="transform: translate(10px, 12px);"
|
||||
>
|
||||
<q-card-section class="q-py-none">
|
||||
<span class="text-uppercase text-weight-bold text-accent">{{ $t('profile.preferences.display_options')
|
||||
}}</span>
|
||||
</q-card-section>
|
||||
<span class="text-uppercase text-weight-bold text-accent bg-dark q-px-sm">
|
||||
{{ $t('profile.preferences.display_options') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="flex-center text-uppercase"
|
||||
>
|
||||
<span class="q-mx-md text-weight-medium">{{ $t('profile.preferences.light_mode') }}</span>
|
||||
<q-toggle
|
||||
v-model="is_dark_mode"
|
||||
@update:model-value="value => toggle_dark_mode(value)"
|
||||
size="xl"
|
||||
class="col-auto"
|
||||
checked-icon="dark_mode"
|
||||
unchecked-icon="light_mode"
|
||||
/>
|
||||
<span class="q-mx-md text-weight-medium">{{ $t('profile.preferences.dark_mode') }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
flat
|
||||
class="col-auto justify-center content-center q-mb-lg q-pa-none"
|
||||
:style="$q.dark.isActive ? 'background-color: #FFF1;' : 'background-color: #0001;'"
|
||||
<div
|
||||
class="col-auto row justify-center content-center q-mb-sm q-pa-sm rounded-5"
|
||||
style="border: 1px solid var(--q-accent);"
|
||||
>
|
||||
<q-card-section class="q-py-none">
|
||||
<span class="text-uppercase text-weight-bold text-accent">{{ $t('profile.preferences.language_options')
|
||||
}}</span>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section
|
||||
horizontal
|
||||
class="flex-center"
|
||||
<q-item
|
||||
v-for="mode of dark_mode_options"
|
||||
:key="mode.label"
|
||||
clickable
|
||||
dense
|
||||
v-ripple
|
||||
class="col rounded-5 q-ma-sm shadow-4"
|
||||
:class="mode.quasar_value === $q.dark.mode ? 'bg-accent text-white text-weight-bolder' : ''"
|
||||
@click="ui_store.user_preferences.is_dark_mode = mode.value"
|
||||
>
|
||||
<LanguageSwitch class="col-auto" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-form>
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
:name="mode.icon"
|
||||
size="md"
|
||||
:color="mode.quasar_value === $q.dark.mode ? 'white' : ''"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section class="text-uppercase justify-center">
|
||||
<q-item-label> {{ $t(mode.label) }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
v-if="mode.quasar_value === $q.dark.mode"
|
||||
name="check"
|
||||
color="white"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-auto row text-uppercase text-weight-bold text-accent"
|
||||
style="transform: translate(10px, 12px);"
|
||||
>
|
||||
<div class="col-auto bg-dark q-px-sm">{{ $t('profile.preferences.language_options') }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-auto justify-center content-center q-pa-sm rounded-5"
|
||||
style="border: 1px solid var(--q-accent);"
|
||||
>
|
||||
<LanguageSwitch class="col-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
const model = defineModel<string | number | {label: string, value: unknown} | undefined>();
|
||||
|
||||
const { readonly = false, localizeOptions = false } = defineProps<{
|
||||
options: { label: string, value: string | number }[];
|
||||
labelString: string;
|
||||
isEditing: boolean;
|
||||
readonly?: boolean;
|
||||
localizeOptions?: boolean;
|
||||
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-select
|
||||
v-model="model"
|
||||
dense
|
||||
:stack-label="!isEditing"
|
||||
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||
label-color="accent"
|
||||
class="q-ma-xs"
|
||||
popup-content-class="text-weight-medium text-h6 rounded-5"
|
||||
options-selected-class="bg-accent text-white"
|
||||
:menu-offset="[0, 10]"
|
||||
:options="options"
|
||||
:readonly="readonly || !isEditing"
|
||||
:hide-dropdown-icon="!isEditing"
|
||||
:label="labelString"
|
||||
:option-label="opt => localizeOptions ? $t(opt.label) : opt.label ?? opt"
|
||||
hint=''
|
||||
>
|
||||
<template #label>
|
||||
<span class="text-weight-bolder text-accent text-uppercase">{{ labelString }}</span>
|
||||
</template>
|
||||
|
||||
<template #selected-item="scope">
|
||||
<span class="text-weight-light" style="font-size: 1.2em;">{{ scope.opt.label }}</span>
|
||||
</template>
|
||||
</q-select>
|
||||
</template>
|
||||
|
|
@ -16,44 +16,46 @@
|
|||
|
||||
<template>
|
||||
<div
|
||||
:class="$q.screen.lt.md ? 'column no-wrap flex-center' : 'row'"
|
||||
:style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
|
||||
class="column flex-center"
|
||||
>
|
||||
<MenuHeader
|
||||
:user-first-name="firstName"
|
||||
:user-last-name="lastName"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<q-card
|
||||
class="col-auto q-pa-xs"
|
||||
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-sm'"
|
||||
>
|
||||
<q-tabs
|
||||
v-model="current_menu"
|
||||
:vertical="$q.screen.gt.sm"
|
||||
active-color="accent"
|
||||
indicator-color="accent"
|
||||
<div class="row col full-width">
|
||||
<div
|
||||
class="col-auto q-pa-xs bg-dark rounded-5 shadow-2"
|
||||
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-sm'"
|
||||
>
|
||||
<slot name="tabs"></slot>
|
||||
</q-tabs>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
class="col"
|
||||
:class="$q.screen.lt.md ? 'full-width' : 'q-ml-sm'"
|
||||
>
|
||||
<q-tab-panels
|
||||
v-model="current_menu"
|
||||
animated
|
||||
vertical
|
||||
transition-prev="jump-up"
|
||||
transition-next="jump-up"
|
||||
class="rounded-5"
|
||||
style="height: 50vh;"
|
||||
<q-tabs
|
||||
v-model="current_menu"
|
||||
:vertical="$q.screen.gt.sm"
|
||||
active-color="accent"
|
||||
indicator-color="accent"
|
||||
>
|
||||
<slot name="tabs"></slot>
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<q-card
|
||||
class="col"
|
||||
:class="$q.screen.lt.md ? 'full-width' : 'q-ml-sm'"
|
||||
>
|
||||
<slot name="panels"></slot>
|
||||
</q-tab-panels>
|
||||
</q-card>
|
||||
<q-tab-panels
|
||||
v-model="current_menu"
|
||||
animated
|
||||
vertical
|
||||
transition-prev="jump-up"
|
||||
transition-next="jump-up"
|
||||
class="rounded-5"
|
||||
style="height: 50vh;"
|
||||
>
|
||||
<slot name="panels"></slot>
|
||||
</q-tab-panels>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,17 @@
|
|||
import type { MessageLanguages } from "src/boot/i18n";
|
||||
|
||||
export interface DarkModeOption {
|
||||
label: string;
|
||||
value: boolean | null;
|
||||
quasar_value: boolean | "auto";
|
||||
icon: string;
|
||||
}
|
||||
export const dark_mode_options: DarkModeOption[] = [
|
||||
{ label: 'profile.preferences.dark_mode', value: true, quasar_value: true, icon: 'dark_mode'},
|
||||
{ label: 'profile.preferences.light_mode', value: false, quasar_value: false, icon: 'light_mode'},
|
||||
{ label: 'profile.preferences.auto_mode', value: null, quasar_value: "auto", icon: 'brightness_auto'},
|
||||
]
|
||||
|
||||
export class Preferences {
|
||||
id: number;
|
||||
notifications: boolean;
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
export interface SchedulePreset {
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
presets_shifts: ShiftPreset[];
|
||||
}
|
||||
|
||||
class ShiftPreset {
|
||||
week_day: Weekday;
|
||||
preset_id: number;
|
||||
sort_order: number;
|
||||
type: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
is_remote: boolean;
|
||||
|
||||
constructor() {
|
||||
this.week_day = '';
|
||||
this.preset_id = -1;
|
||||
this.sort_order = -1;
|
||||
this.type = '';
|
||||
this.start_time = '';
|
||||
this.end_time = '';
|
||||
this.is_remote = false;
|
||||
}
|
||||
}
|
||||
|
||||
export type Weekday = 'SUN' | 'MON' | 'TUE' | 'WED' | 'THU' | 'FRI' | 'SAT' | '';
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
defineProps({
|
||||
show: { type: Boolean, required: true }
|
||||
})
|
||||
|
||||
const beforeEnter = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
html_element.style.width = '0'
|
||||
html_element.style.opacity = '0'
|
||||
}
|
||||
|
||||
const enter = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
const w = html_element.scrollWidth
|
||||
html_element.style.transition = 'width 0.25s ease, opacity 0.25s ease'
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
html_element.style.width = w + 'px'
|
||||
html_element.style.opacity = '1'
|
||||
})
|
||||
}
|
||||
|
||||
const afterEnter = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
html_element.style.width = 'auto' // restore natural width
|
||||
}
|
||||
|
||||
const beforeLeave = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
html_element.style.width = element.scrollWidth + 'px'
|
||||
html_element.style.opacity = '1'
|
||||
}
|
||||
|
||||
const leave = (element: Element) => {
|
||||
const html_element = element as HTMLElement;
|
||||
|
||||
// force reflow
|
||||
void html_element.offsetWidth
|
||||
|
||||
html_element.style.transition = 'width 0.25s ease, opacity 0.25s ease'
|
||||
html_element.style.width = '0'
|
||||
html_element.style.opacity = '0'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
@before-enter="beforeEnter"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@before-leave="beforeLeave"
|
||||
@leave="leave"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="h-slide"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.h-slide {
|
||||
overflow: hidden;
|
||||
/* required for width transitions */
|
||||
display: inline-block;
|
||||
/* ensures width applies correctly */
|
||||
}
|
||||
</style>
|
||||
|
|
@ -26,13 +26,18 @@
|
|||
clickable
|
||||
dense
|
||||
v-ripple
|
||||
class="col rounded-5 q-ma-sm shadow-1 "
|
||||
class="col rounded-5 q-ma-sm shadow-4"
|
||||
:class="locale === $i18n.locale ? 'bg-accent text-white text-weight-bolder' : ''"
|
||||
@click="setDisplayLanguage(locale as MessageLanguages)"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-img :src="`src/assets/${locale}.png`"/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section class="text-uppercase justify-center">
|
||||
<q-item-label> {{ $t(`profile.preferences.${locale}`) }}</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-icon
|
||||
v-if="locale === $i18n.locale"
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@
|
|||
dense
|
||||
:icon="props.value ? 'lock' : 'lock_open'"
|
||||
:color="props.value ? 'white' : 'grey-5'"
|
||||
class="rounded-5 z-top"
|
||||
class="rounded-5 "
|
||||
:class="props.value ? 'bg-accent' : ''"
|
||||
@click.stop="props.row.is_approved = !props.row.is_approved"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
export interface shiftColor {
|
||||
type_label: string;
|
||||
background_color: string;
|
||||
font_color: string;
|
||||
}
|
||||
|
||||
export const shift_type_legend: shiftColor[] = [
|
||||
{
|
||||
type_label: 'shared.shift_type.regular',
|
||||
background_color: 'blue-grey-4',
|
||||
font_color: 'blue-grey-8',
|
||||
},
|
||||
{
|
||||
type_label: 'shared.shift_type.evening',
|
||||
background_color: 'warning',
|
||||
font_color: 'blue-grey-2',
|
||||
},
|
||||
{
|
||||
type_label: 'shared.shift_type.emergency',
|
||||
background_color: 'amber-10',
|
||||
font_color: 'blue-grey-2',
|
||||
},
|
||||
{
|
||||
type_label: 'shared.shift_type.overtime',
|
||||
background_color: 'negative',
|
||||
font_color: 'blue-grey-2',
|
||||
},
|
||||
{
|
||||
type_label: 'shared.shift_type.vacation',
|
||||
background_color: 'purple-10',
|
||||
font_color: 'blue-grey-2',
|
||||
},
|
||||
{
|
||||
type_label: 'shared.shift_type.holiday',
|
||||
background_color: 'purple-8',
|
||||
font_color: 'blue-grey-2',
|
||||
},
|
||||
{
|
||||
type_label: 'shared.shift_type.sick',
|
||||
background_color: 'grey-8',
|
||||
font_color: 'blue-grey-2',
|
||||
},
|
||||
]
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
import type { QTableColumn } from "quasar";
|
||||
|
||||
/* eslint-disable */
|
||||
export enum NavigatorConstants {
|
||||
NEXT_PERIOD = 1,
|
||||
PREVIOUS_PERIOD = -1,
|
||||
}
|
||||
|
||||
export interface TimesheetOverview {
|
||||
email: string;
|
||||
employee_name: string;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
const shift_rules = useShiftRules(t('timesheet.errors.SHIFT_TIME_REQUIRED'),);
|
||||
|
||||
const COMMENT_LENGTH_MAX = 280;
|
||||
|
||||
const SHIFT_OPTIONS: ShiftOption[] = [
|
||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: 'blue-grey-3' },
|
||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
||||
];
|
||||
|
||||
const shift = defineModel<Shift>('shift', { required: true });
|
||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||
const select_ref = useTemplateRef<QSelect>('select');
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
<script
|
||||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { ShiftLegendItem } from 'src/modules/timesheets/models/shift.models';
|
||||
|
||||
const { t } = useI18n();
|
||||
const is_showing_legend = ref(false);
|
||||
|
||||
const legend: ShiftLegendItem[] = [
|
||||
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' },
|
||||
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
|
||||
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
|
||||
{ type: 'VACATION', color: 'purple-10', label_type: 'timesheet.shift.types.VACATION' },
|
||||
{ type: 'HOLIDAY', color: 'purple-5', label_type: 'timesheet.shift.types.HOLIDAY' },
|
||||
{ type: 'SICK', color: 'grey-8', label_type: 'timesheet.shift.types.SICK' },
|
||||
]
|
||||
|
||||
const shift_type_legend = computed(() =>
|
||||
legend.map(item => ({ ...item, label: t(item.label_type) }))
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="items-center"
|
||||
:class="$q.screen.lt.md ? 'column' : 'row'"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
rounded
|
||||
color="primary"
|
||||
class="col-auto q-my-sm"
|
||||
@click="is_showing_legend = !is_showing_legend"
|
||||
>
|
||||
<template #default>
|
||||
<q-icon
|
||||
:name="is_showing_legend ? 'close' : 'info_outline'"
|
||||
size="md"
|
||||
class="col-auto"
|
||||
/>
|
||||
</template>
|
||||
</q-btn>
|
||||
|
||||
<transition
|
||||
appear
|
||||
enter-active-class="animated fadeIn"
|
||||
leave-active-class="animated fadeOut"
|
||||
class="col-auto"
|
||||
>
|
||||
<div
|
||||
v-if="is_showing_legend"
|
||||
class="q-py-xs bg-white rounded-5 shadow-2 text-center q-my-xs"
|
||||
>
|
||||
<q-badge
|
||||
v-for="shift_type in shift_type_legend"
|
||||
:key="shift_type.type"
|
||||
:color="shift_type.color"
|
||||
:label="shift_type.label"
|
||||
:text-color="shift_type.text_color || 'white'"
|
||||
class="q-pa-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
||||
style="font-size: 0.8em;"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -27,26 +27,8 @@
|
|||
class="row items-center full-width bg-dark shadow-2 rounded-5 q-my-xs"
|
||||
style="border: 2px solid var(--q-negative)"
|
||||
>
|
||||
<q-item-section class="col-auto">
|
||||
<q-badge
|
||||
outline
|
||||
color="negative"
|
||||
class="bg-dark text-weight-bolder"
|
||||
>{{ error.conflicts.date }}</q-badge>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section class="col-auto">
|
||||
<q-badge
|
||||
outline
|
||||
color="negative"
|
||||
class="bg-dark text-weight-bolder"
|
||||
>
|
||||
{{ error.conflicts.start_time }} - {{ error.conflicts.end_time }}
|
||||
</q-badge>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-label class="text-weight-medium text-caption q-ml-md">
|
||||
{{ $t('timesheet.shift.errors.' + error.error_code) }}
|
||||
{{ $t('timesheet.errors.' + error) }}
|
||||
</q-item-label>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
|
|
|||
|
|
@ -11,13 +11,6 @@ export type ShiftType = 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'HOLIDAY' | 'VACAT
|
|||
|
||||
export type ShiftErrorCode = 'SHIFT_OVERLAP' | 'MISSING_START_TIME' | 'MISSING_END_TIME' | 'COMMENT_LENGTH_EXCEEDED' | 'APPROVAL_LOCK' | 'INVALID_DATE' | 'INVALID TYPE' | 'INVALID_TIMESHEET';
|
||||
|
||||
export type ShiftLegendItem = {
|
||||
type: ShiftType;
|
||||
color: string;
|
||||
label_type: string;
|
||||
text_color?: string;
|
||||
};
|
||||
|
||||
export class Shift {
|
||||
id: number;
|
||||
timesheet_id: number;
|
||||
|
|
@ -49,23 +42,4 @@ export interface ShiftOption {
|
|||
value: ShiftType;
|
||||
icon: string;
|
||||
icon_color: string;
|
||||
}
|
||||
|
||||
export interface ShiftAPIResponse {
|
||||
ok: boolean;
|
||||
data?: {
|
||||
shift: Shift;
|
||||
overtime: unknown;
|
||||
}
|
||||
error?: ShiftAPIError;
|
||||
}
|
||||
|
||||
export interface ShiftAPIError {
|
||||
error_code: ShiftErrorCode;
|
||||
conflicts:
|
||||
{
|
||||
date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { api } from "src/boot/axios";
|
||||
import type { Shift, ShiftAPIResponse } from "src/modules/timesheets/models/shift.models";
|
||||
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||
import type { Shift } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const ShiftService = {
|
||||
deleteShiftById: async (shift_id: number) => {
|
||||
|
|
@ -7,14 +8,14 @@ export const ShiftService = {
|
|||
return response.data;
|
||||
},
|
||||
|
||||
createNewShifts: async (new_shifts: Shift[]):Promise<ShiftAPIResponse[]> => {
|
||||
createNewShifts: async (new_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
|
||||
const response = await api.post(`/shift/create`, new_shifts);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateShifts: async (existing_shifts: Shift[]) => {
|
||||
updateShifts: async (existing_shifts: Shift[]):Promise<BackendResponse<Shift>> => {
|
||||
console.log('sent shifts: ', existing_shifts)
|
||||
const response = await api.patch(`/shift/update`, existing_shifts);
|
||||
return response;
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
|
@ -3,16 +3,25 @@
|
|||
lang="ts"
|
||||
>
|
||||
import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue';
|
||||
import EmployeeListAddModifyDialog from 'src/modules/employee-list/components/employee/employee-list-add-modify-dialog.vue';
|
||||
import AddModifyDialog from 'src/modules/employee-list/components/add-modify-dialog.vue';
|
||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
||||
onMounted(async () => {
|
||||
await employee_list_api.getEmployeeList();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="column flex-center">
|
||||
<EmployeeListAddModifyDialog />
|
||||
<q-page class="column items-center justify-start">
|
||||
<AddModifyDialog />
|
||||
|
||||
<PageHeaderTemplate title="employee_list.page_header" />
|
||||
|
||||
|
||||
<EmployeeListTable />
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -21,17 +21,10 @@ import { onMounted } from 'vue';
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="bg-secondary column items-center justify-center">
|
||||
<q-page class="bg-secondary row items-center justify-center">
|
||||
<MenuEmployee
|
||||
v-if="employee_roles.includes(auth_store.user?.role.toUpperCase() ?? 'GUEST')"
|
||||
class="col-sm-12 col-md-10 col-lg-9 col-xl-8"
|
||||
class="col-sm-12 col-md-10 col-lg-7 col-xl-5"
|
||||
/>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.q-field--standout.q-field--readonly .q-field__control:before) {
|
||||
border: none !important;
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
export enum RouteNames {
|
||||
/* eslint-disable */
|
||||
LOGIN = 'login',
|
||||
LOGIN_SUCCESS = 'login-success',
|
||||
DASHBOARD = 'dashboard',
|
||||
|
|
|
|||
|
|
@ -1,45 +1,101 @@
|
|||
/* eslint-disable */
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { EmployeeListService } from "src/modules/employee-list/services/employee-list-service";
|
||||
import { EmployeeProfile } from "src/modules/employee-list/models/employee-profile.models";
|
||||
import { Notify } from "quasar";
|
||||
|
||||
export const useEmployeeStore = defineStore('employee', () => {
|
||||
const employee = ref<EmployeeProfile>(new EmployeeProfile);
|
||||
const employee_list = ref<EmployeeProfile[]>([]);
|
||||
const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
|
||||
const is_add_modify_dialog_open = ref<boolean>(false);
|
||||
const management_mode = ref<'modify_employee' | 'add_employee'>('add_employee');
|
||||
const is_loading = ref(false);
|
||||
const isLoadingEmployeeList = ref(false);
|
||||
|
||||
const getEmployeeList = async () => {
|
||||
isLoadingEmployeeList.value = true;
|
||||
try {
|
||||
const response = await EmployeeListService.getEmployeeList();
|
||||
employee_list.value = response;
|
||||
} catch (error) {
|
||||
console.error("Ran into an error fetching employee list: ", error);
|
||||
//TODO: trigger an alert window with an error message here!
|
||||
const openAddModifyDialog = async (employee_email?: string) => {
|
||||
is_add_modify_dialog_open.value = true;
|
||||
|
||||
if (employee_email === undefined) {
|
||||
management_mode.value = 'add_employee'
|
||||
employee.value = new EmployeeProfile();
|
||||
return;
|
||||
}
|
||||
isLoadingEmployeeList.value = false;
|
||||
|
||||
is_loading.value = true;
|
||||
management_mode.value = 'modify_employee';
|
||||
await getEmployeeDetails(employee_email);
|
||||
is_loading.value = false;
|
||||
}
|
||||
|
||||
const closeAddModifyDialog = () => {
|
||||
is_add_modify_dialog_open.value = false;
|
||||
management_mode.value = 'add_employee';
|
||||
employee.value = new EmployeeProfile;
|
||||
};
|
||||
|
||||
const getEmployeeDetails = async (email?: string) => {
|
||||
is_loading.value = true;
|
||||
const getEmployeeList = async (): Promise<boolean> => {
|
||||
try {
|
||||
const response = await EmployeeListService.getEmployeeList();
|
||||
if (response.success && response.data) employee_list.value = response.data;
|
||||
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error("Ran into an error fetching employee list: ", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getEmployeeDetails = async (email?: string): Promise<boolean> => {
|
||||
try {
|
||||
if (email === undefined) {
|
||||
const response = await EmployeeListService.getEmployeeDetails();
|
||||
employee.value = response;
|
||||
} else{
|
||||
if (response.success && response.data) employee.value = response.data;
|
||||
return response.success;
|
||||
} else {
|
||||
const response = await EmployeeListService.getEmployeeDetailsWithEmployeeEmail(email);
|
||||
employee.value = response;
|
||||
if (response.success && response.data) employee.value = response.data;
|
||||
return response.success;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('There was an error retrieving employee info: ', error);
|
||||
//TODO: trigger an alert window with an error message here!
|
||||
}
|
||||
is_loading.value = false;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return { employee, employee_list, isShowingEmployeeAddModifyWindow, isLoadingEmployeeList, is_loading, getEmployeeList, getEmployeeDetails };
|
||||
const createOrUpdateEmployee = async (profile: EmployeeProfile) => {
|
||||
let response;
|
||||
|
||||
if (management_mode.value === 'add_employee') {
|
||||
const { birth_date, external_payroll_id, last_work_day, ...create_payload} = profile;
|
||||
response = await EmployeeListService.createNewEmployee(create_payload);
|
||||
} else {
|
||||
|
||||
response = await EmployeeListService.updateEmployee(profile);
|
||||
}
|
||||
|
||||
closeAddModifyDialog();
|
||||
|
||||
if (response.success) await getEmployeeList();
|
||||
else {
|
||||
Notify.create({
|
||||
message: 'failed to update or create employee',
|
||||
color: 'negative',
|
||||
})}
|
||||
};
|
||||
|
||||
return {
|
||||
employee,
|
||||
employee_list,
|
||||
is_add_modify_dialog_open,
|
||||
management_mode,
|
||||
is_loading,
|
||||
getEmployeeList,
|
||||
getEmployeeDetails,
|
||||
openAddModifyDialog,
|
||||
closeAddModifyDialog,
|
||||
createOrUpdateEmployee,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,53 @@
|
|||
/* eslint-disable */
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
import { SchedulePresetsService } from "src/modules/profile/services/schedule-presets-service";
|
||||
import type { SchedulePreset } from "src/modules/profile/models/schedule-presets.models";
|
||||
import { SchedulePresetsService } from "src/modules/employee-list/services/schedule-presets-service";
|
||||
import { SchedulePreset, SchedulePresetFrontend } from "src/modules/employee-list/models/schedule-presets.models";
|
||||
|
||||
|
||||
export const useSchedulePresetsStore = defineStore('schedule_presets_store', () => {
|
||||
const schedule_presets = ref<SchedulePreset>();
|
||||
const schedule_presets = ref<SchedulePreset[]>([new SchedulePreset]);
|
||||
const current_schedule_preset = ref<SchedulePresetFrontend>(new SchedulePresetFrontend);
|
||||
const is_manager_open = ref(false);
|
||||
|
||||
const createSchedulePreset = async (): Promise<boolean> => {
|
||||
const setCurrentSchedulePreset = (preset_id: number) => {
|
||||
if (preset_id === -1) {
|
||||
current_schedule_preset.value = new SchedulePresetFrontend;
|
||||
return;
|
||||
}
|
||||
current_schedule_preset.value = new SchedulePresetFrontend(schedule_presets.value.find(preset => preset.id === preset_id)!)
|
||||
};
|
||||
|
||||
const openSchedulePresetManager = (preset_id: number) => {
|
||||
if (preset_id === -1) {
|
||||
current_schedule_preset.value = new SchedulePresetFrontend;
|
||||
} else {
|
||||
setCurrentSchedulePreset(preset_id);
|
||||
}
|
||||
|
||||
is_manager_open.value = true;
|
||||
};
|
||||
|
||||
const createSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => {
|
||||
try {
|
||||
// const new_preset: SchedulePreset = ??
|
||||
// await SchedulePresetsService.createSchedulePresets(new_preset);
|
||||
return true;
|
||||
const response = await SchedulePresetsService.createSchedulePresets(preset);
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while creating schedule preset: ', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateSchedulePreset = async (): Promise<boolean> => {
|
||||
const updateSchedulePreset = async (preset: SchedulePreset): Promise<boolean> => {
|
||||
try {
|
||||
return true;
|
||||
const response = await SchedulePresetsService.updateSchedulePresets(preset);
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while updating schedule preset: ', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSchedulePreset = async (preset_id: number): Promise<boolean> => {
|
||||
try {
|
||||
|
|
@ -37,16 +57,20 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
console.error('DEV ERROR || error while deleting schedule preset: ', error);
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const findSchedulePresetList = async (): Promise<boolean> => {
|
||||
try {
|
||||
return true;
|
||||
const response = await SchedulePresetsService.getSchedulePresetsList();
|
||||
if (response.success && response.data) schedule_presets.value = response.data;
|
||||
|
||||
return response.success;
|
||||
} catch (error) {
|
||||
console.error('DEV ERROR || error while searching for schedule presets: ', error);
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applySchedulePreset = async (): Promise<boolean> => {
|
||||
try {
|
||||
|
|
@ -55,10 +79,14 @@ export const useSchedulePresetsStore = defineStore('schedule_presets_store', ()
|
|||
console.error('DEV ERROR || error while building schedule: ', error);
|
||||
return false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
schedule_presets,
|
||||
current_schedule_preset,
|
||||
is_manager_open,
|
||||
setCurrentSchedulePreset,
|
||||
openSchedulePresetManager,
|
||||
createSchedulePreset,
|
||||
updateSchedulePreset,
|
||||
deleteSchedulePreset,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import { Notify } from "quasar";
|
|||
import { defineStore } from "pinia";
|
||||
import { ShiftService } from "src/modules/timesheets/services/shift-service";
|
||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import type { ShiftAPIError } from "src/modules/timesheets/models/shift.models";
|
||||
|
||||
export const useShiftStore = defineStore('shift_store', () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const shift_errors = ref<ShiftAPIError[]>([]);
|
||||
const shift_errors = ref<string[]>([]);
|
||||
|
||||
const deleteShiftById = async (shift_id: number): Promise<boolean> => {
|
||||
try {
|
||||
|
|
@ -22,7 +21,6 @@ export const useShiftStore = defineStore('shift_store', () => {
|
|||
|
||||
const createNewShifts = async (): Promise<boolean> => {
|
||||
if (timesheet_store.timesheets === undefined) return false;
|
||||
const has_errors = false;
|
||||
|
||||
try {
|
||||
const days = timesheet_store.timesheets.flatMap(week => week.days);
|
||||
|
|
@ -30,14 +28,10 @@ export const useShiftStore = defineStore('shift_store', () => {
|
|||
|
||||
if (new_shifts?.length > 0) {
|
||||
const response = await ShiftService.createNewShifts(new_shifts);
|
||||
if (response.every(res => res.ok)) {
|
||||
if (response.success) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
response.forEach(res => {
|
||||
shift_errors.value.push(res.error!);
|
||||
});
|
||||
}
|
||||
else { shift_errors.value.push(response.error!) }
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
|
|
@ -55,7 +49,7 @@ export const useShiftStore = defineStore('shift_store', () => {
|
|||
if (existing_shifts?.length > 0) {
|
||||
const response = await ShiftService.updateShifts(existing_shifts);
|
||||
|
||||
if (response.status < 400) {
|
||||
if (response.success) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,9 +64,11 @@ export const useUiStore = defineStore('ui', () => {
|
|||
|
||||
const setPreferences = () => {
|
||||
if (user_preferences.value !== undefined) {
|
||||
Dark.set(user_preferences.value.is_dark_mode ?? 'auto');
|
||||
// if user_preferences.value.is_dark_mode === null
|
||||
Dark.set(user_preferences.value.is_dark_mode ?? "auto");
|
||||
locale.value = user_preferences.value.display_language;
|
||||
}
|
||||
console.log('quasar dark mode: ', q.dark.mode, 'preferences: ', user_preferences.value.is_dark_mode);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
31
src/utils/table-grid-FLIP.ts
Normal file
31
src/utils/table-grid-FLIP.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { type Ref, nextTick } from 'vue';
|
||||
|
||||
export const animateFlip = (container: Ref<HTMLElement | null>) => {
|
||||
const el = container.value;
|
||||
if (!el) return;
|
||||
|
||||
const children = Array.from(el.children) as HTMLElement[];
|
||||
|
||||
// FIRST: record initial positions
|
||||
const firstRects = children.map(c => c.getBoundingClientRect());
|
||||
|
||||
// Do LAST → INVERT → PLAY after DOM update
|
||||
void nextTick(() => {
|
||||
const lastRects = children.map(c => c.getBoundingClientRect());
|
||||
|
||||
children.forEach((child, i) => {
|
||||
const dx = firstRects[i]!.left - lastRects[i]!.left;
|
||||
const dy = firstRects[i]!.top - lastRects[i]!.top;
|
||||
|
||||
if (!dx && !dy) return;
|
||||
|
||||
child.style.transition = 'none';
|
||||
child.style.transform = `translate(${dx}px, ${dy}px)`;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
child.style.transition = 'transform 250ms ease';
|
||||
child.style.transform = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user