diff --git a/package-lock.json b/package-lock.json index 7e526c7a650d9d72063f77ab6365299d023db7c2..a60b61f67195e50b022320405733b0e6d6f4e206 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@popperjs/core": "^2.11.8", "axios": "^1.6.8", "bootstrap": "^5.3.3", + "chart.js": "^4.4.2", "install": "^0.13.0", "jQuery": "^1.7.4", "js-cookie": "^3.0.5", @@ -21,6 +22,7 @@ "pinia-plugin-persist": "^1.0.0", "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.21", + "vue-chartjs": "^5.3.1", "vue-router": "^4.3.0", "xml2js": "^0.6.2" }, @@ -2943,6 +2945,11 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5291,6 +5298,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", + "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -13185,6 +13203,15 @@ } } }, + "node_modules/vue-chartjs": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.1.tgz", + "integrity": "sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-component-type-helpers": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.14.tgz", diff --git a/package.json b/package.json index f6f0fe5167b65c45684f9482e4e1e0930b5ee93b..c5fee03be5ba0bd83eac108d176d78c6812cec66 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@popperjs/core": "^2.11.8", "axios": "^1.6.8", "bootstrap": "^5.3.3", + "chart.js": "^4.4.2", "install": "^0.13.0", "jQuery": "^1.7.4", "js-cookie": "^3.0.5", @@ -29,6 +30,7 @@ "pinia-plugin-persist": "^1.0.0", "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.21", + "vue-chartjs": "^5.3.1", "vue-router": "^4.3.0", "xml2js": "^0.6.2" }, diff --git a/src/api/services/GoalService.ts b/src/api/services/GoalService.ts index 0f0781e3492860efcf75ae71afa3d23fec0685f1..f5301e2dbec1081e914aab9479cce1c18fdff5d7 100644 --- a/src/api/services/GoalService.ts +++ b/src/api/services/GoalService.ts @@ -78,4 +78,23 @@ export class GoalService { mediaType: 'application/json', }); } + /** + * Update a challenge + * Update a challenge day as completed + * @returns any Successfully updated the challenge + * @throws ApiError + */ + public static getGoal({ + id, + }: { + id: number, + }): CancelablePromise<GoalDTO> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/goals/{id}', + query: { + 'id': id, + }, + }); + } } diff --git a/src/api/services/UserService.ts b/src/api/services/UserService.ts index 51df6e46b965ecbb438f7dcfb68258422e13d22e..1be7d5b0c9164b5efc7e4442c1cd84dd678b2898 100644 --- a/src/api/services/UserService.ts +++ b/src/api/services/UserService.ts @@ -4,6 +4,10 @@ /* eslint-disable */ import type { Account } from '../models/Account'; import type { BankAccountDTO } from '../models/BankAccountDTO'; +import type { BudgetRequestDTO } from '../models/BudgetRequestDTO'; +import type { BudgetResponseDTO } from '../models/BudgetResponseDTO'; +import type { ExpenseRequestDTO } from '../models/ExpenseRequestDTO'; +import type { ExpenseResponseDTO } from '../models/ExpenseResponseDTO'; import type { FeedbackRequestDTO } from '../models/FeedbackRequestDTO'; import type { FeedbackResponseDTO } from '../models/FeedbackResponseDTO'; import type { PasswordResetDTO } from '../models/PasswordResetDTO'; diff --git a/src/components/SavingGoalComponents/SavingGoal.vue b/src/components/SavingGoalComponents/SavingGoal.vue index e3de05112d6f449f7d2a1f3f0ba047d8b056fcd7..40d65c8f1a21bf217e8d04792751f4458a07e52d 100644 --- a/src/components/SavingGoalComponents/SavingGoal.vue +++ b/src/components/SavingGoalComponents/SavingGoal.vue @@ -2,12 +2,18 @@ import SavingGoalList from "@/components/SavingGoalComponents/SavingGoalList.vue"; import SavingGoalRoadmap from "@/components/SavingGoalComponents/SavingGoalRoadmap.vue"; import SavingGoalCreate from "@/components/SavingGoalComponents/SavingGoalCreate.vue"; +import type {GoalDTO} from "@/api"; +import {GoalService} from "@/api"; + export default { components: {SavingGoalCreate, SavingGoalRoadmap, SavingGoalList}, data() { return { bluePanelMaxHeight: 'auto' as string, - createClicked: false as boolean, + createClicked: true as boolean, + selectedGoal: [] as any, + createdGoal: [] as any, + key: 0 as number, }; }, mounted() { @@ -20,7 +26,7 @@ export default { if (timelineElement instanceof HTMLElement) { // Calculate the max-height based on the height of the timeline const timelineHeight = timelineElement.offsetHeight; - this.bluePanelMaxHeight = `${timelineHeight*1.55}px`; + this.bluePanelMaxHeight = '700px'; } else { this.bluePanelMaxHeight = '700px'; } @@ -28,9 +34,19 @@ export default { createGoal() { this.createClicked = true; }, - goToSavingGoal() { + async goToSavingGoal(savingGoal: GoalDTO) { + this.$emit('goToSavingGoal', savingGoal); + let response = await GoalService.getGoal({id: savingGoal.id as number}); + console.log(response) + this.selectedGoal = response this.createClicked = false; + this.key++ }, + createSavingGoal(savingGoal: GoalDTO) { + this.$emit('createSavingGoal', savingGoal) + this.createdGoal = savingGoal; + this.createClicked = false; + } }, }; </script> @@ -38,7 +54,7 @@ export default { <template> <div class="cont"> <div class="row"> - <div class="col-lg-4 blue-background overflow-auto" :style="{ 'max-height': bluePanelMaxHeight }"> + <div class="col-lg-4 blue-background overflow-scroll" :style="{ 'max-height': bluePanelMaxHeight }"> <h3 style="color: white; margin-bottom: 16px">Your saving goals</h3> <div> <button class="btn btn-success btn-lg" style="font-weight: 600; margin-bottom: 20px" @click="createGoal">Create new saving goal</button> @@ -46,8 +62,8 @@ export default { <saving-goal-list @goToSavingGoal="goToSavingGoal"></saving-goal-list> </div> <div class="spacer"/> - <saving-goal-create v-if="createClicked"></saving-goal-create> - <saving-goal-roadmap v-else></saving-goal-roadmap> + <saving-goal-create @createGoalClick="createSavingGoal" v-if="createClicked"></saving-goal-create> + <saving-goal-roadmap :key="key" :selected-goal="selectedGoal" v-else></saving-goal-roadmap> </div> </div> </template> @@ -61,8 +77,12 @@ export default { } .blue-background { - background-color: #0A58CA; + margin-top: 20px; + margin-bottom: 20px; + padding: 12px; + background-color: #003A58; width: 27%; + border-radius: 0 2em 2em 0; } .spacer { diff --git a/src/components/SavingGoalComponents/SavingGoalCreate.vue b/src/components/SavingGoalComponents/SavingGoalCreate.vue index 280af93df3667458a4d0e26e20a9baee66553ca8..e839cd594e127679b9ea649ee87c7a56487b9240 100644 --- a/src/components/SavingGoalComponents/SavingGoalCreate.vue +++ b/src/components/SavingGoalComponents/SavingGoalCreate.vue @@ -1,4 +1,42 @@ <script setup lang="ts"> +import {GoalService, type CreateGoalDTO, type GoalDTO} from "@/api" +import {ref} from "vue"; + +const name = ref("") +const desc = ref("") +const date = ref("") +const amount = ref(0) +const createdConfirm = ref(""); +const errorMessage = ref("") + +const createGoalClick = async () => { + console.log("create goal clicked") + console.log(name.value) + console.log(desc.value) + console.log(date.value) + console.log(amount.value) + + + const createGoalPayload: CreateGoalDTO = { + name: name.value, + description: desc.value, + targetAmount: amount.value, + targetDate: date.value + " 00:00:00.000000000", + }; + + console.log(createGoalPayload) + + try { + let response = await GoalService.createGoal({ requestBody: createGoalPayload }); + if(response.name != "") { + createdConfirm.value = "Your goal has been created! Refresh the page to se it in the list" + errorMessage.value = "" + } + } catch (error: any) { + console.log(error.message); + errorMessage.value = error.message; + } +} </script> @@ -6,32 +44,40 @@ <div class="col-lg-8"> <h1>Create a new saving goal!</h1> <br> - <p>Create a name for this saving goal: </p> + <p>Create a name for this saving goal </p> <div class="input-group mb-3"> - <span class="input-group-text" id="basic-addon1">Name:</span> - <input type="text" class="form-control" placeholder="Name of Saving Goal" aria-label="Username" aria-describedby="basic-addon1"> + <span class="input-group-text" id="basic-addon1">Name</span> + <input v-model="name" type="text" class="form-control" placeholder="Name of Saving Goal" aria-label="Username" aria-describedby="basic-addon1" required> </div> - <p>Add a description to this saving goal: </p> + <p>Add a description to this saving goal </p> <div class="input-group mb-3"> - <span class="input-group-text" id="basic-addon2">Description:</span> - <textarea class="form-control" aria-label="With textarea"></textarea> + <span class="input-group-text" id="basic-addon2">Description</span> + <textarea v-model="desc" class="form-control" aria-label="With textarea"></textarea> </div> <!--Change this to date picker?--> - <p>When should this saving goal end?:</p> + <p>When should this saving goal end?</p> <div class="input-group mb-3"> - <input type="date" class="form-control" aria-label="Amount of days" placeholder="Amount of days (as number)"> + <input v-model="date" type="date" class="form-control" aria-label="Amount of days" placeholder="Amount of days (as number)" required> </div> - <p>How much do you want to save during this saving goal: </p> + <p>How much do you want to save during this saving goal? </p> <div class="input-group"> - <input type="text" class="form-control" aria-label="" placeholder="NOK (as number)"> + <input v-model="amount" type="number" class="form-control" aria-label="" placeholder="NOK (as number)" required> <span class="input-group-text">NOK</span> </div> <br> - <button class="btn btn-primary btn-lg">Create goal!</button> + <button class="btn btn-primary btn-lg" @click="createGoalClick">Create goal!</button> + + <div style="color: green; font-size: 32px"> + {{ createdConfirm }} + </div> + + <div style="color: red; font-size: 32px"> + {{ errorMessage }} + </div> </div> </template> diff --git a/src/components/SavingGoalComponents/SavingGoalList.vue b/src/components/SavingGoalComponents/SavingGoalList.vue index e792585dbd3ade07037dd7601843b58b121f5b24..23604b4bea8ff280bb018c04811c900a44fb362b 100644 --- a/src/components/SavingGoalComponents/SavingGoalList.vue +++ b/src/components/SavingGoalComponents/SavingGoalList.vue @@ -1,21 +1,26 @@ <script setup lang="ts"> -import {ref} from "vue"; - -const savingGoalList = ref([ - { title: 'Spain trip', MoneyTarget: '200kr', description: 'You wanted to save 200kr for a spain trip' }, - { title: 'Italy Escapade', MoneyTarget: '200kr', description: 'Experience the magic of Italy with us! Our goal is to save 200kr for an amazing trip to Italy.' }, - { title: 'French Getaway', MoneyTarget: '200kr', description: 'Join us as we plan to save 200kr for a delightful trip to France!' }, - { title: 'Exploring Greece', MoneyTarget: '200kr', description: 'Dreaming of Greece? Lets work together to save 200kr for that unforgettable trip!' }, - { title: 'German Adventure', MoneyTarget: '200kr', description: 'Discover the charm of Germany with us! We are aiming to save 200kr for this exciting adventure.' }, - { title: 'German Adventure', MoneyTarget: '200kr', description: 'Discover the charm of Germany with us! We are aiming to save 200kr for this exciting adventure.' }, - { title: 'German Adventure', MoneyTarget: '200kr', description: 'Discover the charm of Germany with us! We are aiming to save 200kr for this exciting adventure.' }, - { title: 'German Adventure', MoneyTarget: '200kr', description: 'Discover the charm of Germany with us! We are aiming to save 200kr for this exciting adventure.' } -]) +import {ref, onMounted} from "vue"; +import {GoalService, type GoalDTO, type CreateGoalDTO} from "@/api" + +const savingGoalList = ref([] as any) + +defineProps() + +const getGoals = async () => { + try { + let response = await GoalService.getGoals(); + savingGoalList.value = response + } catch (error: any) { + console.log(error.message); + } +} + +onMounted(getGoals) const emits = defineEmits(['goToSavingGoal']); -const goToSavingGoal = () => { - emits('goToSavingGoal'); +const goToSavingGoal = (savingGoal: GoalDTO) => { + emits('goToSavingGoal', savingGoal); }; const deleteSavingGoal = () => { @@ -24,14 +29,14 @@ const deleteSavingGoal = () => { </script> <template> - <div v-for="(savingGoal, index) in savingGoalList" :key="index" class="card bg-primary"> + <div v-for="(savingGoal, index) in savingGoalList" :key="index" class="card bg-body"> <div class="card-header"> Saving goal {{ index + 1 }} </div> <div class="card-body"> - <h5 class="card-title">{{ savingGoal.title }}</h5> + <h5 class="card-title">{{ savingGoal.goalName }}</h5> <p class="card-text">{{ savingGoal.description }}</p> - <a href="#" class="btn btn-light" @click="goToSavingGoal">Go to saving goal</a> + <a href="#" class="btn btn-primary" @click="goToSavingGoal(savingGoal)">Go to saving goal</a> <a href="#" class="btn btn-danger" @click="deleteSavingGoal" style="margin-left: 8px">Delete</a> </div> </div> @@ -42,11 +47,20 @@ const deleteSavingGoal = () => { color: white; } +.card-header { + background-color: #40698A; +} + +.card-body { + background-color: #447093; +} + * { font-weight: 600; } .card { margin-bottom: 20px; + border: none; } </style> \ No newline at end of file diff --git a/src/components/SavingGoalComponents/SavingGoalRoadmap.vue b/src/components/SavingGoalComponents/SavingGoalRoadmap.vue index 018c4579e1948559e1bbc2f93220fac6f97a3d0b..0f31c099235859f68a00f65281020f6375b73276 100644 --- a/src/components/SavingGoalComponents/SavingGoalRoadmap.vue +++ b/src/components/SavingGoalComponents/SavingGoalRoadmap.vue @@ -1,62 +1,248 @@ <script lang="ts"> -interface Step { - title: string; - showPanel: boolean; - description: string; - percentFinished: number; - unFinished: boolean; -} +import {CategoryScale, Chart as ChartJS, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from 'chart.js' +import {Line} from 'vue-chartjs' +import type {ChallengeDTO, GoalDTO, MarkChallengeDTO} from "@/api"; +import {GoalService} from '@/api' + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +) + + export default { + components: { + Line + }, data() { return { image: 'https://th.bing.com/th/id/OIG3.NMbdxmKYKVnxYGLOa0Z0?w=1024&h=1024&rs=1&pid=ImgDetMain' as string, altImage: 'https://th.bing.com/th/id/OIG4.gVWUC.rwCb8faTNx31yU?w=1024&h=1024&rs=1&pid=ImgDetMain' as string, title: 'Spain trip' as string, - //This will be changed to info gathered from backend - steps: [ - { title: 'Challenge 1', showPanel: false, description: 'Save 50kr on coffee in 3 days', percentFinished: 22, unFinished: false }, - { title: 'Challenge 2', showPanel: false, description: 'Save 500kr on food in 7 days', percentFinished: 73, unFinished: false }, - { title: 'Challenge 3', showPanel: false, description: 'Save 350kr on clothes in 5 days', percentFinished: 50, unFinished: true }, - { title: 'Challenge 4', showPanel: false, description: 'Save 150kr on coffee in 4 days', percentFinished: 10, unFinished: true }, - { title: 'Challenge 5', showPanel: false, description: 'Save 50kr on coffee in 3 days', percentFinished: 90, unFinished: true } - ] - , - bluePanelMaxHeight: 'auto' as string + bluePanelMaxHeight: 'auto' as string, + roadmapSelected: true as boolean, + statsSelected: false as boolean, + chartData: { + labels: [1, 2, 3, 4, 5, 6, '7'], + datasets: [ + { + label: this.selectedGoal.name, + backgroundColor: '#003A58', + data: [11, 24, 30, 47, 53, 62, 79] + } + ] + }, + chartOptions: { + responsive: true, + maintainAspectRatio: true, + scales: { + y: { + min: 0, + max: this.selectedGoal.targetAmount + } + } + }, + newPrice: 0, + savedSoFar: 0 as number, + currentChallengeIndex: 0, }; }, - mounted() { + async mounted() { setTimeout(() => { - this.togglePanel(this.steps[2]); + this.findCurrentChallenge() + this.disableAllChecksThatNotCurrent() + this.togglePanel(this.selectedGoal.challenges[this.currentChallengeIndex]) + this.calculateSavedSoFar() + this.onLoadDisableChecks(this.selectedGoal) + this.onLoadAddDataToGraph(this.selectedGoal) }, 500); - }, computed: { computeImageFilter() { - return (step: Step) => { - return step.unFinished ? 'none' : 'grayscale(100%)'; + return (challenge: ChallengeDTO) => { + return challenge ? 'none' : 'grayscale(100%)'; }; } }, + props: { + selectedGoal: { + type: Object, + default: null, + }, + }, methods: { - togglePanel(step: Step) { + togglePanel(step: any) { if (step.showPanel) { step.showPanel = false; } else { - this.steps.forEach((s) => (s.showPanel = false)); + this.selectedGoal.challenges.forEach((s: any) => (s.showPanel = false)); step.showPanel = true; this.scrollToPanel(step); } }, - scrollToPanel(step: Step) { + + scrollToPanel(step: any) { if (step.showPanel) { this.$nextTick(() => { - const panel = document.getElementById(`panel-${this.steps.indexOf(step)}`); + const panel = document.getElementById(`panel-${this.selectedGoal.challenges.indexOf(step)}`); if (panel) { panel.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); } }, + + changeDisplay() { + if (this.roadmapSelected) { + this.roadmapSelected = false + this.statsSelected = true + } else { + this.roadmapSelected = true + this.statsSelected = false + setTimeout(() => { + this.onLoadDisableChecks(this.selectedGoal) + this.disableAllChecksThatNotCurrent() + }, 100); + } + }, + + convertTemplateTextToChallengeText(challenge: ChallengeDTO) { + let challengeText: any + challengeText = challenge.challengeTemplate?.text + challengeText = challengeText.replace('{unit_amount}', challenge.challengeTemplate?.amount?.toString()) + challengeText = challengeText.replace('{checkDays}', challenge.checkDays?.toString()) + challengeText = challengeText.replace('{totalDays}', challenge.totalDays?.toString()) + let totalAmount: any + if (challenge.checkDays !== undefined && challenge.amount !== undefined) { + totalAmount = challenge.checkDays * challenge.amount; + } else { + // Handle the case when challenge.checkDays or challenge.amount is undefined + } + challengeText = challengeText.replace('{total_amount}', totalAmount.toString()) + challengeText = challengeText.replace('{amount}', challenge.amount?.toString()) + return challengeText + }, + + calculateTotalAmountFromChallenges() { + let totalAmountFromChallenges = 0 + for (const challenge of this.selectedGoal.challenges) { + totalAmountFromChallenges += challenge.amount + } + return totalAmountFromChallenges + }, + + async handleCheckboxClick(challenge: ChallengeDTO, index: number, amount: number) { + + this.lockCheckBox(challenge, index) + const markChallengePayload: MarkChallengeDTO = { + id: challenge.id, + day: index, + amount: amount, + }; + + try { + await GoalService.updateChallenge({ requestBody: markChallengePayload }); + const today: Date = new Date(); + const dateString: string = today.toISOString().split('T')[0]; // Extract YYYY-MM-DD part + + if (challenge.progressList) { + challenge.progressList.push({ day: index, amount: amount, completedAt: dateString }) + } + + this.addDataToChart(amount, dateString); + this.calculateSavedSoFar(); + } catch (error: any) { + console.log(error.message); + } + }, + + lockCheckBox(challenge: ChallengeDTO, index: number) { + const checkboxId = challenge.id + 'inlineCheckbox' + index + const checkbox = document.getElementById(checkboxId) as HTMLInputElement | null; + if (checkbox) { + // Disable the checkbox + checkbox.disabled = true; + } + }, + + onLoadDisableChecks(goal: GoalDTO) { + (goal.challenges || []).forEach((challenge: any) => { + challenge.progressList.forEach((progress: any) => { + // Assuming 'amount' is the property you want to add from progressList + const checkBoxId = challenge.id + 'inlineCheckbox' + progress.day + const checkbox = document.getElementById(checkBoxId) as HTMLInputElement | null; + if (checkbox) { + // Disable the checkbox + checkbox.checked = true; + checkbox.disabled = true; + } + }); + }); + }, + + onLoadAddDataToGraph(goal: GoalDTO) { + (goal.challenges || []).forEach((challenge: any) => { + challenge.progressList.forEach((progress: any) => { + this.addDataToChart(progress.amount, progress.completedAt); + }); + }); + }, + + addDataToChart(data: number, date: string) { + // Find the last dataset + const lastDataset = this.chartData.datasets[this.chartData.datasets.length - 1]; + + // Calculate the new label based on the last label + const newLabel = date.split('T')[0]; + + // Calculate the new data point based on the last data point + const lastDataPoint = lastDataset.data[lastDataset.data.length - 1]; + const newDataPoint = lastDataPoint + data; + + // Add the new label and data point to the chart data + this.chartData.labels.push(newLabel); + lastDataset.data.push(newDataPoint); + }, + + calculateSavedSoFar() { + this.savedSoFar = 0; // Reset savedSoFar before calculating again + this.selectedGoal.challenges.forEach((challenge: ChallengeDTO) => { + // Check if progressList exists before accessing its elements + if (challenge.progressList) { + challenge.progressList.forEach((progress: any) => { + // Assuming 'amount' is the property you want to add from progressList + this.savedSoFar += progress.amount; + }); + } + }); + }, + + findCurrentChallenge() { + const today: Date = new Date(); + this.selectedGoal.challenges.forEach((challenge: ChallengeDTO, index: number) => { + const startDate: Date = new Date(challenge.startDate as string); + const endDate: Date = new Date(challenge.endDate as string); + + if (today >= startDate && today <= endDate) { + this.currentChallengeIndex = index + } + }) + }, + + disableAllChecksThatNotCurrent() { + this.selectedGoal.challenges.forEach((challenge: ChallengeDTO, index: number) => { + if (index !== this.currentChallengeIndex && challenge.checkDays !== undefined) { + for (let i = 1; i < challenge.checkDays + 1; i++) { + this.lockCheckBox(challenge, i) + } + } + }) + } }, }; </script> @@ -64,52 +250,69 @@ export default { <template> <div class="col-lg-8"> <div class="SavingGoalTitle text-center"> - {{title}} + {{selectedGoal.name}} <br> <p class="d-inline-flex gap-1"> - <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample" style="font-size: 25px;"> - See more info + <button @click="changeDisplay" class="btn btn-primary" type="button" style="font-size: 25px;"> + <div v-if="roadmapSelected"> + Se statistikk + </div> + <div v-else> + Se sparesti + </div> </button> </p> - <div class="collapse" id="collapseExample"> - <div class="card card-body bg-primary mx-auto" style="width: 80%;"> - Total saved: 20kr - </div> - </div> </div> - <ul class="timeline"> - <li v-for="(step, index) in steps" :key="index" :class="{ 'timeline-inverted': index % 2 !== 0 }"> - <div class="timeline-image z-1" @click="togglePanel(step)"> - <img class="circular-image" :src="step.showPanel ? altImage : image" :style="{ filter: computeImageFilter(step) }" alt=""> - </div> - <div class="timeline-panel z-3" :id="'panel-' + index" v-show="step.showPanel"> - <div class="timeline-heading"> - <h4>{{ step.title }}</h4> - <h4 class="subheading">{{step.description}}</h4> + <div v-if="roadmapSelected"> + <ul class="timeline"> + <li v-for="(challenge, index) in selectedGoal.challenges" :key="index" :class="{ 'timeline-inverted': index % 2 !== 0 }"> + <div class="timeline-image z-1" @click="togglePanel(challenge)"> + <img class="circular-image" :src="challenge.showPanel ? altImage : image" :style="{ filter: computeImageFilter(challenge) }" alt=""> </div> - <div class="timeline-body"> - <br> - <p class=""> - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - </p> - <br> - <div class="progress"> - <div class="progress-bar" role="progressbar" :style="{ width: step.percentFinished + '%' }" :aria-valuenow="step.percentFinished" aria-valuemin="0" aria-valuemax="100"></div> + <div class="timeline-panel z-3" :id="'panel-' + index" v-show="challenge.showPanel"> + <div class="timeline-heading"> + <h4>Utfordring {{ index +1 }}</h4> + <h4 class="subheading">{{convertTemplateTextToChallengeText(challenge)}}</h4> </div> - <br> - <div class="form-check form-check-inline"> - <input class="form-check-input" type="checkbox" id="inlineCheckbox1" value="option1"> - <label class="form-check-label" style="color:white;" for="inlineCheckbox1">Day 1</label> - </div> - <div class="form-check form-check-inline"> - <input class="form-check-input" type="checkbox" id="inlineCheckbox2" value="option2"> - <label class="form-check-label" style="color:white;" for="inlineCheckbox1">Day 2</label> + <div class="timeline-body"> + <br> + <p> + Pris per enhet: {{challenge.challengeTemplate.amount}} kr <img src="../../assets/icons/edit-button.svg" alt="editIcon" data-bs-toggle="collapse" href="#collapseExample" role="button" aria-expanded="false" aria-controls="exampleModal"> + </p> + <br> + <div class="collapse" id="collapseExample" style="background-color: white; padding: 12px; border-radius: 5px"> + <div class="card card-body"> + <div class="input-group mb-3"> + <span class="input-group-text" id="basic-addon1">Ny pris</span> + <input v-model="newPrice" type="number" class="form-control" placeholder="Pris i kr" aria-label="newPrice" aria-describedby="basic-addon1" required> + </div> + </div> + <br> + <button class="btn btn-success">Bekreft endring</button> + </div> + <br> + <div class="progress"> + <div class="progress-bar" role="progressbar" :style="{ width: challenge.percentFinished + '%' }" :aria-valuenow="challenge.percentFinished" aria-valuemin="0" aria-valuemax="100"></div> + </div> + <br> + <div class="checkbox-row"> + <div class="form-check form-check-inline" v-for="(day, index) in challenge.checkDays" :key="index"> + <input class="form-check-input" @click="handleCheckboxClick(challenge, index+1, challenge.challengeTemplate.amount)" type="checkbox" :id="challenge.id + 'inlineCheckbox' + (index + 1)" :value="day" :disabled="day.checked"> + <label class="form-check-label" style="color:white;" :for="'inlineCheckbox' + (index + 1)">{{ day }}</label> + </div> + </div> </div> </div> - </div> - <div class="line" v-if="index < steps.length - 1"></div> - </li> - </ul> + <div class="line" v-if="index < selectedGoal.challenges.length - 1"></div> + </li> + </ul> + </div> + <div v-else> + <h3 style="font-weight: 600;">Du har så langt spart {{ savedSoFar }} kr</h3> + <h3 style="font-weight: 600;">Ditt penge mål er: {{selectedGoal.targetAmount}} kr</h3> + <h3 style="font-weight: 600;">Utfordringene i denne sparestien vil spare deg {{ calculateTotalAmountFromChallenges() }} kr</h3> + <Line ref="chart" :data="chartData" :options="chartOptions" /> + </div> </div> </template> @@ -127,7 +330,7 @@ export default { padding-bottom: 10px; color: white; border-radius: 1em; - background-color: #0A58CA; + background-color: #003A58; } .timeline { @@ -165,11 +368,12 @@ export default { .timeline > li .timeline-panel { position: relative; float: left; - width: 41%; + width: 31%; padding: 0 20px 20px 30px; text-align: right; - background-color: #0A58CA; + background-color: #003A58; border-radius: 1em; + margin-left: 110px; } .timeline>li .timeline-panel:before { @@ -190,7 +394,7 @@ export default { z-index: 100; position: absolute; left: 50%; - border: 7px solid #001664; + border: 7px solid #003A58; border-radius: 100%; background-color: #00ffff; box-shadow: 0 0 5px #00e1ff; @@ -210,6 +414,7 @@ export default { float: right; padding: 0 30px 20px 20px; text-align: left; + margin-right: 110px; } .timeline>li.timeline-inverted>.timeline-panel:before { @@ -431,4 +636,16 @@ export default { max-width: 100%; height: auto; } + +.checkbox-row { + display: flex; + flex-wrap: wrap; + margin-bottom: 12px; /* Adjust as needed */ + text-align: left !important; +} + +.form-check { + flex: 0 0 calc(25% - 10px); /* 20% width for each checkbox, adjust margin accordingly */ + margin-right: 10px; /* Adjust as needed */ +} </style> \ No newline at end of file