Skip to content

Commit

Permalink
feat(dashboard): write-off overview
Browse files Browse the repository at this point in the history
  • Loading branch information
JustSamuel committed Feb 18, 2025
1 parent 4c7e639 commit 90392dd
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 3 deletions.
5 changes: 5 additions & 0 deletions apps/dashboard/src/components/TopNavbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ const navItems = computed(() => [
route: '/payout',
visible: isAllowed('get', ['all'], 'SellerPayout', ['any']),
notifications: pendingPayouts?.value
},
{
label: t('common.navigation.writeOffs'),
route: '/write-offs',
visible: isAllowed('get', ['all'], 'WriteOff', ['any']),
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<template>
<div class="flex flex-col gap-5">
<DataTable
:rows="rows"
:value="writeOffs"
:rows-per-page-options="[5, 10, 25, 50, 100]"
:paginator="paginator"
lazy
@page="onPage($event)"
:total-records="totalRecords"
data-key="id"
class="w-full"
tableStyle="min-width: 50rem"
>
<template #header>
<div class="flex flex-row align-items-center justify-content-between">
<IconField iconPosition="left">
<InputIcon class="pi pi-search"> </InputIcon>
<InputText v-model="idQuery" :placeholder="t('common.id')"
@keyup.enter="searchId()" @focusout="searchId()" />
</IconField>
<Button :label="t('common.create')" icon="pi pi-plus" @click="showDialog = true" />
</div>
</template>
<Column field="id" :header="t('common.id')">
<template #body="slotProps">
<Skeleton v-if="isLoading" class="w-6 my-1 h-1rem surface-300" />
<span v-else>{{ slotProps.data.id }}</span>
</template>
</Column>
<Column field="date" :header="t('common.date')">
<template #body="slotProps">
<Skeleton v-if="isLoading" class="w-6 my-1 h-1rem surface-300" />
<span v-else>{{ formatDateFromString(slotProps.data.createdAt) }}</span>
</template>
</Column>
<Column :header="t('common.name')">
<template #body="slotProps">
<Skeleton v-if="isLoading" class="w-6 my-1 h-1rem surface-300" />
<span v-else>{{ getName(slotProps.data) }}</span>
</template>
</Column>
<Column field="amount" :header="t('common.amount')">
<template #body="slotProps">
<Skeleton v-if="isLoading" class="w-3 my-1 h-1rem surface-300" />
<span v-else>{{ formatPrice(slotProps.data.amount) }}</span>
</template>
</Column>
<Column :header="t('common.actions')" style="width: 10%">
<template #body="slotProps">
<Skeleton v-if="isLoading" class="w-3 my-1 h-1rem surface-300" />
<span v-else class="flex flex-row align-items-center">
<Button
v-tooltip.top="t('common.delete')"
type="button"
icon="pi pi-times"
class="p-button-rounded p-button-text p-button-plain"
@click="() => showWarning()"
/>
<Button
v-tooltip.top="t('common.downloadPdf')"
type="button"
icon="pi pi-file-export"
class="p-button-rounded p-button-text p-button-plain"
@click="() => downloadPdf(slotProps.data.id)"
/>
</span>
</template>
</Column>
</DataTable>
</div>
<Dialog
modal
ref="dialog"
@show="addListenerOnDialogueOverlay(dialog)"
v-model:visible="showWarningModal"
:draggable="false"
class="w-auto flex w-9 md:w-4"
:header="t('modules.financial.write-offs.delete')"
>
{{ t('modules.financial.write-offs.delete.not-possible') }}
</Dialog>
<FormDialog v-model="showDialog" :form="form" :header="t('modules.financial.write-off.create')" :is-editable="true">
<template #form="slotProps">
<WriteOffCreateForm :form="slotProps.form" @submit:success="showDialog = false"/>
</template>
</FormDialog>
</template>

<script setup lang="ts">
import { useWriteOffStore } from "@/stores/writeoff.store";
import { onMounted, type Ref, ref, watch } from "vue";
import type { PaginatedWriteOffResponse, WriteOffResponse } from "@sudosos/sudosos-client";
import type { DataTablePageEvent } from "primevue/datatable";
import { formatDateFromString, formatPrice } from "@/utils/formatterUtils";
import Column from "primevue/column";
import { useI18n } from "vue-i18n";
import Button from "primevue/button";
import { addListenerOnDialogueOverlay } from "@/utils/dialogUtil";
import { useToast } from "primevue/usetoast";
import FormDialog from "@/components/FormDialog.vue";
import PayoutCreateForm from "@/modules/financial/components/payout/forms/PayoutCreateForm.vue";
import { schemaToForm } from "@/utils/formUtils";
import { createWriteOffSchema } from "@/utils/validation-schema";
import WriteOffCreateForm from "@/modules/financial/components/write-offs/forms/WriteOffCreateForm.vue";
const { t } = useI18n();
const toast = useToast();
const writeOffStore = useWriteOffStore();
const totalRecords = ref<number>(0);
const isLoading = ref<boolean>(true);
const rows = ref<number>(5);
const paginator = ref<boolean>(true);
const page = ref<number>(0);
const writeOffs = ref();
const showWarningModal: Ref<boolean> = ref(false);
const dialog = ref();
const showWarning = () => {
showWarningModal.value = true;
};
const showDialog: Ref<boolean> = ref(false);
const form = schemaToForm(createWriteOffSchema);
const idQuery = ref<string>('');
onMounted(async () => {
await loadWriteOffs();
});
async function loadWriteOffs() {
isLoading.value = true;
const response: PaginatedWriteOffResponse = await writeOffStore.fetchWriteOffs(rows.value, page.value);
if (response) {
writeOffs.value = response.records;
totalRecords.value = response._pagination.count || 0;
}
isLoading.value = false;
}
async function onPage(event: DataTablePageEvent) {
rows.value = event.rows;
page.value = event.first;
await loadWriteOffs();
}
watch(() => writeOffStore.getUpdatedAt, () => {
loadWriteOffs().then(() => console.error('loaded', writeOffs.value));
});
const searchId = () => {
const queryNumber = parseInt(idQuery.value);
const isNan = isNaN(queryNumber);
if (isNan) {
loadWriteOffs();
return;
}
writeOffStore.fetchWriteOff(queryNumber).then((res) => {
writeOffs.value = [res];
}).catch(() => {
writeOffs.value = [];
});
};
const getName = (writeOff: WriteOffResponse) => {
return writeOff.to.firstName + ' ' + writeOff.to.lastName;
};
const downloadPdf = async (id: number) => {
toast.add({
severity: 'warn',
summary: t('common.toast.info.unsupported'),
detail: t('modules.financial.write-offs.downloadPdf.not-possible'),
life: 3000,
});
};
</script>

<style scoped lang="scss">
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<div class="flex flex-column justify-content-between gap-2">
<InputUserSpan :label="t('modules.financial.forms.payout.for')"
:value="form.model.user.value.value"
@update:value="form.context.setFieldValue('user', $event)"
:errors="form.context.errors.value.user"
:show-positive="true"
id="name" placeholder="John Doe"/>

<skeleton v-if="userBalance === null && form.model.user.value.value" class="w-6 my-1 h-0.5rem surface-300"/>
<div v-else-if="userBalance" class="flex flex-row gap-1"
:class="{'text-gray-700': !balanceError, 'text-red-500 font-bold': balanceError}">
<span>
{{ t('modules.financial.forms.payout.currentBalance', { balance: formatPrice(userBalance.amount) }) }}</span>
</div>

<div class="flex w-full justify-content-end">
<ErrorSpan :error="balanceError"/>
</div>

</div>
</template>

<script setup lang="ts">
import { type PropType, ref, type Ref, watch } from "vue";
import * as yup from "yup";
import { useI18n } from "vue-i18n";
import { type createWriteOffSchema } from "@/utils/validation-schema";
import type { Form } from "@/utils/formUtils";
import { setSubmit } from "@/utils/formUtils";
import { useToast } from "primevue/usetoast";
import InputUserSpan from "@/components/InputUserSpan.vue";
import { formatPrice } from "@/utils/formatterUtils";
import apiService from "@/services/ApiService";
import type { BalanceResponse } from "@sudosos/sudosos-client";
import ErrorSpan from "@/components/ErrorSpan.vue";
const { t } = useI18n();
const toast = useToast();
const emit = defineEmits(['submit:success', 'submit:error']);
const props = defineProps({
form: {
type: Object as PropType<Form<yup.InferType<typeof createWriteOffSchema>>>,
required: true,
},
});
const userBalance: Ref<BalanceResponse | null | undefined> = ref(null);
const balanceError = ref<string>('');
watch(() => props.form.model.user.value.value, () => {
if (props.form.model.user.value.value.id) {
apiService.balance.getBalanceId(props.form.model.user.value.value.id).then((res) => {
userBalance.value = res.data;
}).catch(() => {
userBalance.value = undefined;
});
}
});
const validateAmount = () => {
if (userBalance.value && userBalance.value.amount.amount >= 0) {
balanceError.value = `${t('modules.financial.forms.write-off.negative')}`;
} else {
balanceError.value = '';
}
};
watch(() => userBalance.value, () => {
validateAmount();
});
setSubmit(props.form, props.form.context.handleSubmit(async (values) => {
console.error('values', values);
}));
</script>

<style scoped lang="scss">
</style>
13 changes: 11 additions & 2 deletions apps/dashboard/src/modules/financial/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import InvoiceOverview from "@/modules/financial/views/invoice/InvoiceOverview.v
import InvoiceInfoView from "@/modules/financial/views/invoice/InvoiceInfoView.vue";
import { isAllowed } from "@/utils/permissionUtils";
import InvoiceCreateView from "@/modules/financial/views/invoice/InvoiceCreateView.vue";
import InvoiceAccountOverview from "@/modules/financial/views/invoice/InvoiceAccountOverview.vue";
import WriteOffsView from "@/modules/financial/views/write-offs/WriteOffsView.vue";

export function financialRoutes(): RouteRecordRaw[] {
return [
Expand Down Expand Up @@ -71,7 +71,16 @@ export function financialRoutes(): RouteRecordRaw[] {
requiresAuth: true,
isAllowed: () => isAllowed('get', ['own', 'organ'], 'SellerPayout', ['any'])
}
}
},
{
path: '/write-offs',
component: WriteOffsView,
name: 'writeOffs',
meta: {
requiresAuth: true,
isAllowed: () => isAllowed('get', ['all'], 'WriteOff', ['any'])
}
}
]
}
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template>
<div class="page-container">
<div class="page-title">{{ t('modules.financial.write-offs.title') }}</div>
<div class="content-wrapper flex flex-column">
<CardComponent :header="t('modules.financial.write-offs.table.header')" class="full-width">

<WriteOffTable/>
</CardComponent>
</div>
</div>
</template>

<script setup lang="ts">
import WriteOffTable from "@/modules/financial/components/write-offs/WriteOffTable.vue";
import { useI18n } from "vue-i18n";
import CardComponent from "@/components/CardComponent.vue";
const { t } = useI18n();
</script>

<style scoped lang="scss">
</style>
44 changes: 44 additions & 0 deletions apps/dashboard/src/stores/writeoff.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import apiService from "@/services/ApiService";
import type { PaginatedWriteOffResponse, WriteOffResponse, WriteOffRequest } from "@sudosos/sudosos-client";
import { defineStore } from "pinia";

export const useWriteOffStore = defineStore('writeoff', {
state: () => ({
writeOffs: {} as Record<number,any>,
updatedAt: 0,
}),
getters: {
getWriteOff: (state) => (id: number): any | null => {
return state.writeOffs[id] || null;
},
getUpdatedAt(): number {
return this.updatedAt;
},
getAllWriteOffs(): Record<number, any> {
return this.writeOffs;
}
},
actions: {
async fetchWriteOffs(take: number, skip: number): Promise<PaginatedWriteOffResponse> {
return apiService.writeOffs.getAllWriteOffs(undefined, undefined, take, skip).then((res) => {
res.data.records.forEach((writeOff: WriteOffResponse) => {
this.writeOffs[writeOff.id] = writeOff;
});
return res.data;
});
},
async fetchWriteOff(id: number): Promise<WriteOffResponse> {
return apiService.writeOffs.getSingleWriteOff(id).then((res) => {
this.writeOffs[id] = res.data;
return res.data;
});
},
async createWriteOff(values: WriteOffRequest): Promise<WriteOffResponse> {
return apiService.writeOffs.createWriteOff(values).then((res) => {
this.writeOffs[res.data.id] = res.data;
this.updatedAt = Date.now();
return res.data;
});
},
},
});
5 changes: 5 additions & 0 deletions apps/dashboard/src/utils/validation-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,8 @@ export const createInvoiceObject =
date: yup.string().required(),
attention: yup.string(),
});

export const createWriteOffSchema =
yup.object({
user: yup.mixed<BaseUserResponse>().required(),
});
Loading

0 comments on commit 90392dd

Please sign in to comment.