-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4c7e639
commit 90392dd
Showing
8 changed files
with
367 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
apps/dashboard/src/modules/financial/components/write-offs/WriteOffTable.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
82 changes: 82 additions & 0 deletions
82
apps/dashboard/src/modules/financial/components/write-offs/forms/WriteOffCreateForm.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
apps/dashboard/src/modules/financial/views/write-offs/WriteOffsView.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
}, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.