Add InvoiceService and initial invoice dashboard widget

This commit is contained in:
2026-05-27 11:12:26 +02:00
parent 56bf75bf74
commit 167bee78f8
7 changed files with 198 additions and 24 deletions
@@ -185,6 +185,19 @@ public function salesStatistics()
return ApiDataTransformer::snakeToCamel($statistics);
}
public static function getDueInvoices()
{
$today = date('Y-m-d');
$invoices = Invoice::where('due_date', '<=', $today)
->whereIn('payment_status', ['issued', 'due', 'reminded'])
->orderBy('due_date', 'asc')
->get();
return $invoices->map(function ($invoice) {
return ApiDataTransformer::snakeToCamel($invoice->toArray());
});
}
public function preview($id)
{
@@ -479,7 +479,7 @@ const updateLineItems = (newItems: LineItem[]) => {
// Calculate the new total amount
let total = 0;
updatedItems.forEach(item => {
updatedItems.forEach((item: LineItem) => {
total += item.quantity * item.price;
});
invoice.value.totalAmount = total;
+35 -19
View File
@@ -2,31 +2,38 @@
import Heading from '@/components/Heading.vue';
import { onMounted, ref } from "vue"
import AppLayout from '@/layouts/AppLayout.vue';
import { Trophy, UserCheck2, X, ChevronRight } from 'lucide-vue-next';
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'
import Button from '@/components/ui/crm-button/Button.vue';
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'
import { invoices } from '@/routes';
import { toRoundedCurrency } from '@/lib/utils'
import { toRoundedCurrency, toLocalDate } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Link, usePage } from '@inertiajs/vue3';
import { usePage, router } from '@inertiajs/vue3';
import axios, { AxiosError } from "axios";
import { toast } from "vue-sonner";
import { AppPageProps, Todo } from "@/types";
import { SalesStatistics, Todo, Invoice } from "@/types";
import Todos from '@/components/Todos.vue';
import InvoiceService from '@/services/InvoiceService';
import { Table, TableRow, TableCell } from '@/components/ui/crm-table';
const salesStatistics = ref({
interface Props {
dueInvoices: Invoice[];
}
const props = defineProps<Props>();
const salesStatistics = ref<SalesStatistics>({
year: new Date().getFullYear(),
totalRevenue: 0,
paid: 0,
paidPercent: 0,
draft: 0,
draftPercent: 0,
issued: 0,
issuedPercent: 0,
due: 0,
duePercent: 0,
reminded: 0,
remindedPercent: 0
})
const salesTarget = ref(60000) // TODO: aus settings
const todos = ref<Todo[]>([])
const showCompleted = ref(false)
@@ -48,8 +55,7 @@ onMounted(async () => {
// Load sales statistics
try {
let response = await axios.get('/api/invoices/salesStatistics')
salesStatistics.value = response.data
salesStatistics.value = await InvoiceService.getSalesStatistics()
} catch (error) {
toast.error('Fehler beim Laden der Daten', { description: (error as AxiosError).message })
}
@@ -67,7 +73,7 @@ onMounted(async () => {
<!-- Nachrichten -->
<Card class="break-inside-avoid mb-12">
<!-- <Card class="break-inside-avoid mb-12">
<CardHeader>
<CardTitle>Benachrichtigungen</CardTitle>
<CardDescription>Card Description</CardDescription>
@@ -104,11 +110,11 @@ onMounted(async () => {
<CardFooter>
<Button>Alles leeren</Button>
</CardFooter>
</Card>
</Card> -->
<!-- Aufgaben -->
<Card class="break-inside-avoid mb-8">
<Card class="break-inside-avoid mb-8 ">
<CardHeader class="flex justify-between flex-wrap">
<div>
<CardTitle class="mb-1.5">Aufgaben</CardTitle>
@@ -120,7 +126,7 @@ onMounted(async () => {
<Label for="show-completed" class="text-muted-foreground">Erledigte</Label>
</div>
</CardHeader>
<CardContent>
<CardContent class="max-h-[600px] overflow-y-auto">
<Todos :modelValue="todos" :show-completed="showCompleted" :show-todoable="true" />
</CardContent>
</Card>
@@ -132,11 +138,21 @@ onMounted(async () => {
<CardDescription>Card Description</CardDescription>
</CardHeader>
<CardContent>
<Table class="text-sm">
<TableRow v-for="invoice in props.dueInvoices" @mouseover="router.prefetch(invoices())" @click="router.visit(invoices())">
<TableCell class="align-top">
<span class="whitespace-nowrap" v-if="invoice.dueDate">{{ toLocalDate(invoice.dueDate)
}}</span>
</TableCell>
<TableCell>
<span class="font-semibold">{{ invoice.nr }}</span> {{ invoice.title }}<br>
<span class="text-muted-foreground">{{ invoice.billingData?.companyName }}</span>
</TableCell>
<TableCell class="align-top text-right">{{ toRoundedCurrency(invoice.totalAmount) }}
</TableCell>
</TableRow>
</Table>
</CardContent>
<CardFooter>
<Link :href="invoices()" prefetch><Button>Zu den Rechnungen</Button></Link>
</CardFooter>
</Card>
+3 -3
View File
@@ -3,7 +3,6 @@
import { computed, ref, onMounted } from 'vue'
import { type Invoice } from '@/types'
import { newInvoice } from '@/types/index.d'
import axios from 'axios'
import AppLayout from '@/layouts/AppLayout.vue'
import AppHeader from '@/components/AppHeader.vue'
import { Button } from '@/components/ui/crm-button'
@@ -18,6 +17,7 @@ import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { statusBadgeLabels } from '@/components/ui/status-badge'
import InvoiceDialog from '@/components/documents/InvoiceDialog.vue'
import { hotkey, getPlatformModifierSymbol } from '@/lib/utils'
import InvoiceService from '@/services/InvoiceService'
// Initial invoice data from inertia (see InvoiceController::show)
interface Props {
@@ -34,8 +34,8 @@ const searchField = ref()
onMounted(async () => {
// Load older invoices after initial page load
try {
const invoiceBeforeThisYearResponse = await axios.get('/api/invoices/summaryBeforeThisYear')
invoicesData.value = invoicesData.value.concat(invoiceBeforeThisYearResponse.data as Invoice[])
const invoiceBeforeThisYearResponse = await InvoiceService.getSummaryBeforeThisYear()
invoicesData.value = invoicesData.value.concat(invoiceBeforeThisYearResponse)
invoicesData.value = invoicesData.value.sort(
(a, b) => new Date(a.invoiceDate).getTime() - new Date(b.invoiceDate).getTime()
)
+129
View File
@@ -0,0 +1,129 @@
import axios, { AxiosResponse } from 'axios';
import { Invoice, SalesStatistics as SalesStatistics, LineItem } from '@/types';
const API_URL = '/api/invoices';
const LINE_ITEM_API_URL = '/api/lineitems';
export default {
/**
* Retrieves all invoices
* @returns Promise<Invoice[]>
*/
async getAllInvoices(): Promise<Invoice[]> {
const response = await axios.get<Invoice[]>(API_URL);
return response.data;
},
/**
* Retrieves invoice summary for all time
* @returns Promise<Invoice[]>
*/
async getSummaryAll(): Promise<Invoice[]> {
const response = await axios.get<Invoice[]>(`${API_URL}/summary`);
return response.data;
},
/**
* Retrieves invoice summary for the current year
* @returns Promise<Invoice[]>
*/
async getSummaryThisYear(): Promise<Invoice[]> {
const response = await axios.get<Invoice[]>(`${API_URL}/summaryThisYear`);
return response.data;
},
/**
* Retrieves invoice summary for before the current year
* @returns Promise<Invoice[]>
*/
async getSummaryBeforeThisYear(): Promise<Invoice[]> {
const response = await axios.get<Invoice[]>(`${API_URL}/summaryBeforeThisYear`);
return response.data;
},
/**
* Retrieves sales statistics
* @returns Promise<InvoiceStatistics>
*/
async getSalesStatistics(): Promise<SalesStatistics> {
const response = await axios.get<SalesStatistics>(`${API_URL}/salesStatistics`);
return response.data;
},
/**
* Creates a new invoice
* @param invoice - The invoice data to create
* @returns Promise<Invoice>
*/
async createInvoice(invoice: Partial<Invoice>): Promise<Invoice> {
const response = await axios.post<Invoice>(API_URL, invoice);
return response.data;
},
/**
* Updates an existing invoice
* @param invoice - The invoice object to update
* @returns Promise<Invoice>
*/
async updateInvoice(invoice: Partial<Invoice>): Promise<Invoice> {
const response = await axios.put<Invoice>(`${API_URL}/${invoice.id}`, invoice);
return response.data;
},
/**
* Deletes an invoice by ID
* @param invoiceId - The id of the invoice to delete
* @returns Promise<AxiosResponse>
*/
async deleteInvoice(invoiceId: number): Promise<AxiosResponse> {
const response = await axios.delete(`${API_URL}/${invoiceId}`);
return response.data;
},
/**
* Retrieves line items for a specific invoice
* @param invoiceId - The ID of the invoice
* @returns Promise<LineItem[]>
*/
async getLineItems(invoiceId: number): Promise<LineItem[]> {
const response = await axios.get<LineItem[]>(`${LINE_ITEM_API_URL}/${invoiceId}`);
return response.data;
},
/**
* Sends a reminder for an invoice
* @param invoiceId - The ID of the invoice to remind
* @param to - Email address to send the reminder to
* @param cc - Optional email address to CC the reminder to
* @returns Promise<AxiosResponse>
*/
async remindInvoice(invoiceId: number, to: string, cc?: string): Promise<AxiosResponse> {
const params = { to, cc };
const response = await axios.get(`${API_URL}/${invoiceId}/remind`, { params });
return response.data;
},
/**
* Exports an invoice as PDF
* @param invoiceId - The ID of the invoice to export
* @returns Promise<Blob>
*/
async exportPdf(invoiceId: number): Promise<Blob> {
const response = await axios.get(`${API_URL}/${invoiceId}/pdf`, {
responseType: 'blob'
});
return response.data;
},
/**
* Exports an invoice as XML
* @param invoiceId - The ID of the invoice to export
* @returns Promise<Blob>
*/
async exportXml(invoiceId: number): Promise<Blob> {
const response = await axios.get(`${API_URL}/${invoiceId}/xml`, {
responseType: 'blob'
});
return response.data;
}
};
+15
View File
@@ -277,6 +277,21 @@ export function newInvoice(): Invoice {
}
}
export interface SalesStatistics {
year: number;
totalRevenue: number;
paid: number;
paidPercent: number;
draft: number;
draftPercent: number;
issued: number;
issuedPercent: number;
due: number;
duePercent: number;
reminded: number;
remindedPercent: number;
}
export function newBillingData() {
return {
companyName: "",
+2 -1
View File
@@ -19,7 +19,8 @@
Route::redirect('/', '/dashboard');
Route::get('dashboard', function () {
return Inertia::render('Dashboard');
return Inertia::render('Dashboard',
['dueInvoices' => InvoiceController::getDueInvoices()]);
})->name('dashboard');
// CRM