Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard/invoices): add ability to create invoices #304

Closed
wants to merge 8 commits into from
37 changes: 29 additions & 8 deletions apps/dashboard/src/components/FindUser.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template>
selected: {{ selectedUser }}
<Dropdown
v-model="selectedUser"
:options="users"
:options="users.concat(defaultUser)"
optionLabel="fullName"
:loading="loading"
:filter="true"
Expand All @@ -18,28 +19,33 @@
</template>

<script setup lang="ts">
import { onMounted, type PropType, ref, watch } from "vue";
import {onBeforeMount, onMounted, type PropType, ref, watch} from "vue";
import type { Ref } from "vue";
import apiService from "@/services/ApiService";
import { debounce } from "lodash";
import type { BaseUserResponse, UserResponse } from "@sudosos/sudosos-client";

const lastQuery = ref("");
const selectedUser = ref(null);
const selectedUser: Ref<UserResponse | undefined> = ref(undefined);
const loading = ref(false);

const users: Ref<(BaseUserResponse & { fullName: string })[]> = ref([]);
const emits = defineEmits(['update:value']);
const emits = defineEmits(['update:modelValue']);

defineProps({
value: {
const props = defineProps({
modelValue: {
type: Object as PropType<UserResponse>,
},
placeholder: {
type: String,
required: false,
default: ''
},
disabled: {
type: Boolean,
required: false,
default: false
},
});

const transformUsers = (userData: BaseUserResponse[]) => {
Expand All @@ -66,14 +72,29 @@ const filterUsers = (e: any) => {
}
};

const defaultUser: Ref<Array<BaseUserResponse & { fullName: string }>> = ref([]);

onBeforeMount(() => {
if (props.modelValue) {
defaultUser.value = transformUsers([props.modelValue]);
}
});

watch(
() => props.modelValue,
(newValue) => {
selectedUser.value = newValue;
}
);

onMounted(async () => {
apiService.user.getAllUsers(10, 0).then((res) => {
users.value = transformUsers(res.data.records); // Transform users
users.value = transformUsers(res.data.records.concat(props.modelValue || [])); // Transform users
});
});

watch(selectedUser, () => {
emits('update:value', selectedUser.value);
emits('update:modelValue', selectedUser.value);
});

</script>
Expand Down
11 changes: 4 additions & 7 deletions apps/dashboard/src/components/InputSpan.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,9 @@
:placeholder="placeholder"
v-model="internalValue as number"
:disabled="disabled"/>

<Checkbox v-if="type === 'boolean'"
v-model="internalValue"
binary
<InputSwitch v-if="type === 'switch'"
v-model="internalValue as boolean"
:disabled="disabled"/>

</span>
<div class="flex justify-content-end">
<ErrorSpan :error="errors"/>
Expand All @@ -49,7 +46,7 @@ import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import ErrorSpan from "@/components/ErrorSpan.vue";
import CalendarString from "@/components/CalendarString.vue";

import InputSwitch from "primevue/inputswitch";
import type { HintedString } from "primevue/ts-helpers";
import InputNumber from "primevue/inputnumber";

Expand Down Expand Up @@ -95,7 +92,7 @@ const emit = defineEmits(['update:value']);

const stringInputs = ['text', 'textarea'];
const numberInputs = ['currency', 'number'];
const booleanInputs = ['boolean'];
const booleanInputs = ['boolean', 'switch'];

const initialValue = () => {
if (stringInputs.includes(props.type)) return '';
Expand Down
12 changes: 10 additions & 2 deletions apps/dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,8 @@
"pointOfSaleCreated": "Successfully created point of sale.",
"waiveFinesSuccess": "Successfully waived fines.",
"waiveFinesRejected": "Canceled waiving fines.",
"canceled": "Canceled"
"canceled": "Canceled",
"invoiceCreated": "Successfully created invoice."
},
"termsOfService": {
"acceptFirst": "Accept the Terms of Service",
Expand Down Expand Up @@ -394,7 +395,14 @@
"AreYouSure": "Are you sure you want to delete this invoice?",
"CreditNoteWarning": "This will not create a credit note.",
"Unrecoverable": "This deletes the invoice and cannot be undone.",
"Credit": "Credit Invoice"
"Credit": "Credit Invoice",
"InvoiceUsers": "Invoice Users",
"Name": "Name",
"Balance": "Balance",
"CreateInvoice": "Create Invoice",
"isCreditInvoice": "Is credit invoice",
"InvoiceUsersDescription": "Invoice user with oustanding debts.",
"NoInvoiceableUsers": "Currently, there are no users with outstanding debts."
},
"pdf": {
"Table": "Table",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<template>
<CardComponent :header="$t('c_invoiceInfo.InvoiceUsers')" class="w-5">
<p class="my-0 font-italic text-sm ">{{ $t('c_invoiceInfo.InvoiceUsersDescription') }}</p>
<p v-if="invoiceableUsersWithBalance.length === 0">{{ $t('c_invoiceInfo.NoInvoiceableUsers') }}</p>
<DataTable
v-else
:value="invoiceableUsersWithBalance"
>
<Column field="user.id" :header="$t('c_invoiceInfo.id')"/>
<Column :header="$t('c_invoiceInfo.Name')" field="user.firstName">
<template #body="slotProps">
{{ `${slotProps.data.user.firstName} ${slotProps.data.user.lastName}` }}
</template>
</Column>
<Column field="balance.amount" :header="$t('c_invoiceInfo.Balance')">
<template #body="slotProps">
{{ formatPrice(slotProps.data.balance.amount) }}
</template>
</Column>
<Column :header="$t('c_invoiceInfo.Actions')" style="width: 10%">
<template #body="slotProps" >
<Button
type="button"
icon="pi pi-file-edit"
class="p-button-rounded p-button-text p-button-plain"
@click="() => handleCreateInvoice(slotProps.data.user)"
/>
</template>
</Column>

</DataTable>
</CardComponent>
<FormDialog v-model="showDialog" :form="form" :header="$t('invoice.CreateInvoice')">
<template #form="slotProps">
<InvoiceCreateForm :form="slotProps.form" @submit:success="showDialog = false"/>
</template>
</FormDialog>
</template>

<script setup lang="ts">
import CardComponent from "@/components/CardComponent.vue";
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import { onMounted, type Ref, ref } from "vue";
import type { BalanceResponse, UserResponse } from "@sudosos/sudosos-client";
import apiService from "@/services/ApiService";
import { formatPrice } from "sudosos-dashboard/src/utils/formatterUtils";
import FormDialog from "@/components/FormDialog.vue";
import InvoiceCreateForm from "@/modules/financial/components/invoice/forms/InvoiceCreateForm.vue";
import { schemaToForm } from "@/utils/formUtils";
import { createInvoiceSchema } from "@/utils/validation-schema";
import { useAuthStore, useUserStore } from "@sudosos/sudosos-frontend-common";

interface InvoiceableUserWithBalance {
user: UserResponse;
balance: BalanceResponse;
}

const userStore = useUserStore();
const invoiceableUsers: Ref<UserResponse[]> = ref([]);
const balances: Ref<BalanceResponse[]> = ref([]);
const invoiceableUsersWithBalance: Ref<InvoiceableUserWithBalance[]> = ref([]);
const showDialog: Ref<boolean> = ref(false);
const form = schemaToForm(createInvoiceSchema);

onMounted(async () => {
await getAllUsers(0);
await getInvoiceableBalances(0);
const usersMap = new Map(
invoiceableUsers.value.map(user => [user.id, user])
);

invoiceableUsersWithBalance.value = balances.value
.filter(balance => usersMap.has(balance.id))
.map(balance => ({
user: usersMap.get(balance.id)!,
balance: balance
}));
});

const getAllUsers = async (skip: number) => {
const response = await apiService.user.getAllUsersOfUserType("INVOICE", Number.MAX_SAFE_INTEGER, skip);
invoiceableUsers.value.push(...response.data.records);
if (response.data._pagination.count > response.data.records.length) {
await getAllUsers(skip + response.data.records.length);
}
};


// Typing is absolutely fucked in the api but it works
const getInvoiceableBalances = async (skip: number) => {
//@ts-ignore-next-line
const response = await apiService.balance.getAllBalance(undefined, undefined, -1, undefined, undefined, undefined, ["INVOICE"], undefined, undefined, false, Number.MAX_SAFE_INTEGER, skip);
//@ts-ignore-next-line
balances.value.push(...response.data.records);
//@ts-ignore-next-line
if (response.data._pagination.count > response.data.records.length) {
//@ts-ignore-next-line
await getInvoiceableBalances(skip + response.data.records.length);
}
};

const handleCreateInvoice = async (user: UserResponse) => {
const invoiceUserDefaults = await apiService.invoices.getSingleInvoiceUser(user.id);
showDialog.value = true;
const currentUser = userStore.getCurrentUser.user;
const values = {
for: user,
by: currentUser || undefined,
addressee: '',
description: '',
date: '',
reference: '',
isCreditInvoice: false,
street: invoiceUserDefaults.data.street || '',
postalCode: invoiceUserDefaults.data.postalCode || '',
city: invoiceUserDefaults.data.city || '',
country: invoiceUserDefaults.data.country || '',
attention: '',
};
form.context.resetForm({ values });
};

</script>

<style scoped lang="scss">

</style>
Loading
Loading