Add GoalCreateModal component with creation workflow and integrate it into DepartmentGoals.vue
This commit is contained in:
@@ -5,6 +5,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import DepartmentDashboardPageWrapper from "@/views/dashboards/departmentDashboard/DepartmentDashboardPageWrapper.vue";
|
||||
import NotFoundFallBackPageWrapper from "@/components/page/wrappers/NotFoundFallBackPageWrapper.vue";
|
||||
import { Goals } from "@/components/session/token/SessionUser/Objects/Goals.vue";
|
||||
import { showGoalCreateModal } from "./functions/showGoalCreateModal";
|
||||
|
||||
const departmentId = ref(SessionUser.functions.getDepartmentIdFromUrl());
|
||||
const goals = ref([]);
|
||||
@@ -99,7 +100,7 @@ const formatDate = (dateString) => {
|
||||
};
|
||||
|
||||
const onCreateGoal = () => {
|
||||
Goals.showCreateObjectForm(fetchGoals, { departments: [parseInt(departmentId.value)] });
|
||||
showGoalCreateModal(fetchGoals, { departments: [parseInt(departmentId.value)] });
|
||||
};
|
||||
|
||||
const onEditGoal = (goal) => {
|
||||
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
|
||||
const props = defineProps({
|
||||
initialDepartments: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
onSave: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
onCancel: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
const departments = ref([]);
|
||||
const products = ref([]);
|
||||
const users = ref([]);
|
||||
|
||||
const productSearch = ref('');
|
||||
const userSearch = ref('');
|
||||
|
||||
const filteredProducts = computed(() => {
|
||||
if (!productSearch.value) return products.value;
|
||||
return products.value.filter(p => p.name.toLowerCase().includes(productSearch.value.toLowerCase()));
|
||||
});
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
if (!userSearch.value) return users.value;
|
||||
return users.value.filter(u =>
|
||||
(u.display_name?.toLowerCase().includes(userSearch.value.toLowerCase())) ||
|
||||
(u.email?.toLowerCase().includes(userSearch.value.toLowerCase())) ||
|
||||
(u.customer_number?.toString().includes(userSearch.value))
|
||||
);
|
||||
});
|
||||
|
||||
const form = ref({
|
||||
departments: [...props.initialDepartments],
|
||||
criteria: {
|
||||
type: 'REVENUE',
|
||||
target: 0,
|
||||
start: new Date().toISOString().split('T')[0],
|
||||
end: new Date(new Date().setMonth(new Date().getMonth() + 1)).toISOString().split('T')[0],
|
||||
products: [],
|
||||
users: [],
|
||||
departments: [...props.initialDepartments]
|
||||
}
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [depsRes, prodsRes, usersRes] = await Promise.all([
|
||||
SessionUser.objects.departments.get.all(),
|
||||
SessionUser.objects.products.get.all(),
|
||||
authenticatedRequest('/users', 'GET')
|
||||
]);
|
||||
|
||||
departments.value = depsRes.data.data;
|
||||
products.value = prodsRes.data.data;
|
||||
users.value = usersRes.data.data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch data for goal creation:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
// Basic validation
|
||||
if (form.value.departments.length === 0) {
|
||||
alert("Vælg mindst én afdeling");
|
||||
return;
|
||||
}
|
||||
if (!form.value.criteria.target || form.value.criteria.target <= 0) {
|
||||
alert("Indtast et gyldigt mål");
|
||||
return;
|
||||
}
|
||||
|
||||
props.onSave(form.value);
|
||||
};
|
||||
|
||||
const toggleSelection = (list, id) => {
|
||||
const index = list.indexOf(id);
|
||||
if (index > -1) {
|
||||
list.splice(index, 1);
|
||||
} else {
|
||||
list.push(id);
|
||||
}
|
||||
};
|
||||
|
||||
const isType = (type) => form.value.criteria.type === type;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="goal-create-modal-content has-text-left">
|
||||
<div v-if="loading" class="has-text-centered py-5">
|
||||
<span class="icon is-large">
|
||||
<i class="fas fa-spinner fa-pulse fa-2x"></i>
|
||||
</span>
|
||||
<p>Henter data...</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- Departments (Ownership) -->
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Afdelinger (Ejer)
|
||||
<span class="tag is-rounded is-small ml-2" v-if="form.departments.length > 0">
|
||||
{{ form.departments.length }} valgt
|
||||
</span>
|
||||
</label>
|
||||
<div class="control">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div v-for="dept in departments" :key="dept.id" class="column is-6-mobile is-4-tablet">
|
||||
<label class="checkbox card p-2 h-100 is-flex is-align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.departments"
|
||||
:value="dept.id"
|
||||
@change="form.criteria.departments = [...form.departments]"
|
||||
>
|
||||
<span class="ml-2 is-size-7">{{ dept.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">Vælg hvilke afdelinger dette mål tilhører.</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Goal Type -->
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Mål Type</label>
|
||||
<div class="control has-icons-left">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="form.criteria.type">
|
||||
<option value="REVENUE">Omsætning</option>
|
||||
<option value="PRODUCT">Produktsalg</option>
|
||||
<option value="VISITS">Besøg / Antal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-bullseye"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Mål (Target)</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="number" v-model.number="form.criteria.target" placeholder="f.eks. 10000">
|
||||
<div class="icon is-small is-left">
|
||||
<i :class="isType('REVENUE') ? 'fas fa-dkk' : 'fas fa-hashtag'"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dates -->
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Start Dato</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="date" v-model="form.criteria.start">
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-calendar"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field">
|
||||
<label class="label">Slut Dato</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="date" v-model="form.criteria.end">
|
||||
<div class="icon is-small is-left">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Specific Criteria Selections -->
|
||||
<div class="tabs is-small is-boxed mb-2 mt-4">
|
||||
<ul>
|
||||
<li :class="{'is-active': true}">
|
||||
<a>
|
||||
<span>Yderligere filtre</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="box is-shadowless border">
|
||||
<!-- Products (only if type is PRODUCT or as optional filter) -->
|
||||
<div class="field mb-4">
|
||||
<label class="label is-size-7">
|
||||
Produkter (Valgfrit)
|
||||
<span class="tag is-rounded is-small ml-2" v-if="form.criteria.products.length > 0">
|
||||
{{ form.criteria.products.length }} valgt
|
||||
</span>
|
||||
</label>
|
||||
<div class="control mb-2">
|
||||
<input class="input is-small" type="text" v-model="productSearch" placeholder="Søg produkter...">
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="scrollable-selection">
|
||||
<div v-for="prod in filteredProducts" :key="prod.id" class="selection-item">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" :value="prod.id" v-model="form.criteria.products">
|
||||
{{ prod.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users / Customers -->
|
||||
<div class="field">
|
||||
<label class="label is-size-7">
|
||||
Kunder / Medarbejdere (Valgfrit)
|
||||
<span class="tag is-rounded is-small ml-2" v-if="form.criteria.users.length > 0">
|
||||
{{ form.criteria.users.length }} valgt
|
||||
</span>
|
||||
</label>
|
||||
<div class="control mb-2">
|
||||
<input class="input is-small" type="text" v-model="userSearch" placeholder="Søg kunder...">
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="scrollable-selection">
|
||||
<div v-for="user in filteredUsers" :key="user.id" class="selection-item">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" :value="user.customer_number" v-model="form.criteria.users">
|
||||
{{ user.display_name || user.email }} ({{ user.customer_number }})
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons is-right mt-5">
|
||||
<button class="button" @click="onCancel">Annuller</button>
|
||||
<button class="button is-dark" @click="submit">Opret mål</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.goal-create-modal-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.scrollable-selection {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background: #fdfdfd;
|
||||
}
|
||||
|
||||
.selection-item {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid #dbdbdb;
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { h, render } from 'vue';
|
||||
import Swal from 'sweetalert2';
|
||||
import GoalCreateModal from '../components/GoalCreateModal.vue';
|
||||
import { Goals } from "@/components/session/token/SessionUser/Objects/Goals.vue";
|
||||
|
||||
/**
|
||||
* Show the custom goal create modal
|
||||
* @param {Function} onAfterSubmit Callback after successful creation
|
||||
* @param {Object} lockedValues Initial values (e.g., { departments: [1] })
|
||||
*/
|
||||
export const showGoalCreateModal = (onAfterSubmit = null, lockedValues = {}) => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const swalInstance = Swal.fire({
|
||||
title: 'Opret nyt mål',
|
||||
html: container,
|
||||
showConfirmButton: false,
|
||||
width: '800px',
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => {
|
||||
const vnode = h(GoalCreateModal, {
|
||||
initialDepartments: lockedValues.departments || [],
|
||||
onSave: async (formData) => {
|
||||
try {
|
||||
Swal.showLoading();
|
||||
await Goals.add(formData.departments, formData.criteria);
|
||||
Swal.fire({
|
||||
title: 'Succes!',
|
||||
text: 'Målet er blevet oprettet.',
|
||||
icon: 'success',
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
if (onAfterSubmit) onAfterSubmit();
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to create goal:", error);
|
||||
Swal.fire({
|
||||
title: 'Fejl!',
|
||||
text: 'Der opstod en fejl ved oprettelse af målet.',
|
||||
icon: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
Swal.close();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
render(vnode, container);
|
||||
},
|
||||
willClose: () => {
|
||||
render(null, container);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user