fix: resolve all ruff and ESLint lint errors

Backend (ruff):
- F821: Add TYPE_CHECKING imports for Vehicle, Contact, File in models
  (file.py, retouch.py, sale.py, vehicle.py)
- E741: Rename ambiguous variable  to /
  (copilot_service.py, price_compare_service.py, openrouter.py)
- F841: Remove unused variable  in sale_service.py

Frontend (ESLint):
- react/no-unescaped-entities: Escape quotes in ContractPreview.tsx
- @next/next/no-img-element: Replace <img> with <Image> from next/image
  (FileGallery.tsx, FileList.tsx, FilePreview.tsx, BeforeAfterSlider.tsx)

Tests: 392 backend passed, 112 frontend passed, next build successful
This commit is contained in:
2026-07-17 21:16:38 +02:00
parent f1d6de0e47
commit 341d0c6f38
30 changed files with 4078 additions and 48 deletions
+4
View File
@@ -2,6 +2,7 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, Integer, String, func from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
@@ -9,6 +10,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base from app.database import Base
if TYPE_CHECKING:
from app.models.vehicle import Vehicle
class File(Base): class File(Base):
"""File attachment entity linked to a vehicle.""" """File attachment entity linked to a vehicle."""
+4
View File
@@ -3,6 +3,7 @@
import enum import enum
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import DateTime, ForeignKey, String, Text, func from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
@@ -10,6 +11,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base from app.database import Base
if TYPE_CHECKING:
from app.models.vehicle import Vehicle
class RetouchStatus(str, enum.Enum): class RetouchStatus(str, enum.Enum):
pending = "pending" pending = "pending"
+5
View File
@@ -4,6 +4,7 @@ import enum
import uuid import uuid
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import ( from sqlalchemy import (
Boolean, Boolean,
@@ -20,6 +21,10 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base from app.database import Base
if TYPE_CHECKING:
from app.models.contact import Contact
from app.models.vehicle import Vehicle
class SaleStatus(str, enum.Enum): class SaleStatus(str, enum.Enum):
draft = "draft" draft = "draft"
+1 -1
View File
@@ -2,7 +2,7 @@
import enum import enum
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime
from sqlalchemy import Boolean, DateTime, Enum, String, func from sqlalchemy import Boolean, DateTime, Enum, String, func
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
+4 -1
View File
@@ -4,12 +4,12 @@ import enum
import uuid import uuid
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import ( from sqlalchemy import (
CheckConstraint, CheckConstraint,
Date, Date,
DateTime, DateTime,
Enum,
ForeignKey, ForeignKey,
Integer, Integer,
Numeric, Numeric,
@@ -22,6 +22,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base from app.database import Base
if TYPE_CHECKING:
from app.models.file import File
class VehicleCondition(str, enum.Enum): class VehicleCondition(str, enum.Enum):
new = "new" new = "new"
+1 -1
View File
@@ -2,7 +2,7 @@
import uuid import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import Response from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
+2 -3
View File
@@ -2,11 +2,11 @@
import uuid import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.dependencies import get_current_user, get_pagination, require_role from app.dependencies import get_pagination, require_role
from app.models.user import User from app.models.user import User
from app.schemas.user import ( from app.schemas.user import (
UserCreate, UserCreate,
@@ -17,7 +17,6 @@ from app.schemas.user import (
from app.services.auth_service import ( from app.services.auth_service import (
create_user as svc_create_user, create_user as svc_create_user,
deactivate_user as svc_deactivate_user, deactivate_user as svc_deactivate_user,
get_user_by_id,
list_users as svc_list_users, list_users as svc_list_users,
update_user as svc_update_user, update_user as svc_update_user,
) )
-1
View File
@@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db from app.database import get_db
from app.dependencies import get_current_user, get_pagination from app.dependencies import get_current_user, get_pagination
from app.models.user import User from app.models.user import User
from app.models.vehicle import Vehicle
from app.schemas.vehicle import ( from app.schemas.vehicle import (
MobileDePushResponse, MobileDePushResponse,
MobileDeStatusResponse, MobileDeStatusResponse,
+1 -1
View File
@@ -4,7 +4,7 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Literal, Optional from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.utils.ust_validation import validate_vat_id from app.utils.ust_validation import validate_vat_id
+1 -1
View File
@@ -5,7 +5,7 @@ from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import Literal, Optional from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator
VEHICLE_TYPES = Literal["lkw", "pkw", "baumaschine", "stapler", "transporter"] VEHICLE_TYPES = Literal["lkw", "pkw", "baumaschine", "stapler", "transporter"]
+1 -1
View File
@@ -43,7 +43,7 @@ def _parse_ai_response(raw_content: str) -> dict[str, Any]:
# Strip markdown code fences if present # Strip markdown code fences if present
if text.startswith("```"): if text.startswith("```"):
lines = text.split("\n") lines = text.split("\n")
lines = [l for l in lines if not l.strip().startswith("```")] lines = [line for line in lines if not line.strip().startswith("```")]
text = "\n".join(lines).strip() text = "\n".join(lines).strip()
try: try:
+1 -2
View File
@@ -6,7 +6,6 @@ import os
import uuid import uuid
from datetime import date from datetime import date
from decimal import Decimal from decimal import Decimal
from typing import Any
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,7 +13,7 @@ from sqlalchemy.orm import selectinload
from app.models.datev_export import DATEVExport from app.models.datev_export import DATEVExport
from app.models.sale import Sale from app.models.sale import Sale
from app.utils.datev import generate_datev_csv, validate_datev_csv from app.utils.datev import generate_datev_csv
async def create_export( async def create_export(
-1
View File
@@ -8,7 +8,6 @@ Enforces MAX_FILE_SIZE_MB from config (default 50MB, but endpoint enforces 20MB)
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
import uuid import uuid
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
+1 -2
View File
@@ -8,10 +8,9 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any
import httpx import httpx
from sqlalchemy import and_, select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings from app.config import settings
-2
View File
@@ -5,8 +5,6 @@ from __future__ import annotations
import logging import logging
import os import os
import uuid import uuid
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -5,7 +5,6 @@ from __future__ import annotations
import logging import logging
import random import random
import uuid import uuid
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -83,7 +82,7 @@ async def compare_prices(
# Calculate average price # Calculate average price
if listings: if listings:
average_price = round(sum(l.price for l in listings) / len(listings), 2) average_price = round(sum(listing.price for listing in listings) / len(listings), 2)
else: else:
average_price = None average_price = None
+1 -3
View File
@@ -3,8 +3,7 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from datetime import date, datetime, timezone from datetime import date
from decimal import Decimal
from typing import Any from typing import Any
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
@@ -26,7 +25,6 @@ async def create_sale(db: AsyncSession, data: dict[str, Any]) -> Sale:
Raises ValueError if vehicle or buyer contact not found. Raises ValueError if vehicle or buyer contact not found.
""" """
vehicle_id = data["vehicle_id"] vehicle_id = data["vehicle_id"]
buyer_contact_id = data["buyer_contact_id"]
# Verify vehicle exists # Verify vehicle exists
vehicle = await db.get(Vehicle, vehicle_id) vehicle = await db.get(Vehicle, vehicle_id)
-1
View File
@@ -8,7 +8,6 @@ from typing import Any
from sqlalchemy import and_, func, or_, select from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.vehicle import Vehicle from app.models.vehicle import Vehicle
-1
View File
@@ -9,7 +9,6 @@ from __future__ import annotations
import logging import logging
import uuid import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import async_session_factory from app.database import async_session_factory
from app.services.ocr_service import process_ocr from app.services.ocr_service import process_ocr
-1
View File
@@ -10,7 +10,6 @@ from datetime import date
from decimal import Decimal from decimal import Decimal
from typing import Any from typing import Any
from app.config import settings
# HTML template for the sales contract # HTML template for the sales contract
-1
View File
@@ -6,7 +6,6 @@ Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext
import csv import csv
import io import io
from datetime import date
from decimal import Decimal from decimal import Decimal
from typing import Any from typing import Any
+1 -1
View File
@@ -84,7 +84,7 @@ def _parse_response(raw_content: str) -> dict[str, Any]:
if text.startswith("```"): if text.startswith("```"):
lines = text.split("\n") lines = text.split("\n")
# Remove first line (```json or ```) and last line (```) # Remove first line (```json or ```) and last line (```)
lines = [l for l in lines if not l.strip().startswith("```")] lines = [line for line in lines if not line.strip().startswith("```")]
text = "\n".join(lines).strip() text = "\n".join(lines).strip()
try: try:
+1
View File
@@ -0,0 +1 @@
{"extends":"next/core-web-vitals"}
+5 -2
View File
@@ -7,6 +7,7 @@ import {
getThumbnailUrl, getThumbnailUrl,
type FileResponse, type FileResponse,
} from '@/lib/files'; } from '@/lib/files';
import Image from 'next/image';
import { FilePreview } from './FilePreview'; import { FilePreview } from './FilePreview';
interface FileGalleryProps { interface FileGalleryProps {
@@ -76,10 +77,12 @@ export function FileGallery({ vehicleId }: FileGalleryProps) {
data-testid={`gallery-item-${image.id}`} data-testid={`gallery-item-${image.id}`}
> >
{thumbUrl && ( {thumbUrl && (
<img <Image
src={thumbUrl} src={thumbUrl}
alt={image.original_filename} alt={image.original_filename}
className="w-full h-full object-cover transition-transform group-hover:scale-105" fill
className="object-cover transition-transform group-hover:scale-105"
unoptimized
/> />
)} )}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-end p-2"> <div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors flex items-end p-2">
+6 -3
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import Image from 'next/image';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { import {
@@ -124,13 +125,15 @@ export function FileList({ vehicleId, onFileDeleted }: FileListProps) {
data-testid={`file-item-${file.id}`} data-testid={`file-item-${file.id}`}
> >
{/* Thumbnail or file icon */} {/* Thumbnail or file icon */}
<div className="aspect-square bg-background flex items-center justify-center"> <div className="aspect-square bg-background flex items-center justify-center relative">
{isImg && thumbUrl ? ( {isImg && thumbUrl ? (
<img <Image
src={thumbUrl} src={thumbUrl}
alt={file.original_filename} alt={file.original_filename}
className="w-full h-full object-cover" fill
className="object-cover"
data-testid={`file-thumb-${file.id}`} data-testid={`file-thumb-${file.id}`}
unoptimized
/> />
) : ( ) : (
<div className="flex flex-col items-center gap-2 text-text-muted"> <div className="flex flex-col items-center gap-2 text-text-muted">
+5 -1
View File
@@ -1,5 +1,6 @@
'use client'; 'use client';
import Image from 'next/image';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { isImageMime, formatFileSize, type FileResponse } from '@/lib/files'; import { isImageMime, formatFileSize, type FileResponse } from '@/lib/files';
@@ -22,11 +23,14 @@ export function FilePreview({ file, open, onClose }: FilePreviewProps) {
{/* Preview content */} {/* Preview content */}
{isImg ? ( {isImg ? (
<div className="flex justify-center"> <div className="flex justify-center">
<img <Image
src={downloadUrl} src={downloadUrl}
alt={file.original_filename} alt={file.original_filename}
width={800}
height={600}
className="max-w-full max-h-[60vh] rounded-lg" className="max-w-full max-h-[60vh] rounded-lg"
data-testid="preview-image" data-testid="preview-image"
unoptimized
/> />
</div> </div>
) : ( ) : (
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState, useCallback, useRef, useEffect } from 'react'; import { useState, useCallback, useRef, useEffect } from 'react';
import Image from 'next/image';
interface BeforeAfterSliderProps { interface BeforeAfterSliderProps {
beforeSrc: string; beforeSrc: string;
@@ -78,12 +79,14 @@ export function BeforeAfterSlider({
onClick={handleClick} onClick={handleClick}
> >
{/* After image (full, background) */} {/* After image (full, background) */}
<img <Image
src={afterSrc} src={afterSrc}
alt={afterLabel} alt={afterLabel}
className="absolute inset-0 w-full h-full object-cover" fill
className="object-cover"
data-testid="after-image" data-testid="after-image"
draggable={false} draggable={false}
unoptimized
/> />
{/* Before image (clipped to left of slider) */} {/* Before image (clipped to left of slider) */}
@@ -91,13 +94,15 @@ export function BeforeAfterSlider({
className="absolute inset-0 overflow-hidden" className="absolute inset-0 overflow-hidden"
style={{ width: `${sliderPos}%` }} style={{ width: `${sliderPos}%` }}
> >
<img <Image
src={beforeSrc} src={beforeSrc}
alt={beforeLabel} alt={beforeLabel}
className="absolute inset-0 h-full object-cover" fill
className="object-cover"
style={{ width: `${containerRef.current?.clientWidth || 100}%` }} style={{ width: `${containerRef.current?.clientWidth || 100}%` }}
data-testid="before-image" data-testid="before-image"
draggable={false} draggable={false}
unoptimized
/> />
</div> </div>
@@ -67,7 +67,7 @@ export function ContractPreview({ saleId, contractPdfPath }: ContractPreviewProp
<div className="bg-gray-50 p-8 text-center rounded" data-testid="contract-empty"> <div className="bg-gray-50 p-8 text-center rounded" data-testid="contract-empty">
<p className="text-gray-500">Es wurde noch kein Vertrag generiert.</p> <p className="text-gray-500">Es wurde noch kein Vertrag generiert.</p>
<p className="text-sm text-gray-400 mt-2"> <p className="text-sm text-gray-400 mt-2">
Klicken Sie auf "Vertrag neu generieren", um ein PDF zu erstellen. Klicken Sie auf &quot;Vertrag neu generieren&quot;, um ein PDF zu erstellen.
</p> </p>
</div> </div>
)} )}
+4011 -1
View File
File diff suppressed because it is too large Load Diff
+11 -9
View File
@@ -12,22 +12,24 @@
}, },
"dependencies": { "dependencies": {
"next": "14.2.5", "next": "14.2.5",
"next-intl": "3.17.0",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1"
"next-intl": "3.17.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "5.5.4", "@testing-library/jest-dom": "6.4.8",
"@testing-library/react": "16.0.1",
"@types/node": "20.14.0", "@types/node": "20.14.0",
"@types/react": "18.3.3", "@types/react": "18.3.3",
"@types/react-dom": "18.3.0", "@types/react-dom": "18.3.0",
"tailwindcss": "3.4.7", "@vitejs/plugin-react": "4.3.1",
"postcss": "8.4.40",
"autoprefixer": "10.4.19", "autoprefixer": "10.4.19",
"vitest": "2.0.5", "eslint": "^8.57.1",
"@testing-library/react": "16.0.1", "eslint-config-next": "^14.2.35",
"@testing-library/jest-dom": "6.4.8",
"jsdom": "24.1.1", "jsdom": "24.1.1",
"@vitejs/plugin-react": "4.3.1" "postcss": "8.4.40",
"tailwindcss": "3.4.7",
"typescript": "5.5.4",
"vitest": "2.0.5"
} }
} }