added invoices and status in cockpit, created info button for contract status types

This commit is contained in:
2026-02-08 01:18:12 +01:00
parent 1ad4fe0819
commit aee48a8ccb
45 changed files with 4543 additions and 863 deletions
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi } from '../../services/api';
import { ContractEmailsSection } from '../../components/email';
import { ContractDetailModal } from '../../components/contracts';
import InvoicesSection from '../../components/contracts/InvoicesSection';
import { useAuth } from '../../context/AuthContext';
import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
@@ -11,7 +12,7 @@ import Badge from '../../components/ui/Badge';
import Input from '../../components/ui/Input';
import Modal from '../../components/ui/Modal';
import FileUpload from '../../components/ui/FileUpload';
import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator } from 'lucide-react';
import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X } from 'lucide-react';
import { calculateConsumption, calculateCosts } from '../../utils/energyCalculations';
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask } from '../../types';
@@ -45,6 +46,42 @@ const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' |
DEACTIVATED: 'default',
};
// Status-Erklärungen für Info-Modal
const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
];
function StatusInfoModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/20" onClick={onClose} />
<div className="relative bg-white rounded-lg shadow-xl p-4 max-w-sm w-full mx-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900">Vertragsstatus-Übersicht</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-2">
{statusDescriptions.map(({ status, label, description, color }) => (
<div key={status} className="flex items-start gap-2">
<span className={`font-medium text-sm min-w-[90px] ${color}`}>{label}</span>
<span className="text-sm text-gray-600">{description}</span>
</div>
))}
</div>
</div>
</div>
);
}
// Prüft ob die Laufzeit als "unbefristet" gilt (≤ 4 Wochen / 1 Monat / 30 Tage)
function isUnlimitedDuration(durationCode: string): boolean {
const match = durationCode.match(/^(\d+)([TMWJ])$/);
@@ -1203,6 +1240,9 @@ export default function ContractDetail() {
// Bestätigungsdialog für Folgevertrag
const [showFollowUpConfirm, setShowFollowUpConfirm] = useState(false);
// Status-Info Modal
const [showStatusInfo, setShowStatusInfo] = useState(false);
const { data, isLoading } = useQuery({
queryKey: ['contract', id],
queryFn: () => contractApi.getById(contractId),
@@ -1399,6 +1439,13 @@ export default function ContractDetail() {
<h1 className="text-2xl font-bold">{c.contractNumber}</h1>
<Badge>{typeLabels[c.type]}</Badge>
<Badge variant={statusVariants[c.status]}>{statusLabels[c.status]}</Badge>
<button
onClick={() => setShowStatusInfo(true)}
className="text-gray-400 hover:text-blue-600 transition-colors"
title="Status-Erklärung"
>
<Info className="w-4 h-4" />
</button>
</div>
{c.customer && (
<p className="text-gray-500 ml-10">
@@ -2151,6 +2198,14 @@ export default function ContractDetail() {
bonus={c.energyDetails.bonus}
/>
)}
{/* Rechnungen */}
<InvoicesSection
ecdId={c.energyDetails.id}
invoices={c.energyDetails.invoices || []}
contractId={contractId}
canEdit={hasPermission('contracts:update') && !isCustomer}
/>
</Card>
)}
@@ -2589,6 +2644,9 @@ export default function ContractDetail() {
</div>
</div>
</Modal>
{/* Status-Info Modal */}
<StatusInfoModal isOpen={showStatusInfo} onClose={() => setShowStatusInfo(false)} />
</div>
);
}
+60 -6
View File
@@ -8,7 +8,7 @@ import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import Select from '../../components/ui/Select';
import type { ContractType } from '../../types';
import { Plus, Trash2, Eye, EyeOff } from 'lucide-react';
import { Plus, Trash2, Eye, EyeOff, Info, X } from 'lucide-react';
// Contract types are now loaded dynamically from the database
@@ -21,6 +21,42 @@ const statusOptions = [
{ value: 'DEACTIVATED', label: 'Deaktiviert' },
];
// Status-Erklärungen für Info-Modal
const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
];
function StatusInfoModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/20" onClick={onClose} />
<div className="relative bg-white rounded-lg shadow-xl p-4 max-w-sm w-full mx-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900">Vertragsstatus-Übersicht</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-2">
{statusDescriptions.map(({ status, label, description, color }) => (
<div key={status} className="flex items-start gap-2">
<span className={`font-medium text-sm min-w-[90px] ${color}`}>{label}</span>
<span className="text-sm text-gray-600">{description}</span>
</div>
))}
</div>
</div>
</div>
);
}
export default function ContractForm() {
const { id } = useParams();
const [searchParams] = useSearchParams();
@@ -143,6 +179,9 @@ export default function ContractForm() {
const [showSimPins, setShowSimPins] = useState<Record<number, boolean>>({});
const [showSimPuks, setShowSimPuks] = useState<Record<number, boolean>>({});
// Status-Info Modal
const [showStatusInfo, setShowStatusInfo] = useState(false);
// For new contracts, mark as "loaded" immediately so provider change detection works
useEffect(() => {
if (!isEdit) {
@@ -635,11 +674,23 @@ export default function ContractForm() {
options={typeOptions}
/>
<Select
label="Status"
{...register('status')}
options={statusOptions}
/>
<div>
<div className="flex items-center gap-1 mb-1">
<label className="block text-sm font-medium text-gray-700">Status</label>
<button
type="button"
onClick={() => setShowStatusInfo(true)}
className="text-gray-400 hover:text-blue-600 transition-colors"
title="Status-Erklärung"
>
<Info className="w-4 h-4" />
</button>
</div>
<Select
{...register('status')}
options={statusOptions}
/>
</div>
<Select
label="Vertriebsplattform"
@@ -1322,6 +1373,9 @@ export default function ContractForm() {
</Button>
</div>
</form>
{/* Status-Info Modal */}
<StatusInfoModal isOpen={showStatusInfo} onClose={() => setShowStatusInfo(false)} />
</div>
);
}
+55 -2
View File
@@ -9,7 +9,7 @@ import Input from '../../components/ui/Input';
import Select from '../../components/ui/Select';
import Badge from '../../components/ui/Badge';
import CopyButton from '../../components/ui/CopyButton';
import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight } from 'lucide-react';
import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight, Info, X } from 'lucide-react';
import type { Contract, ContractType, ContractStatus } from '../../types';
const typeLabels: Record<ContractType, string> = {
@@ -41,6 +41,42 @@ const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' |
DEACTIVATED: 'default',
};
// Status-Erklärungen für Info-Modal
const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
];
function StatusInfoModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/20" onClick={onClose} />
<div className="relative bg-white rounded-lg shadow-xl p-4 max-w-sm w-full mx-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900">Vertragsstatus-Übersicht</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-2">
{statusDescriptions.map(({ status, label, description, color }) => (
<div key={status} className="flex items-start gap-2">
<span className={`font-medium text-sm min-w-[90px] ${color}`}>{label}</span>
<span className="text-sm text-gray-600">{description}</span>
</div>
))}
</div>
</div>
</div>
);
}
export default function ContractList() {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
@@ -54,6 +90,9 @@ export default function ContractList() {
// State für aufgeklappte Verträge (Baumstruktur)
const [expandedContracts, setExpandedContracts] = useState<Set<number>>(new Set());
// Status-Info Modal
const [showStatusInfo, setShowStatusInfo] = useState(false);
const { hasPermission, isCustomer, isCustomerPortal, user } = useAuth();
const queryClient = useQueryClient();
@@ -358,7 +397,18 @@ export default function ContractList() {
)}
<th className="text-left py-3 px-4 font-medium text-gray-600">Typ</th>
<th className="text-left py-3 px-4 font-medium text-gray-600">Anbieter / Tarif</th>
<th className="text-left py-3 px-4 font-medium text-gray-600">Status</th>
<th className="text-left py-3 px-4 font-medium text-gray-600">
<span className="flex items-center gap-1">
Status
<button
onClick={() => setShowStatusInfo(true)}
className="text-gray-400 hover:text-blue-600 transition-colors"
title="Status-Erklärung"
>
<Info className="w-4 h-4" />
</button>
</span>
</th>
<th className="text-left py-3 px-4 font-medium text-gray-600">Beginn</th>
<th className="text-right py-3 px-4 font-medium text-gray-600">Aktionen</th>
</tr>
@@ -472,6 +522,9 @@ export default function ContractList() {
<div className="text-center py-8 text-gray-500">Keine Verträge gefunden.</div>
</Card>
)}
{/* Status-Info Modal */}
<StatusInfoModal isOpen={showStatusInfo} onClose={() => setShowStatusInfo(false)} />
</div>
);
}
@@ -12,7 +12,7 @@ import Modal from '../../components/ui/Modal';
import Input from '../../components/ui/Input';
import Select from '../../components/ui/Select';
import FileUpload from '../../components/ui/FileUpload';
import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight } from 'lucide-react';
import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight, Info } from 'lucide-react';
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary } from '../../types';
@@ -1496,6 +1496,7 @@ function ContractsTab({
const navigate = useNavigate();
const queryClient = useQueryClient();
const [expandedContracts, setExpandedContracts] = useState<Set<number>>(new Set());
const [showStatusInfo, setShowStatusInfo] = useState(false);
// Lade Vertragsbaum statt flacher Liste
const { data: treeData, isLoading } = useQuery({
@@ -1537,6 +1538,15 @@ function ContractsTab({
DEACTIVATED: 'default',
};
const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
];
const toggleExpand = (contractId: number) => {
setExpandedContracts(prev => {
const next = new Set(prev);
@@ -1597,6 +1607,15 @@ function ContractsTab({
</span>
<Badge>{typeLabels[contract.type] || contract.type}</Badge>
<Badge variant={statusVariants[contract.status] || 'default'}>{contract.status}</Badge>
{depth === 0 && !isPredecessor && (
<button
onClick={(e) => { e.stopPropagation(); setShowStatusInfo(true); }}
className="text-gray-400 hover:text-blue-600 transition-colors"
title="Status-Erklärung"
>
<Info className="w-4 h-4" />
</button>
)}
{isPredecessor && (
<span className="text-xs text-gray-500 ml-2">(Vorgänger)</span>
@@ -1693,6 +1712,29 @@ function ContractsTab({
) : (
<p className="text-gray-500">Keine Verträge vorhanden.</p>
)}
{/* Status Info Modal */}
{showStatusInfo && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/20" onClick={() => setShowStatusInfo(false)} />
<div className="relative bg-white rounded-lg shadow-xl p-4 max-w-sm w-full mx-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900">Vertragsstatus-Übersicht</h3>
<button onClick={() => setShowStatusInfo(false)} className="text-gray-400 hover:text-gray-600">
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-2">
{statusDescriptions.map(({ status, label, description, color }) => (
<div key={status} className="flex items-start gap-2">
<span className={`font-medium text-sm min-w-[90px] ${color}`}>{label}</span>
<span className="text-sm text-gray-600">{description}</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
}