Add LineItem CSV import and fix Unit API
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, computed, watch, onMounted, onUpdated, toRaw } from "vue"
|
||||
import { Customer, Invoice, Contact, PaymentTerms, Address, LineItem, PaymentStatus } from "@/types"
|
||||
import { Customer, Invoice, Contact, PaymentTerms, Address, LineItem, PaymentStatus, Unit } from "@/types"
|
||||
import { newCustomer, newContact, newBillingData } from '@/types/index.d'
|
||||
import { toCurrency, toLocalDate, toShortISOString, calcDueDate, toFixedRounded } from '@/lib/utils'
|
||||
import axios from 'axios'
|
||||
@@ -24,7 +24,7 @@ import { Input } from '@/components/ui/crm-input';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'
|
||||
import { StatusBadge, statusBadgeLabels } from '@/components/ui/status-badge'
|
||||
import LineItemTable from '@/components/documents/LineItemTable.vue'
|
||||
import { Eye, FileText, Trash2, BookUser, User, CodeXml, MessageCircleQuestion, Loader2, Ellipsis, Check, FileCheck, Ban, Logs } from "lucide-vue-next"
|
||||
import { Eye, FileText, Trash2, BookUser, User, CodeXml, MessageCircleQuestion, Loader2, Ellipsis, Check, FileCheck, Ban, Logs, Import } from "lucide-vue-next"
|
||||
import { alertStore } from "@/stores/alertStore"
|
||||
import { GrowingTextarea } from '../ui/growing-textarea'
|
||||
import { toast } from "vue-sonner"
|
||||
@@ -38,7 +38,9 @@ const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
|
||||
const invoice = ref<Invoice>()
|
||||
const units = ref([] as Unit[])
|
||||
const customers = ref([] as Customer[])
|
||||
const paymentTermsData = ref([] as PaymentTerms[])
|
||||
const isDirty = ref(false)
|
||||
@@ -51,6 +53,8 @@ const alert = alertStore()
|
||||
const reminderDialogOpen = ref(false)
|
||||
const reminderLoading = ref(false)
|
||||
const value = ref<DateValue>() // TODO: name properly
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'cancel', 'delete'])
|
||||
|
||||
const isOpen = computed({
|
||||
@@ -74,10 +78,12 @@ onMounted(async () => {
|
||||
const promises = [];
|
||||
promises.push(axios.get('/api/customers'))
|
||||
promises.push(axios.get('/api/paymentterms'))
|
||||
promises.push(axios.get('/api/units'))
|
||||
const responses = await Promise.all(promises)
|
||||
let responseIndex = 0
|
||||
customers.value = responses[responseIndex].data as Customer[]
|
||||
paymentTermsData.value = responses[responseIndex + 1].data as PaymentTerms[]
|
||||
units.value = responses[responseIndex + 2].data as Unit[]
|
||||
} catch (error) {
|
||||
toast.error('Fehler beim Laden der Daten', error || String(error))
|
||||
}
|
||||
@@ -211,14 +217,14 @@ watch(importCustomer,
|
||||
invoice.value.billingData.companyName = newValue.companyName
|
||||
invoice.value.billingData.vatId = newValue.vatId
|
||||
|
||||
if (!invoice.value.billingData.billingAddress)
|
||||
invoice.value.billingData.billingAddress = newValue.billingAddress as Address
|
||||
if (!invoice.value.billingData.contactFirstName)
|
||||
invoice.value.billingData.contactFirstName = newValue.contacts && newValue.contacts.length > 0 ? newValue.contacts[0].firstName : ''
|
||||
if (!invoice.value.billingData.contactLastName)
|
||||
invoice.value.billingData.contactLastName = newValue.contacts && newValue.contacts.length > 0 ? newValue.contacts[0].lastName : ''
|
||||
if (!invoice.value.billingData.paymentTerms)
|
||||
invoice.value.billingData.paymentTerms = newValue.paymentTerms as PaymentTerms
|
||||
// if (!invoice.value.billingData.billingAddress)
|
||||
invoice.value.billingData.billingAddress = newValue.billingAddress as Address
|
||||
// if (!invoice.value.billingData.contactFirstName)
|
||||
invoice.value.billingData.contactFirstName = newValue.contacts && newValue.contacts.length > 0 ? newValue.contacts[0].firstName : ''
|
||||
// if (!invoice.value.billingData.contactLastName)
|
||||
invoice.value.billingData.contactLastName = newValue.contacts && newValue.contacts.length > 0 ? newValue.contacts[0].lastName : ''
|
||||
// if (!invoice.value.billingData.paymentTerms)
|
||||
invoice.value.billingData.paymentTerms = newValue.paymentTerms as PaymentTerms
|
||||
|
||||
// console.warn('trigger invoice watcher')
|
||||
invoice.value.customer = newValue
|
||||
@@ -305,7 +311,7 @@ const save = async () => {
|
||||
title: item.title,
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
unit: item.unit,
|
||||
unitId: item.unitId,
|
||||
price: item.price
|
||||
}))
|
||||
}
|
||||
@@ -470,6 +476,54 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
// console.groupEnd();
|
||||
}
|
||||
|
||||
const importLineItems = () => {
|
||||
if (!fileInput.value) {
|
||||
fileInput.value = document.createElement('input');
|
||||
fileInput.value.type = 'file';
|
||||
fileInput.value.accept = '.csv';
|
||||
fileInput.value.addEventListener('change', handleFileUpload);
|
||||
}
|
||||
fileInput.value.click();
|
||||
}
|
||||
|
||||
const handleFileUpload = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (!input.files || !input.files[0]) return;
|
||||
|
||||
const file = input.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('csv', file);
|
||||
|
||||
try {
|
||||
if (!invoice.value || !invoice.value.id) {
|
||||
throw new Error('Rechnung muss gespeichert werden, bevor Positionen importiert werden können');
|
||||
}
|
||||
|
||||
const response = await axios.post(`/api/lineitems/import/${invoice.value.id}`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
if (invoice.value) {
|
||||
invoice.value.items = response.data;
|
||||
isDirty.value = true;
|
||||
}
|
||||
|
||||
toast.success('Positionen erfolgreich importiert');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Importieren der Positionen:', error);
|
||||
toast.error('Fehler beim Importieren der Positionen', {
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
} finally {
|
||||
// Reset file input
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -553,11 +607,31 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
|
||||
|
||||
<!-- Import Items -->
|
||||
<DropdownMenuItem class="flex items-center justify-between" @click="importLineItems">
|
||||
<div class="flex items-center gap-3">
|
||||
<Import :strokeWidth="1.5" class="text-muted-foreground" />
|
||||
<div class="mr-4 flex flex-col">
|
||||
<span>Posten importieren</span>
|
||||
<span class="text-xs text-muted-foreground">(CSV)</span>
|
||||
</div>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
<Kbd class="visible-mac">⌘</Kbd>
|
||||
<Kbd class="visible-pc">Ctrl</Kbd>
|
||||
<Kbd>I</Kbd>
|
||||
</KbdGroup>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<!-- Preview -->
|
||||
<DropdownMenuItem v-if="invoice?.paymentStatus == 'draft'"
|
||||
class="flex items-center justify-between" @click="preview">
|
||||
<div class="flex items-center gap-3">
|
||||
<Eye :strokeWidth="1.5" class="text-current" />
|
||||
<Eye :strokeWidth="1.5" class="text-muted-foreground" />
|
||||
<span class="mr-4">Vorschau</span>
|
||||
</div>
|
||||
<KbdGroup>
|
||||
@@ -595,8 +669,8 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuSeparator v-if="invoice && invoice.paymentStatus != 'draft'" />
|
||||
|
||||
<!-- Audit -->
|
||||
<DropdownMenuItem v-if="invoice && invoice.paymentStatus != 'draft'"
|
||||
@@ -821,8 +895,8 @@ const updateLineItems = (newItems: LineItem[]) => {
|
||||
</div>
|
||||
|
||||
|
||||
<LineItemTable :lineItems="invoice.items" @update:lineItems="updateLineItems" sticky-top="7"
|
||||
:isLoading="itemsLoading" class="mt-4" />
|
||||
<LineItemTable :lineItems="invoice.items" :units="units" @update:lineItems="updateLineItems"
|
||||
sticky-top="7" :isLoading="itemsLoading" class="mt-4" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user