Implement YNAB Export from UI #22
@@ -11,6 +11,7 @@
 | 
				
			|||||||
    "@mdi/font": "5.9.55",
 | 
					    "@mdi/font": "5.9.55",
 | 
				
			||||||
    "@vueuse/core": "^7.6.1",
 | 
					    "@vueuse/core": "^7.6.1",
 | 
				
			||||||
    "autoprefixer": "^10.4.2",
 | 
					    "autoprefixer": "^10.4.2",
 | 
				
			||||||
 | 
					    "file-saver": "^2.0.5",
 | 
				
			||||||
    "pinia": "^2.0.11",
 | 
					    "pinia": "^2.0.11",
 | 
				
			||||||
    "postcss": "^8.4.6",
 | 
					    "postcss": "^8.4.6",
 | 
				
			||||||
    "tailwindcss": "^3.0.18",
 | 
					    "tailwindcss": "^3.0.18",
 | 
				
			||||||
@@ -18,6 +19,7 @@
 | 
				
			|||||||
    "vue-router": "^4.0.12"
 | 
					    "vue-router": "^4.0.12"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
 | 
					    "@types/file-saver": "^2.0.5",
 | 
				
			||||||
    "@vitejs/plugin-vue": "^2.0.0",
 | 
					    "@vitejs/plugin-vue": "^2.0.0",
 | 
				
			||||||
    "@vue/cli-plugin-babel": "5.0.0-beta.7",
 | 
					    "@vue/cli-plugin-babel": "5.0.0-beta.7",
 | 
				
			||||||
    "@vue/cli-plugin-typescript": "~4.5.0",
 | 
					    "@vue/cli-plugin-typescript": "~4.5.0",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, defineProps, onMounted, watchEffect } from "vue";
 | 
					import { computed, defineProps, onMounted, ref, watchEffect } from "vue";
 | 
				
			||||||
import Currency from "../components/Currency.vue";
 | 
					import Currency from "../components/Currency.vue";
 | 
				
			||||||
import { useBudgetsStore } from "../stores/budget";
 | 
					import { useBudgetsStore } from "../stores/budget";
 | 
				
			||||||
import { useAccountStore } from "../stores/budget-account";
 | 
					import { useAccountStore } from "../stores/budget-account";
 | 
				
			||||||
@@ -52,6 +52,18 @@ watchEffect(() => {
 | 
				
			|||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
    useSessionStore().setTitle("Budget for " + selected.value.Month + "/" + selected.value.Year);
 | 
					    useSessionStore().setTitle("Budget for " + selected.value.Month + "/" + selected.value.Year);
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const expandedGroups = ref<Map<string, boolean>>(new Map<string, boolean>())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function toggleGroup(group : {Name : string, Expand: boolean}) {
 | 
				
			||||||
 | 
					    console.log(expandedGroups.value);
 | 
				
			||||||
 | 
					    expandedGroups.value.set(group.Name, !(expandedGroups.value.get(group.Name) ?? group.Expand))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getGroupState(group : {Name : string, Expand: boolean}) : boolean {
 | 
				
			||||||
 | 
					    return expandedGroups.value.get(group.Name) ?? group.Expand;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -78,8 +90,8 @@ onMounted(() => {
 | 
				
			|||||||
            <th>Available</th>
 | 
					            <th>Available</th>
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
        <tbody v-for="group in GroupsForMonth">
 | 
					        <tbody v-for="group in GroupsForMonth">
 | 
				
			||||||
            <p class="text-lg font-bold">{{ group }}</p>
 | 
					            <a class="text-lg font-bold" @click="toggleGroup(group)">{{ (getGroupState(group) ? "−" : "+") + " " + group.Name }}</a>
 | 
				
			||||||
            <tr v-for="category in GetCategories(group)">
 | 
					            <tr v-for="category in GetCategories(group.Name)" v-if="getGroupState(group)">
 | 
				
			||||||
                <td>{{ category.Name }}</td>
 | 
					                <td>{{ category.Name }}</td>
 | 
				
			||||||
                <td></td>
 | 
					                <td></td>
 | 
				
			||||||
                <td></td>
 | 
					                <td></td>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { useBudgetsStore } from "../stores/budget";
 | 
				
			|||||||
import { useSessionStore } from "../stores/session";
 | 
					import { useSessionStore } from "../stores/session";
 | 
				
			||||||
import Card from "../components/Card.vue";
 | 
					import Card from "../components/Card.vue";
 | 
				
			||||||
import Button from "../components/Button.vue";
 | 
					import Button from "../components/Button.vue";
 | 
				
			||||||
 | 
					import { saveAs } from 'file-saver';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const transactionsFile = ref<File | undefined>(undefined);
 | 
					const transactionsFile = ref<File | undefined>(undefined);
 | 
				
			||||||
const assignmentsFile = ref<File | undefined>(undefined);
 | 
					const assignmentsFile = ref<File | undefined>(undefined);
 | 
				
			||||||
@@ -15,6 +16,10 @@ onMounted(() => {
 | 
				
			|||||||
    useSessionStore().setTitle("Settings");
 | 
					    useSessionStore().setTitle("Settings");
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const budgetStore = useBudgetsStore();
 | 
				
			||||||
 | 
					const CurrentBudgetID = computed(() => budgetStore.CurrentBudgetID);
 | 
				
			||||||
 | 
					const CurrentBudgetName = computed(() => budgetStore.CurrentBudgetName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function gotAssignments(e: Event) {
 | 
					function gotAssignments(e: Event) {
 | 
				
			||||||
    const input = (<HTMLInputElement>e.target);
 | 
					    const input = (<HTMLInputElement>e.target);
 | 
				
			||||||
    if (input.files != null)
 | 
					    if (input.files != null)
 | 
				
			||||||
@@ -26,19 +31,17 @@ function gotTransactions(e: Event) {
 | 
				
			|||||||
        transactionsFile.value = input.files[0];
 | 
					        transactionsFile.value = input.files[0];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
function deleteBudget() {
 | 
					function deleteBudget() {
 | 
				
			||||||
    const currentBudgetID = useBudgetsStore().CurrentBudgetID;
 | 
					    if (CurrentBudgetID.value == null)
 | 
				
			||||||
    if (currentBudgetID == null)
 | 
					 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    DELETE("/budget/" + currentBudgetID);
 | 
					    DELETE("/budget/" + CurrentBudgetID.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const budgetStore = useSessionStore();
 | 
					    const budgetStore = useSessionStore();
 | 
				
			||||||
    budgetStore.Budgets.delete(currentBudgetID);
 | 
					    budgetStore.Budgets.delete(CurrentBudgetID.value);
 | 
				
			||||||
    useRouter().push("/")
 | 
					    useRouter().push("/")
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
function clearBudget() {
 | 
					function clearBudget() {
 | 
				
			||||||
    const currentBudgetID = useBudgetsStore().CurrentBudgetID;
 | 
					    POST("/budget/" + CurrentBudgetID.value + "/settings/clear", null)
 | 
				
			||||||
    POST("/budget/" + currentBudgetID + "/settings/clear", null)
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
function cleanNegative() {
 | 
					function cleanNegative() {
 | 
				
			||||||
    // <a href="/budget/{{.Budget.ID}}/settings/clean-negative">Fix all historic negative category-balances</a>
 | 
					    // <a href="/budget/{{.Budget.ID}}/settings/clean-negative">Fix all historic negative category-balances</a>
 | 
				
			||||||
@@ -53,6 +56,22 @@ function ynabImport() {
 | 
				
			|||||||
    const budgetStore = useBudgetsStore();
 | 
					    const budgetStore = useBudgetsStore();
 | 
				
			||||||
    budgetStore.ImportYNAB(formData);
 | 
					    budgetStore.ImportYNAB(formData);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					function ynabExport() {
 | 
				
			||||||
 | 
					    const timeStamp = new Date().toISOString();
 | 
				
			||||||
 | 
					    POST("/budget/"+CurrentBudgetID.value+"/export/ynab/assignments", "")
 | 
				
			||||||
 | 
					        .then(x => x.text())
 | 
				
			||||||
 | 
					        .then(x => {
 | 
				
			||||||
 | 
					            var blob = new Blob([x], {type: "text/plain;charset=utf-8"});
 | 
				
			||||||
 | 
					            saveAs(blob, timeStamp + " " + CurrentBudgetName.value + " - Budget.tsv");
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    POST("/budget/"+CurrentBudgetID.value+"/export/ynab/transactions", "")
 | 
				
			||||||
 | 
					        .then(x => x.text())
 | 
				
			||||||
 | 
					        .then(x => {
 | 
				
			||||||
 | 
					            var blob = new Blob([x], {type: "text/plain;charset=utf-8"});
 | 
				
			||||||
 | 
					            saveAs(blob, timeStamp + " " + CurrentBudgetName.value + " - Transactions.tsv");
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
@@ -94,6 +113,13 @@ function ynabImport() {
 | 
				
			|||||||
                    <Button class="bg-blue-500" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
 | 
					                    <Button class="bg-blue-500" :disabled="filesIncomplete" @click="ynabImport">Importieren</Button>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </Card>
 | 
					            </Card>
 | 
				
			||||||
 | 
					            <Card class="flex-col p-3">
 | 
				
			||||||
 | 
					                <h2 class="text-lg font-bold">Export as YNAB TSV</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="flex flex-row">
 | 
				
			||||||
 | 
					                    <Button class="bg-blue-500" @click="ynabExport">Export</Button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </Card>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
@@ -67,7 +67,10 @@ export const useAccountStore = defineStore("budget/account", {
 | 
				
			|||||||
                let prev = undefined;
 | 
					                let prev = undefined;
 | 
				
			||||||
                for (const category of categories) {
 | 
					                for (const category of categories) {
 | 
				
			||||||
                    if(category.Group != prev)
 | 
					                    if(category.Group != prev)
 | 
				
			||||||
                        categoryGroups.push(category.Group);
 | 
					                        categoryGroups.push({
 | 
				
			||||||
 | 
					                            Name: category.Group,
 | 
				
			||||||
 | 
					                            Expand: category.Group != "Hidden Categories",
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
                    prev = category.Group;
 | 
					                    prev = category.Group;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                return categoryGroups;
 | 
					                return categoryGroups;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1141,6 +1141,11 @@
 | 
				
			|||||||
    "@types/qs" "*"
 | 
					    "@types/qs" "*"
 | 
				
			||||||
    "@types/serve-static" "*"
 | 
					    "@types/serve-static" "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"@types/file-saver@^2.0.5":
 | 
				
			||||||
 | 
					  version "2.0.5"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7"
 | 
				
			||||||
 | 
					  integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@types/glob@^7.1.1":
 | 
					"@types/glob@^7.1.1":
 | 
				
			||||||
  version "7.2.0"
 | 
					  version "7.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
 | 
					  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
 | 
				
			||||||
@@ -4137,6 +4142,11 @@ figures@^2.0.0:
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    escape-string-regexp "^1.0.5"
 | 
					    escape-string-regexp "^1.0.5"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					file-saver@^2.0.5:
 | 
				
			||||||
 | 
					  version "2.0.5"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
 | 
				
			||||||
 | 
					  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
file-uri-to-path@1.0.0:
 | 
					file-uri-to-path@1.0.0:
 | 
				
			||||||
  version "1.0.0"
 | 
					  version "1.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
 | 
					  resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user