Add GoalCreateModal component with creation workflow and integrate it into DepartmentGoals.vue

This commit is contained in:
Jeppe Bundgaard
2026-01-26 18:12:17 +01:00
parent 159470d921
commit 09b0b6c80b
3 changed files with 354 additions and 1 deletions
@@ -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) => {
@@ -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>
@@ -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);
}
});
});
};