From 167bee78f86fb28585442500145bb735cf1ae114 Mon Sep 17 00:00:00 2001 From: Daniel Stock Date: Wed, 27 May 2026 11:12:26 +0200 Subject: [PATCH] Add InvoiceService and initial invoice dashboard widget --- app/Http/Controllers/InvoiceController.php | 13 ++ .../js/components/documents/InvoiceDialog.vue | 2 +- resources/js/pages/Dashboard.vue | 54 +++++--- resources/js/pages/Invoices.vue | 6 +- resources/js/services/InvoiceService.ts | 129 ++++++++++++++++++ resources/js/types/index.d.ts | 15 ++ routes/web.php | 3 +- 7 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 resources/js/services/InvoiceService.ts diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index baddf93..3e9193c 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -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) { diff --git a/resources/js/components/documents/InvoiceDialog.vue b/resources/js/components/documents/InvoiceDialog.vue index 00836ff..6f49b18 100644 --- a/resources/js/components/documents/InvoiceDialog.vue +++ b/resources/js/components/documents/InvoiceDialog.vue @@ -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; diff --git a/resources/js/pages/Dashboard.vue b/resources/js/pages/Dashboard.vue index 0514bd9..9e4d3a3 100644 --- a/resources/js/pages/Dashboard.vue +++ b/resources/js/pages/Dashboard.vue @@ -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(); +const salesStatistics = ref({ 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([]) 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 () => { - + - +
Aufgaben @@ -120,7 +126,7 @@ onMounted(async () => {
- +
@@ -132,11 +138,21 @@ onMounted(async () => { Card Description - + + + + {{ toLocalDate(invoice.dueDate) + }} + + + {{ invoice.nr }} {{ invoice.title }}
+ {{ invoice.billingData?.companyName }} +
+ {{ toRoundedCurrency(invoice.totalAmount) }} + +
+
- - -
diff --git a/resources/js/pages/Invoices.vue b/resources/js/pages/Invoices.vue index 38618b0..6c146a0 100644 --- a/resources/js/pages/Invoices.vue +++ b/resources/js/pages/Invoices.vue @@ -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() ) diff --git a/resources/js/services/InvoiceService.ts b/resources/js/services/InvoiceService.ts new file mode 100644 index 0000000..0d0c95f --- /dev/null +++ b/resources/js/services/InvoiceService.ts @@ -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 + */ + async getAllInvoices(): Promise { + const response = await axios.get(API_URL); + return response.data; + }, + + /** + * Retrieves invoice summary for all time + * @returns Promise + */ + async getSummaryAll(): Promise { + const response = await axios.get(`${API_URL}/summary`); + return response.data; + }, + + /** + * Retrieves invoice summary for the current year + * @returns Promise + */ + async getSummaryThisYear(): Promise { + const response = await axios.get(`${API_URL}/summaryThisYear`); + return response.data; + }, + + /** + * Retrieves invoice summary for before the current year + * @returns Promise + */ + async getSummaryBeforeThisYear(): Promise { + const response = await axios.get(`${API_URL}/summaryBeforeThisYear`); + return response.data; + }, + + /** + * Retrieves sales statistics + * @returns Promise + */ + async getSalesStatistics(): Promise { + const response = await axios.get(`${API_URL}/salesStatistics`); + return response.data; + }, + + /** + * Creates a new invoice + * @param invoice - The invoice data to create + * @returns Promise + */ + async createInvoice(invoice: Partial): Promise { + const response = await axios.post(API_URL, invoice); + return response.data; + }, + + /** + * Updates an existing invoice + * @param invoice - The invoice object to update + * @returns Promise + */ + async updateInvoice(invoice: Partial): Promise { + const response = await axios.put(`${API_URL}/${invoice.id}`, invoice); + return response.data; + }, + + /** + * Deletes an invoice by ID + * @param invoiceId - The id of the invoice to delete + * @returns Promise + */ + async deleteInvoice(invoiceId: number): Promise { + 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 + */ + async getLineItems(invoiceId: number): Promise { + const response = await axios.get(`${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 + */ + async remindInvoice(invoiceId: number, to: string, cc?: string): Promise { + 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 + */ + async exportPdf(invoiceId: number): Promise { + 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 + */ + async exportXml(invoiceId: number): Promise { + const response = await axios.get(`${API_URL}/${invoiceId}/xml`, { + responseType: 'blob' + }); + return response.data; + } +}; \ No newline at end of file diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 6037cf2..f35732e 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -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: "", diff --git a/routes/web.php b/routes/web.php index 6b88aa1..e7ff44f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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