Hauptmenue: Gutschriften/Lieferscheine-Gesamtuebersicht (portal-scoped)
Neuer Menuepunkt 'Gutschriften' -> Seite /credit-notes mit Tabelle aller Belege (Beleg-Nr, Art, Kunde, Vertrag, Betrag, Datum, PDF), Suche + Pagination. Neuer Endpoint GET /credit-notes (NICHT staff-only wie die uebrigen Credit-Note-Endpoints): Staff sieht alle Belege aller Kunden, Portal- Kunden nur eigene + vertretene (Vollmacht via hasAuthorization). customerIds kommt aus dem JWT, nicht aus Query/Body -> nicht manipulierbar. Fuer Portal wird receiptPath aus der Response entfernt (Belege bleiben staff-only). Route requirePermission contracts:read. Verifiziert: Staff -> alle Belege; Portal-scoped -> nur eigene, korrekt zugeordnet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import * as creditNoteService from '../services/creditNote.service.js';
|
|||||||
import { effectiveNumber } from '../services/creditNote.service.js';
|
import { effectiveNumber } from '../services/creditNote.service.js';
|
||||||
import * as numberRangeService from '../services/creditNoteNumberRange.service.js';
|
import * as numberRangeService from '../services/creditNoteNumberRange.service.js';
|
||||||
import * as deliveryRangeService from '../services/deliveryNoteNumberRange.service.js';
|
import * as deliveryRangeService from '../services/deliveryNoteNumberRange.service.js';
|
||||||
|
import * as authorizationService from '../services/authorization.service.js';
|
||||||
import { generateCreditNotePdf } from '../services/creditNotePdf.service.js';
|
import { generateCreditNotePdf } from '../services/creditNotePdf.service.js';
|
||||||
|
|
||||||
// Gutschriften sind ein reiner Mitarbeiter-/Admin-Bereich (interne
|
// Gutschriften sind ein reiner Mitarbeiter-/Admin-Bereich (interne
|
||||||
@@ -42,6 +43,43 @@ function handleError(res: Response, error: unknown, fallback: string) {
|
|||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Gesamtübersicht (Hauptmenü) ----
|
||||||
|
// Anders als die übrigen Credit-Note-Endpunkte NICHT staff-only: Portal-Kunden
|
||||||
|
// dürfen ihre eigenen (+ vertretene) Belege sehen. Scoping über customerIds
|
||||||
|
// aus dem JWT – eine im Body/Query mitgeschickte customerId hat keinen Effekt.
|
||||||
|
export async function listAll(req: AuthRequest, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Portal-User: nur eigene + vertretene Kunden MIT Vollmacht.
|
||||||
|
let customerIds: number[] | undefined;
|
||||||
|
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||||
|
customerIds = [req.user.customerId];
|
||||||
|
const representedIds: number[] = (req.user as any).representedCustomerIds || [];
|
||||||
|
for (const repCustId of representedIds) {
|
||||||
|
if (await authorizationService.hasAuthorization(repCustId, req.user.customerId)) {
|
||||||
|
customerIds.push(repCustId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = parseInt((req.query.page as string) || '1') || 1;
|
||||||
|
const limit = Math.min(parseInt((req.query.limit as string) || '50') || 50, 200);
|
||||||
|
const search = typeof req.query.search === 'string' ? req.query.search : undefined;
|
||||||
|
|
||||||
|
const result = await creditNoteService.getAllCreditNotes({ customerIds, page, limit, search });
|
||||||
|
|
||||||
|
// Portal-Kunden dürfen keine Überweisungsbelege laden → receiptPath aus der
|
||||||
|
// Response entfernen (Beleg-Download bleibt ohnehin staff-only).
|
||||||
|
const isPortal = !!req.user?.isCustomerPortal;
|
||||||
|
const items = isPortal
|
||||||
|
? result.items.map((cn) => ({ ...cn, receiptPath: null }))
|
||||||
|
: result.items;
|
||||||
|
|
||||||
|
res.json({ success: true, data: { items, pagination: result.pagination } } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(res, error, 'Fehler beim Laden der Belegübersicht');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Gutschriften pro Vertrag ----
|
// ---- Gutschriften pro Vertrag ----
|
||||||
|
|
||||||
export async function listByContract(req: AuthRequest, res: Response): Promise<void> {
|
export async function listByContract(req: AuthRequest, res: Response): Promise<void> {
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { authenticate, requirePermission } from '../middleware/auth.js';
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Gesamtübersicht aller Belege (Hauptmenü) – portal-scoped (nicht staff-only).
|
||||||
|
// VOR /:id, damit die Wurzel nicht als ID interpretiert wird.
|
||||||
|
router.get('/', authenticate, requirePermission('contracts:read'), creditNoteController.listAll);
|
||||||
|
|
||||||
// Nummernkreis-Verwaltung (Einstellungen). VOR /:id, damit "number-range"
|
// Nummernkreis-Verwaltung (Einstellungen). VOR /:id, damit "number-range"
|
||||||
// nicht als ID interpretiert wird.
|
// nicht als ID interpretiert wird.
|
||||||
router.get('/number-range', authenticate, requirePermission('settings:read'), creditNoteController.getNumberRange);
|
router.get('/number-range', authenticate, requirePermission('settings:read'), creditNoteController.getNumberRange);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ApiError } from '../utils/apiError.js';
|
|||||||
import { assignNextNumber } from './creditNoteNumberRange.service.js';
|
import { assignNextNumber } from './creditNoteNumberRange.service.js';
|
||||||
import { assignNextNumber as assignNextDeliveryNoteNumber } from './deliveryNoteNumberRange.service.js';
|
import { assignNextNumber as assignNextDeliveryNoteNumber } from './deliveryNoteNumberRange.service.js';
|
||||||
import { deleteUploadByRelativePath } from '../utils/fileCleanup.js';
|
import { deleteUploadByRelativePath } from '../utils/fileCleanup.js';
|
||||||
import { CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '@prisma/client';
|
import { Prisma, CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '@prisma/client';
|
||||||
|
|
||||||
const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
|
const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
|
||||||
|
|
||||||
@@ -180,6 +180,59 @@ export async function getCreditNotesByContract(contractId: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gesamtübersicht aller Belege (Gutschriften + Lieferscheine). `customerIds`
|
||||||
|
// scoped die Liste (Portal: eigene + vertretene Kunden); ohne = alle (Staff).
|
||||||
|
export async function getAllCreditNotes(opts: {
|
||||||
|
customerIds?: number[];
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
}) {
|
||||||
|
const { customerIds, page = 1, limit = 50, search } = opts;
|
||||||
|
const skip = (Math.max(page, 1) - 1) * limit;
|
||||||
|
|
||||||
|
const where: Prisma.CreditNoteWhereInput = {};
|
||||||
|
if (customerIds) {
|
||||||
|
where.contract = { customerId: { in: customerIds } };
|
||||||
|
}
|
||||||
|
if (search && search.trim()) {
|
||||||
|
const s = search.trim();
|
||||||
|
where.OR = [
|
||||||
|
{ number: { contains: s } },
|
||||||
|
{ deliveryNoteNumber: { contains: s } },
|
||||||
|
{ sachwertDescription: { contains: s } },
|
||||||
|
{ contract: { contractNumber: { contains: s } } },
|
||||||
|
{ contract: { customer: { customerNumber: { contains: s } } } },
|
||||||
|
{ contract: { customer: { lastName: { contains: s } } } },
|
||||||
|
{ contract: { customer: { companyName: { contains: s } } } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [items, total] = await Promise.all([
|
||||||
|
prisma.creditNote.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
contract: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
|
type: true,
|
||||||
|
customer: {
|
||||||
|
select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
}),
|
||||||
|
prisma.creditNote.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { items, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } };
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCreditNoteById(id: number) {
|
export async function getCreditNoteById(id: number) {
|
||||||
return prisma.creditNote.findUnique({ where: { id } });
|
return prisma.creditNote.findUnique({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **📋 Hauptmenü: Gutschriften/Lieferscheine-Gesamtübersicht** (2026-08-12)
|
||||||
|
- Neuer Menüpunkt „Gutschriften" (Sidebar, `show: contracts:read`) → Seite
|
||||||
|
`/credit-notes` mit Tabelle aller Belege (Beleg-Nr., Art, Kunde, Vertrag,
|
||||||
|
Betrag, Datum, PDF), Suche + Pagination.
|
||||||
|
- **Scoping:** neuer Endpoint `GET /credit-notes` (NICHT staff-only wie die
|
||||||
|
übrigen Credit-Note-Endpoints): Staff sieht **alle** Belege aller Kunden,
|
||||||
|
Portal-Kunden **nur eigene + vertretene** (Vollmacht via `hasAuthorization`).
|
||||||
|
`customerIds` kommt aus dem JWT (nicht aus Query/Body → nicht manipulierbar).
|
||||||
|
Für Portal wird `receiptPath` aus der Response entfernt (Belege bleiben
|
||||||
|
staff-only). Verifiziert (Staff alle, Portal nur eigene).
|
||||||
- [x] **🚫 Gutschrift nur mit Empfängeradresse (Rechnung > Liefer)** (2026-08-12)
|
- [x] **🚫 Gutschrift nur mit Empfängeradresse (Rechnung > Liefer)** (2026-08-12)
|
||||||
- Beim Klick auf „Gutschrift anlegen" wird geprüft, ob der Vertrag eine
|
- Beim Klick auf „Gutschrift anlegen" wird geprüft, ob der Vertrag eine
|
||||||
Empfängeradresse hat: **Rechnungsadresse hat Vorrang, sonst Lieferadresse**.
|
Empfängeradresse hat: **Rechnungsadresse hat Vorrang, sonst Lieferadresse**.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import CustomerList from './pages/customers/CustomerList';
|
|||||||
import CustomerDetail from './pages/customers/CustomerDetail';
|
import CustomerDetail from './pages/customers/CustomerDetail';
|
||||||
import CustomerForm from './pages/customers/CustomerForm';
|
import CustomerForm from './pages/customers/CustomerForm';
|
||||||
import ContractList from './pages/contracts/ContractList';
|
import ContractList from './pages/contracts/ContractList';
|
||||||
|
import CreditNotesOverview from './pages/CreditNotesOverview';
|
||||||
import ContractDetail from './pages/contracts/ContractDetail';
|
import ContractDetail from './pages/contracts/ContractDetail';
|
||||||
import ContractForm from './pages/contracts/ContractForm';
|
import ContractForm from './pages/contracts/ContractForm';
|
||||||
import ContractCockpit from './pages/contracts/ContractCockpit';
|
import ContractCockpit from './pages/contracts/ContractCockpit';
|
||||||
@@ -200,6 +201,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Contracts */}
|
{/* Contracts */}
|
||||||
<Route path="contracts" element={<PortalConsentGate><ContractList /></PortalConsentGate>} />
|
<Route path="contracts" element={<PortalConsentGate><ContractList /></PortalConsentGate>} />
|
||||||
|
<Route path="credit-notes" element={<PortalConsentGate><CreditNotesOverview /></PortalConsentGate>} />
|
||||||
<Route path="contracts/cockpit" element={<ContractCockpit />} />
|
<Route path="contracts/cockpit" element={<ContractCockpit />} />
|
||||||
<Route path="contracts/new" element={<ContractForm />} />
|
<Route path="contracts/new" element={<ContractForm />} />
|
||||||
<Route path="contracts/:id" element={<PortalConsentGate><ContractDetail /></PortalConsentGate>} />
|
<Route path="contracts/:id" element={<PortalConsentGate><ContractDetail /></PortalConsentGate>} />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
FileCheck,
|
FileCheck,
|
||||||
UserCircle,
|
UserCircle,
|
||||||
|
Receipt,
|
||||||
Gauge,
|
Gauge,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ export default function Sidebar() {
|
|||||||
{ to: '/contracts', icon: FileText, label: 'Verträge', show: hasPermission('contracts:read'), end: true },
|
{ to: '/contracts', icon: FileText, label: 'Verträge', show: hasPermission('contracts:read'), end: true },
|
||||||
{ to: '/contracts/cockpit', icon: AlertCircle, label: 'Vertrags-Cockpit', show: hasPermission('contracts:read') && !isCustomer },
|
{ to: '/contracts/cockpit', icon: AlertCircle, label: 'Vertrags-Cockpit', show: hasPermission('contracts:read') && !isCustomer },
|
||||||
{ to: '/tasks', icon: isCustomer ? MessageSquare : ClipboardList, label: isCustomer ? 'Support-Anfragen' : 'Aufgaben', show: hasPermission('contracts:read') },
|
{ to: '/tasks', icon: isCustomer ? MessageSquare : ClipboardList, label: isCustomer ? 'Support-Anfragen' : 'Aufgaben', show: hasPermission('contracts:read') },
|
||||||
|
{ to: '/credit-notes', icon: Receipt, label: 'Gutschriften', show: hasPermission('contracts:read'), end: true },
|
||||||
{ to: '/my-meters', icon: Gauge, label: 'Zählerstände', show: isCustomerPortal },
|
{ to: '/my-meters', icon: Gauge, label: 'Zählerstände', show: isCustomerPortal },
|
||||||
{ to: '/privacy', icon: Shield, label: 'Datenschutz', show: isCustomerPortal },
|
{ to: '/privacy', icon: Shield, label: 'Datenschutz', show: isCustomerPortal },
|
||||||
{ to: '/authorizations', icon: FileCheck, label: 'Vollmachten', show: isCustomerPortal && hasAuthorizations },
|
{ to: '/authorizations', icon: FileCheck, label: 'Vollmachten', show: isCustomerPortal && hasAuthorizations },
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Receipt, Search, FileText, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { creditNoteApi } from '../services/api';
|
||||||
|
import { fileUrl } from '../utils/fileUrl';
|
||||||
|
import { formatDate } from '../utils/dateFormat';
|
||||||
|
import Badge from '../components/ui/Badge';
|
||||||
|
import Input from '../components/ui/Input';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import type { CreditNoteListItem } from '../types';
|
||||||
|
|
||||||
|
function euro(n: number, currency = 'EUR') {
|
||||||
|
return new Intl.NumberFormat('de-DE', { style: 'currency', currency }).format(n || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLieferschein(cn: CreditNoteListItem) {
|
||||||
|
return cn.type === 'SACHWERT' && cn.amountGross === 0;
|
||||||
|
}
|
||||||
|
function belegNummer(cn: CreditNoteListItem) {
|
||||||
|
return (isLieferschein(cn) ? cn.deliveryNoteNumber : cn.number) ?? `#${cn.id}`;
|
||||||
|
}
|
||||||
|
function customerName(c: CreditNoteListItem['contract']['customer']) {
|
||||||
|
return c.companyName || `${c.firstName} ${c.lastName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreditNotesOverview() {
|
||||||
|
const { hasPermission, isCustomer } = useAuth();
|
||||||
|
const canSeeCustomers = hasPermission('customers:read') && !isCustomer;
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['credit-notes-overview', search, page],
|
||||||
|
queryFn: () => creditNoteApi.listAll({ search: search || undefined, page, limit: 50 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = data?.data?.items ?? [];
|
||||||
|
const pagination = data?.data?.pagination;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<Receipt className="w-6 h-6 text-blue-600" />
|
||||||
|
Gutschriften & Lieferscheine
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow mb-4 p-4">
|
||||||
|
<div className="relative max-w-md">
|
||||||
|
<Search className="w-4 h-4 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||||
|
placeholder="Suche: Nummer, Kunde, Vertrag, Sachwert …"
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">Laden …</div>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">Keine Belege gefunden.</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-gray-50 text-gray-500 text-left">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2 font-medium">Beleg-Nr.</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Art</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Kunde</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Vertrag</th>
|
||||||
|
<th className="px-4 py-2 font-medium text-right">Betrag</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Datum</th>
|
||||||
|
<th className="px-4 py-2 font-medium">PDF</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{items.map((cn) => {
|
||||||
|
const ls = isLieferschein(cn);
|
||||||
|
return (
|
||||||
|
<tr key={cn.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-4 py-2 font-mono">{belegNummer(cn)}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<Badge variant={ls ? 'default' : 'info'}>{ls ? 'Lieferschein' : 'Gutschrift'}</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
{canSeeCustomers ? (
|
||||||
|
<Link to={`/customers/${cn.contract.customer.id}`} className="text-blue-600 hover:underline">
|
||||||
|
{customerName(cn.contract.customer)}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
customerName(cn.contract.customer)
|
||||||
|
)}
|
||||||
|
<span className="text-gray-400 ml-1 font-mono text-xs">{cn.contract.customer.customerNumber}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<Link to={`/contracts/${cn.contract.id}`} className="text-blue-600 hover:underline font-mono">
|
||||||
|
{cn.contract.contractNumber}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-right">
|
||||||
|
{ls ? <span className="text-gray-500 italic">ohne Betrag</span> : <strong>{euro(cn.amountGross, cn.currency)}</strong>}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 whitespace-nowrap">{formatDate(cn.creditDate)}</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
{cn.pdfPath ? (
|
||||||
|
<a href={fileUrl(cn.pdfPath, { inline: true })} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800 inline-flex items-center gap-1">
|
||||||
|
<FileText className="w-4 h-4" /> PDF
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 text-xs">–</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pagination && pagination.totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between mt-4 text-sm text-gray-600">
|
||||||
|
<span>{pagination.total} Belege · Seite {pagination.page} / {pagination.totalPages}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
disabled={page <= 1}
|
||||||
|
onClick={() => setPage((p) => Math.max(p - 1, 1))}
|
||||||
|
className="inline-flex items-center gap-1 px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" /> Zurück
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled={page >= pagination.totalPages}
|
||||||
|
onClick={() => setPage((p) => p + 1)}
|
||||||
|
className="inline-flex items-center gap-1 px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Weiter <ChevronRight className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { ApiResponse, Customer, Contract, ContractTask, ContractTaskSubtask, ContractTaskStatus, SalesPlatform, CancellationPeriod, ContractDuration, ContractCategory, Provider, Tariff, User, Address, BankCard, IdentityDocument, Meter, MeterReading, Invoice, Role, PortalSettings, CustomerRepresentative, CustomerSummary, CustomerReferrals, CreditNote, CreditNoteDefaults, CreditNoteNumberRange, CompanyProfile, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types';
|
import type { ApiResponse, Customer, Contract, ContractTask, ContractTaskSubtask, ContractTaskStatus, SalesPlatform, CancellationPeriod, ContractDuration, ContractCategory, Provider, Tariff, User, Address, BankCard, IdentityDocument, Meter, MeterReading, Invoice, Role, PortalSettings, CustomerRepresentative, CustomerSummary, CustomerReferrals, CreditNote, CreditNoteListItem, CreditNoteDefaults, CreditNoteNumberRange, CompanyProfile, ContractHistoryEntry, AuditLog, AuditSensitivity, AuditRetentionPolicy, CustomerConsent, ConsentType, ConsentStatus, DataDeletionRequest, DeletionRequestStatus, GDPRDashboardStats, RepresentativeAuthorization } from '../types';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// In-Memory-Token-Store
|
// In-Memory-Token-Store
|
||||||
@@ -277,6 +277,10 @@ export const companyProfileApi = {
|
|||||||
|
|
||||||
// Gutschriften (Subventionen am Vertrag)
|
// Gutschriften (Subventionen am Vertrag)
|
||||||
export const creditNoteApi = {
|
export const creditNoteApi = {
|
||||||
|
listAll: async (params?: { page?: number; limit?: number; search?: string }) => {
|
||||||
|
const res = await api.get<ApiResponse<{ items: CreditNoteListItem[]; pagination: { page: number; limit: number; total: number; totalPages: number } }>>('/credit-notes', { params });
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
listByContract: async (contractId: number) => {
|
listByContract: async (contractId: number) => {
|
||||||
const res = await api.get<ApiResponse<CreditNote[]>>(`/contracts/${contractId}/credit-notes`);
|
const res = await api.get<ApiResponse<CreditNote[]>>(`/contracts/${contractId}/credit-notes`);
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
@@ -99,6 +99,21 @@ export interface CreditNotePayoutBankCard {
|
|||||||
description?: string | null;
|
description?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreditNoteListItem extends CreditNote {
|
||||||
|
contract: {
|
||||||
|
id: number;
|
||||||
|
contractNumber: string;
|
||||||
|
type: string;
|
||||||
|
customer: {
|
||||||
|
id: number;
|
||||||
|
customerNumber: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
companyName?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreditNoteDefaults {
|
export interface CreditNoteDefaults {
|
||||||
customerType: CreditNoteCustomerType;
|
customerType: CreditNoteCustomerType;
|
||||||
vatRelevant: boolean;
|
vatRelevant: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user