nyavokevin 8f7019e815 feat(invoice): support group-based invoices without a client
Allow invoices to target either a client or a client group by making
`client_id` nullable and validating that exactly one recipient is set.

Load group relations in invoice data so the frontend can display group
details in invoice views and type definitions.

Make quote acceptance reuse an existing invoice instead of creating a
duplicate, and surface backend status update errors in the quote UI.
2026-04-02 16:08:44 +03:00

179 lines
3.7 KiB
TypeScript

import { request } from "./http";
import { Client } from "./client";
import type { ClientGroup } from "./clientGroup";
export interface Invoice {
id: number;
client_id: number | null;
group_id: number | null;
source_quote_id: number | null;
invoice_number: string;
status:
| "brouillon"
| "emise"
| "envoyee"
| "partiellement_payee"
| "payee"
| "echue"
| "annulee"
| "avoir";
invoice_date: string;
due_date: string | null;
currency: string;
total_ht: number;
total_tva: number;
total_ttc: number;
e_invoicing_channel_id: number | null;
e_invoice_status: string | null;
created_at: string;
updated_at: string;
client?: Client;
group?: ClientGroup;
}
export interface InvoiceListResponse {
data: Invoice[];
meta?: {
current_page: number;
last_page: number;
per_page: number;
total: number;
};
}
export interface InvoiceResponse {
data: Invoice;
}
export interface InvoiceLine {
product_id: number | null;
product_name: string;
packaging_id?: number | null;
packages_qty?: number | null;
units_qty?: number | null;
description?: string;
qty_base?: number | null;
unit_price: number;
unit_price_per_package?: number | null;
tva_rate_id?: number | null;
discount_pct?: number;
quantity: number;
tva?: number;
total_ht?: number;
}
export interface CreateInvoicePayload {
client_id: number | null;
group_id?: number | null;
source_quote_id?: number | null;
status:
| "brouillon"
| "emise"
| "envoyee"
| "partiellement_payee"
| "payee"
| "echue"
| "annulee"
| "avoir";
invoice_date: string;
due_date?: string | null;
currency: string;
total_ht: number;
total_tva: number;
total_ttc: number;
lines: InvoiceLine[];
}
export interface UpdateInvoicePayload extends Partial<CreateInvoicePayload> {
id: number;
}
export const InvoiceService = {
/**
* Get all invoices with pagination
*/
async getAllInvoices(params?: {
page?: number;
per_page?: number;
search?: string;
status?: string;
client_id?: number;
}): Promise<InvoiceListResponse> {
const response = await request<InvoiceListResponse>({
url: "/api/invoices",
method: "get",
params,
});
return response;
},
/**
* Get a specific invoice by ID
*/
async getInvoice(id: number): Promise<InvoiceResponse> {
const response = await request<InvoiceResponse>({
url: `/api/invoices/${id}`,
method: "get",
});
return response;
},
/**
* Create a new invoice
*/
async createInvoice(payload: CreateInvoicePayload): Promise<InvoiceResponse> {
const response = await request<InvoiceResponse>({
url: "/api/invoices",
method: "post",
data: payload,
});
return response;
},
/**
* Create an invoice from a quote
*/
async createFromQuote(quoteId: number): Promise<InvoiceResponse> {
const response = await request<InvoiceResponse>({
url: `/api/invoices/from-quote/${quoteId}`,
method: "post",
});
return response;
},
/**
* Update an existing invoice
*/
async updateInvoice(payload: UpdateInvoicePayload): Promise<InvoiceResponse> {
const { id, ...updateData } = payload;
const response = await request<InvoiceResponse>({
url: `/api/invoices/${id}`,
method: "put",
data: updateData,
});
return response;
},
/**
* Delete an invoice
*/
async deleteInvoice(
id: number
): Promise<{ success: boolean; message: string }> {
const response = await request<{ success: boolean; message: string }>({
url: `/api/invoices/${id}`,
method: "delete",
});
return response;
},
};
export default InvoiceService;