Skip to content

Commit

Permalink
Implemented category management.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Apr 14, 2024
1 parent d56d16b commit 3b8a309
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 3 deletions.
77 changes: 77 additions & 0 deletions frontend/src/components/receipts/CategoryEdit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script setup lang="ts">
import { TarButton, TarModal } from "logitar-vue3-ui";
import { computed, ref, watchEffect } from "vue";
import { nanoid } from "nanoid";
import { useForm } from "vee-validate";
import { useI18n } from "vue-i18n";
import DisplayNameInput from "@/components/shared/DisplayNameInput.vue";
const { t } = useI18n();
const props = withDefaults(
defineProps<{
category?: string;
id?: string;
}>(),
{
id: () => nanoid(),
},
);
const displayName = ref<string>("");
const modalRef = ref<InstanceType<typeof TarModal> | null>(null);
const hasChanges = computed<boolean>(() => displayName.value.trim() !== (props.category?.trim() ?? ""));
function hide(): void {
modalRef.value?.hide();
}
const emit = defineEmits<{
(e: "saved", category: string): void;
}>();
const { handleSubmit, isSubmitting } = useForm();
const onSubmit = handleSubmit(async () => {
emit("saved", displayName.value);
hide();
});
watchEffect(() => (displayName.value = props.category ?? ""));
</script>

<template>
<span>
<TarButton
:icon="category ? 'fas fa-edit' : 'fas fa-plus'"
:text="t(category ? 'actions.edit' : 'actions.create')"
:variant="category ? 'primary' : 'success'"
data-bs-toggle="modal"
:data-bs-target="`#${id}`"
/>
<TarModal
:close="t('actions.close')"
fade
:id="id"
ref="modalRef"
:title="t(category ? 'receipts.categories.title.edit' : 'receipts.categories.title.new')"
>
<form @submit.prevent="onSubmit">
<DisplayNameInput required v-model="displayName" />
</form>
<template #footer>
<TarButton icon="fas fa-ban" :text="t('actions.cancel')" variant="secondary" @click="hide" />
<TarButton
:disabled="isSubmitting || !hasChanges"
:icon="category ? 'fas fa-save' : 'fas fa-plus'"
:loading="isSubmitting"
:status="t('loading')"
:text="t(category ? 'actions.save' : 'actions.create')"
:variant="category ? 'primary' : 'success'"
@click="onSubmit"
/>
</template>
</TarModal>
</span>
</template>
61 changes: 61 additions & 0 deletions frontend/src/components/receipts/CategoryList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import AppDelete from "@/components/shared/AppDelete.vue";
import CategoryEdit from "./CategoryEdit.vue";
import type { CategorySavedEvent } from "@/types/receipts";
import { useCategoryStore } from "@/stores/categories";
const categories = useCategoryStore();
const { t } = useI18n();
const emit = defineEmits<{
(e: "deleted", value: string): void;
(e: "saved", value: CategorySavedEvent): void;
}>();
function onDeleted(category: string): void {
if (categories.remove(category)) {
emit("deleted", category);
}
}
function onSaved(newCategory: string, oldCategory?: string): void {
if (categories.save(newCategory, oldCategory)) {
emit("saved", { newCategory, oldCategory });
} else {
// TODO(fpion): new name is already used
}
}
</script>

<template>
<div>
<div class="mb-3">
<CategoryEdit @saved="onSaved" />
</div>
<p v-if="categories.categories.length === 0">{{ t("receipts.empty") }}</p>
<table v-else class="table table-striped">
<thead>
<tr>
<th scope="col">{{ t("displayName") }}</th>
<th scope="col">{{ t("actions.title") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="category in categories.categories" :key="category">
<td>{{ category }}</td>
<td>
<CategoryEdit class="me-1" :category="category" @saved="onSaved($event, category)" />
<AppDelete
class="ms-1"
confirm="receipts.categories.delete.confirm"
:display-name="category"
title="receipts.categories.delete.title"
@confirmed="onDeleted(category)"
/>
</td>
</tr>
</tbody>
</table>
</div>
</template>
11 changes: 11 additions & 0 deletions frontend/src/i18n/en/receipts.en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
{
"categories": {
"delete": {
"confirm": "Do you really want to delete the following category?",
"title": "Delete category"
},
"title": {
"edit": "Edit category",
"list": "Categories",
"new": "New category"
}
},
"delete": {
"confirm": "Do you really want to delete the following receipt?",
"success": "The receipt has been deleted.",
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/i18n/fr/receipts.fr.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
{
"categories": {
"delete": {
"confirm": "Désirez-vous vraiment supprimer la catégorie suivante ?",
"title": "Supprimer la catégorie"
},
"title": {
"edit": "Éditer la catégorie",
"list": "Catégories",
"new": "Nouvelle catégorie"
}
},
"delete": {
"confirm": "Désirez-vous vraiment supprimer le reçu suivant ?",
"success": "Le reçu a été supprimé.",
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/stores/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineStore } from "pinia";
import { ref } from "vue";

export const useCategoryStore = defineStore(
"category",
() => {
const categories = ref<string[]>([]);

function remove(category: string): boolean {
const index = categories.value.findIndex((c) => c === category);
if (index < 0) {
return false;
}
categories.value.splice(index, 1);
return true;
}

function save(newCategory: string, oldCategory?: string): boolean {
if (newCategory === oldCategory || categories.value.includes(newCategory)) {
return false;
}
const index = categories.value.findIndex((c) => c === oldCategory);
if (index < 0) {
categories.value.push(newCategory);
} else {
categories.value.splice(index, 1, newCategory);
}
return true;
}

return { categories, remove, save };
},
{ persist: true },
); // TODO(fpion): tests
5 changes: 5 additions & 0 deletions frontend/src/types/receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export type CategorizeReceiptPayload = {
itemCategories: ReceiptItemCategory[];
};

export type CategorySavedEvent = {
newCategory: string;
oldCategory?: string;
};

export type DepartmentSummary = {
number: string;
displayName: string;
Expand Down
23 changes: 20 additions & 3 deletions frontend/src/views/receipts/ReceiptEdit.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<script setup lang="ts">
import { TarTab, TarTabs } from "logitar-vue3-ui";
import { computed, inject, onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
import AppBackButton from "@/components/shared/AppBackButton.vue";
import AppDelete from "@/components/shared/AppDelete.vue";
import CategoryList from "@/components/receipts/CategoryList.vue";
import StatusDetail from "@/components/shared/StatusDetail.vue";
import StatusInfo from "@/components/shared/StatusInfo.vue";
import type { ApiError } from "@/types/api";
Expand All @@ -18,7 +20,7 @@ const handleError = inject(handleErrorKey) as (e: unknown) => void;
const route = useRoute();
const router = useRouter();
const toasts = useToastStore();
const { t } = useI18n();
const { d, t } = useI18n();
const isDeleting = ref<boolean>(false);
const receipt = ref<Receipt>();
Expand Down Expand Up @@ -63,12 +65,19 @@ onMounted(async () => {
<main class="container-fluid">
<template v-if="receipt">
<h1>{{ title }}</h1>
<StatusDetail v-if="receipt" :aggregate="receipt">
<StatusDetail :aggregate="receipt" />
<p>
<span>
{{ t("receipts.issuedOn.format", { date: d(receipt.issuedOn, "medium") }) }}
<RouterLink :to="{ name: 'StoreEdit', params: { id: receipt.store.id } }">
<font-awesome-icon icon="fas fa-store" />{{ receipt.store.displayName }}
</RouterLink>
</span>
<template v-if="receipt.processedBy && receipt.processedOn">
<br />
<StatusInfo :actor="receipt.processedBy" :date="receipt.processedOn" format="receipts.processedOn" />
</template>
</StatusDetail>
</p>
<div class="mb-3">
<AppBackButton class="me-1" />
<AppDelete
Expand All @@ -81,6 +90,14 @@ onMounted(async () => {
@confirmed="onDelete"
/>
</div>
<TarTabs>
<TarTab active id="items" :title="`${t('receipts.items.title')} (${receipt.itemCount})`">
<!-- <ReceiptItemList :categories="categories" :receipt="receipt" /> -->
</TarTab>
<TarTab id="categories" :title="t('receipts.categories.title.list')">
<CategoryList />
</TarTab>
</TarTabs>
</template>
</main>
</template>

0 comments on commit 3b8a309

Please sign in to comment.