Compare commits
4 Commits
6b9023c871
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13f647409f | ||
|
|
5d3eed874c | ||
|
|
3a090b7664 | ||
|
|
e2d92a031f |
@@ -1,8 +1,8 @@
|
||||
# LicServer API Spezifikation (v1)
|
||||
|
||||
Dokumentation der REST-Schnittstellen des CASPOS Lizenzservers (`http://192.168.178.174:9981`).
|
||||
Dokumentation der REST-Schnittstellen des CASPOS Lizenzservers.
|
||||
|
||||
## 1. Endpunkte
|
||||
## 1. Endpunkte (Port 9981 - Activation API)
|
||||
|
||||
### Activation (Aktivierung)
|
||||
|
||||
@@ -89,7 +89,119 @@ Standard Fehlerobjekt `ProblemDetails`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Typen & Schemas
|
||||
## 2. Endpunkte (Port 9980 - Management API)
|
||||
|
||||
Alle Anfragen an die Management-API müssen authentifiziert sein.
|
||||
- **Header-Format:** `X-Api-Key: <Schlüssel>`
|
||||
|
||||
### Partners (Partner)
|
||||
|
||||
#### `GET /api-v1/partners`
|
||||
Gibt eine paginierte Liste aller Partner zurück.
|
||||
|
||||
**Request Parameters (Query):**
|
||||
- `search` (string, optional): Filtert nach Name/E-Mail.
|
||||
- `page` (integer, default: 1)
|
||||
- `pageSize` (integer, default: 20)
|
||||
|
||||
**Response `200 OK` (`application/json`):**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalCount": 1,
|
||||
"totalPages": 1,
|
||||
"hasNextPage": false,
|
||||
"hasPreviousPage": false,
|
||||
"items": [
|
||||
{
|
||||
"id": "string (UUID)",
|
||||
"name": "string",
|
||||
"email": "string",
|
||||
"erpId": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api-v1/partners/{id}`
|
||||
Holt Partner-Details zu einer bestimmten ID.
|
||||
|
||||
**Response `200 OK` (`application/json`):**
|
||||
```json
|
||||
{
|
||||
"id": "string (UUID)",
|
||||
"name": "string",
|
||||
"email": "string",
|
||||
"erpId": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Customers (Kunden von Partnern)
|
||||
|
||||
#### `GET /api-v1/partners/{partnerId}/customers`
|
||||
Gibt eine paginierte Liste aller Kunden eines bestimmten Partners zurück.
|
||||
|
||||
**Request Parameters (Query):**
|
||||
- `search` (string, optional): Filtert nach Name/E-Mail.
|
||||
- `page` (integer, default: 1)
|
||||
- `pageSize` (integer, default: 20)
|
||||
|
||||
**Response `200 OK` (`application/json`):**
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"pageSize": 20,
|
||||
"totalCount": 1,
|
||||
"totalPages": 1,
|
||||
"hasNextPage": false,
|
||||
"hasPreviousPage": false,
|
||||
"items": [
|
||||
{
|
||||
"id": "string (UUID)",
|
||||
"name": "string",
|
||||
"street": "string",
|
||||
"houseNumber": "string",
|
||||
"houseNumberAddon": "string",
|
||||
"zipCode": "string",
|
||||
"city": "string",
|
||||
"countryCode": "string",
|
||||
"email": "string",
|
||||
"partnerId": "string (UUID)",
|
||||
"erpId": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api-v1/partners/{partnerId}/customers/{customerId}`
|
||||
Holt detaillierte Kundeninformationen.
|
||||
|
||||
**Response `200 OK` (`application/json`):**
|
||||
```json
|
||||
{
|
||||
"id": "string (UUID)",
|
||||
"name": "string",
|
||||
"street": "string",
|
||||
"houseNumber": "string",
|
||||
"houseNumberAddon": "string",
|
||||
"zipCode": "string",
|
||||
"city": "string",
|
||||
"countryCode": "string",
|
||||
"email": "string",
|
||||
"partnerId": "string (UUID)",
|
||||
"erpId": "string",
|
||||
"partnerName": "string",
|
||||
"taxNumber": "string",
|
||||
"vatIdNumber": "string",
|
||||
"notes": "string",
|
||||
"locations": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Typen & Schemas
|
||||
|
||||
### `LicenseData`
|
||||
```json
|
||||
@@ -112,3 +224,4 @@ Standard Fehlerobjekt `ProblemDetails`.
|
||||
"instance": "string"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { getCompanies, createCompany, updateCompany, deleteCompany, syncCompaniesFromLicServer } from '@/lib/actions/companies'
|
||||
import { getCompanies, createCompany, updateCompany, deleteCompany, syncCompaniesFromLicServer, syncCustomersFromLicServer } from '@/lib/actions/companies'
|
||||
|
||||
export default function CompaniesPage() {
|
||||
const [companies, setCompanies] = useState<any[]>([])
|
||||
@@ -42,8 +42,9 @@ export default function CompaniesPage() {
|
||||
setSyncing(true)
|
||||
setStatusMsg(null)
|
||||
try {
|
||||
const res = await syncCompaniesFromLicServer()
|
||||
setStatusMsg(`${res.count} Partner erfolgreich vom LicServer importiert/aktualisiert.`)
|
||||
const resCompanies = await syncCompaniesFromLicServer()
|
||||
const resCustomers = await syncCustomersFromLicServer()
|
||||
setStatusMsg(`${resCompanies.count} Partner und ${resCustomers.count} Kunden erfolgreich vom LicServer importiert.`)
|
||||
setStatusType('success')
|
||||
loadCompanies()
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -21,14 +21,14 @@ export default function AdminSettings() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// LicServer Config State
|
||||
const [licUrl, setLicUrl] = useState('');
|
||||
const [licKey, setLicKey] = useState('');
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [licSaving, setLicSaving] = useState(false);
|
||||
const [licTesting, setLicTesting] = useState(false);
|
||||
const [licStatus, setLicStatus] = useState<{ ok: boolean; message: string } | null>(null);
|
||||
const [licMsg, setLicMsg] = useState('');
|
||||
const [licMsgType, setLicMsgType] = useState<'success' | 'error' | ''>('');
|
||||
const [licUrl, setLicUrl] = useState('');
|
||||
const [licKey, setLicKey] = useState('');
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [licSaving, setLicSaving] = useState(false);
|
||||
const [licTesting, setLicTesting] = useState(false);
|
||||
const [licStatus, setLicStatus] = useState<{ ok: boolean; message: string } | null>(null);
|
||||
const [licMsg, setLicMsg] = useState('');
|
||||
const [licMsgType, setLicMsgType] = useState<'success' | 'error' | ''>('');
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,7 +63,7 @@ export default function AdminSettings() {
|
||||
.single();
|
||||
if (licRow) {
|
||||
setLicUrl(licRow.licserver_base_url || '');
|
||||
setLicKey(licRow.licserver_api_key || '');
|
||||
setLicKey(licRow.licserver_api_key || '');
|
||||
}
|
||||
} catch (dbErr) {
|
||||
console.error("Fehler beim Laden der LicServer-Einstellungen:", dbErr);
|
||||
@@ -90,7 +90,7 @@ export default function AdminSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/admin/db-backup');
|
||||
if (!res.ok) throw new Error('Export fehlgeschlagen');
|
||||
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -184,11 +184,10 @@ export default function AdminSettings() {
|
||||
</div>
|
||||
|
||||
{statusMsg && (
|
||||
<div className={`p-4 rounded-lg text-sm border ${
|
||||
statusType === 'success' ? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400' :
|
||||
<div className={`p-4 rounded-lg text-sm border ${statusType === 'success' ? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400' :
|
||||
statusType === 'error' ? 'bg-destructive/10 border-destructive/20 text-destructive' :
|
||||
'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-400'
|
||||
}`}>
|
||||
'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-400'
|
||||
}`}>
|
||||
{statusMsg}
|
||||
</div>
|
||||
)}
|
||||
@@ -205,8 +204,8 @@ export default function AdminSettings() {
|
||||
Lädt alle Kategorien, Produkte, Module, Firmen, Endkunden, Bestellungen und SMTP-Einstellungen als ZIP-Datei herunter.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={exporting || importing}
|
||||
className="w-full bg-primary hover:bg-primary/90 text-white"
|
||||
>
|
||||
@@ -235,9 +234,9 @@ export default function AdminSettings() {
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="relative border border-dashed border-slate-200 dark:border-white/10 rounded-lg p-3 hover:bg-slate-100/50 dark:hover:bg-white/5 transition">
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
<input
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
||||
disabled={exporting || importing}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
||||
@@ -251,8 +250,8 @@ export default function AdminSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={!selectedFile || exporting || importing}
|
||||
variant="secondary"
|
||||
className="w-full border border-slate-200 dark:border-white/10"
|
||||
@@ -275,29 +274,26 @@ export default function AdminSettings() {
|
||||
LicServer Konfiguration
|
||||
</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400 text-xs mt-1">
|
||||
Verbindungseinstellungen zum CASPOS Lizenzserver (internes Container-Netz).
|
||||
Der API-Key wird verschlüsselt in der Datenbank gespeichert und nie an den Browser übertragen.
|
||||
Verbindungseinstellungen zum CASPOS Lizenzserver.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{licMsg && (
|
||||
<div className={`p-3 rounded-lg text-sm border ${
|
||||
licMsgType === 'success'
|
||||
? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400'
|
||||
: 'bg-destructive/10 border-destructive/20 text-destructive'
|
||||
}`}>
|
||||
<div className={`p-3 rounded-lg text-sm border ${licMsgType === 'success'
|
||||
? 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-400'
|
||||
: 'bg-destructive/10 border-destructive/20 text-destructive'
|
||||
}`}>
|
||||
{licMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Connection Test Result */}
|
||||
{licStatus && (
|
||||
<div className={`flex items-center gap-2 p-3 rounded-lg text-sm border ${
|
||||
licStatus.ok
|
||||
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-amber-500/10 border-amber-500/20 text-amber-600 dark:text-amber-400'
|
||||
}`}>
|
||||
<div className={`flex items-center gap-2 p-3 rounded-lg text-sm border ${licStatus.ok
|
||||
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-amber-500/10 border-amber-500/20 text-amber-600 dark:text-amber-400'
|
||||
}`}>
|
||||
{licStatus.ok
|
||||
? <CheckCircle2 className="w-4 h-4 shrink-0" />
|
||||
: <XCircle className="w-4 h-4 shrink-0" />}
|
||||
@@ -318,7 +314,7 @@ export default function AdminSettings() {
|
||||
placeholder="http://192.168.178.174:9980"
|
||||
className="text-sm font-mono"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-400">Interne Container-URL ohne trailing slash</p>
|
||||
<p className="text-[10px] text-slate-400">URL des CASPOS Lizenzservers</p>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
@@ -344,51 +340,51 @@ export default function AdminSettings() {
|
||||
{showKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400">Wird als X-Api-Key Header an den LicServer gesendet</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-1">
|
||||
<Button
|
||||
id="lic-save-btn"
|
||||
disabled={licSaving || licTesting}
|
||||
onClick={async () => {
|
||||
setLicSaving(true);
|
||||
setLicMsg('');
|
||||
setLicStatus(null);
|
||||
const res = await saveLicServerConfig(licUrl, licKey);
|
||||
setLicMsgType(res.success ? 'success' : 'error');
|
||||
setLicMsg(res.success ? 'Konfiguration gespeichert.' : (res.error || 'Fehler beim Speichern'));
|
||||
setLicSaving(false);
|
||||
}}
|
||||
className="bg-violet-600 hover:bg-violet-500 text-white"
|
||||
>
|
||||
{licSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <KeyRound className="w-4 h-4 mr-2" />}
|
||||
Speichern
|
||||
</Button>
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-1">
|
||||
<Button
|
||||
id="lic-save-btn"
|
||||
disabled={licSaving || licTesting}
|
||||
onClick={async () => {
|
||||
setLicSaving(true);
|
||||
setLicMsg('');
|
||||
setLicStatus(null);
|
||||
const res = await saveLicServerConfig(licUrl, licKey);
|
||||
setLicMsgType(res.success ? 'success' : 'error');
|
||||
setLicMsg(res.success ? 'Konfiguration gespeichert.' : (res.error || 'Fehler beim Speichern'));
|
||||
setLicSaving(false);
|
||||
}}
|
||||
className="bg-violet-600 hover:bg-violet-500 text-white"
|
||||
>
|
||||
{licSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <KeyRound className="w-4 h-4 mr-2" />}
|
||||
Speichern
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
id="lic-test-btn"
|
||||
variant="outline"
|
||||
disabled={licSaving || licTesting}
|
||||
onClick={async () => {
|
||||
setLicTesting(true);
|
||||
setLicStatus(null);
|
||||
// Save first, then test
|
||||
await saveLicServerConfig(licUrl, licKey);
|
||||
const result = await testLicServerConnection();
|
||||
setLicStatus(result);
|
||||
setLicTesting(false);
|
||||
}}
|
||||
className="border-white/10"
|
||||
>
|
||||
{licTesting ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Wifi className="w-4 h-4 mr-2" />}
|
||||
Verbindung testen
|
||||
</Button>
|
||||
<Button
|
||||
id="lic-test-btn"
|
||||
variant="outline"
|
||||
disabled={licSaving || licTesting}
|
||||
onClick={async () => {
|
||||
setLicTesting(true);
|
||||
setLicStatus(null);
|
||||
// Save first, then test
|
||||
await saveLicServerConfig(licUrl, licKey);
|
||||
const result = await testLicServerConnection();
|
||||
setLicStatus(result);
|
||||
setLicTesting(false);
|
||||
}}
|
||||
className="border-white/10"
|
||||
>
|
||||
{licTesting ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Wifi className="w-4 h-4 mr-2" />}
|
||||
Verbindung testen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
25
shop/app/api/cron/sync-customers/route.ts
Normal file
25
shop/app/api/cron/sync-customers/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { syncCustomersFromLicServer } from '@/lib/actions/companies'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// Check authorization header
|
||||
const authHeader = req.headers.get('authorization')
|
||||
const cronSecret = process.env.CRON_SECRET
|
||||
|
||||
// Secure the cron endpoint in production
|
||||
if (process.env.NODE_ENV === 'production' && cronSecret) {
|
||||
if (authHeader !== `Bearer ${cronSecret}`) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await syncCustomersFromLicServer()
|
||||
return NextResponse.json({ success: true, count: res.count })
|
||||
} catch (err: any) {
|
||||
console.error('[cron] Customer sync failed:', err)
|
||||
return NextResponse.json({ error: err.message || 'Sync failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
85
shop/app/api/licserver/partners/[id]/customers/route.ts
Normal file
85
shop/app/api/licserver/partners/[id]/customers/route.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { getLicServerConfig } from '@/lib/actions/licserver-config'
|
||||
import type { CustomerDtoPagedResult, LicServerProblemDetails } from '@/lib/types/licserver'
|
||||
|
||||
/**
|
||||
* GET /api/licserver/partners/[id]/customers
|
||||
*
|
||||
* Proxy to LicServer GET /api-v1/partners/{id}/customers
|
||||
* [id] must be a valid UUID.
|
||||
* Query params forwarded: search, page, pageSize
|
||||
*
|
||||
* Secured: requires a valid Supabase session.
|
||||
* base_url + api_key are read from DB settings (admin-configurable),
|
||||
* with fallback to LICSERVER_BASE_URL / LICSERVER_API_KEY env vars.
|
||||
*/
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
// ── Auth guard ─────────────────────────────────────────────────────────
|
||||
const supabase = await createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// ── Load config from DB (falls back to env vars) ──────────────────────
|
||||
const cfg = await getLicServerConfig()
|
||||
if (!cfg.base_url || !cfg.api_key) {
|
||||
console.error('[licserver] base_url or api_key not configured (DB or env)')
|
||||
return NextResponse.json(
|
||||
{ error: 'LicServer nicht konfiguriert. Bitte in den Admin-Einstellungen hinterlegen.' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
const { id } = await params
|
||||
|
||||
// Basic UUID format guard
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
if (!UUID_RE.test(id)) {
|
||||
return NextResponse.json({ error: 'Invalid partner ID' }, { status: 400 })
|
||||
}
|
||||
|
||||
// ── Forward query params ───────────────────────────────────────────────
|
||||
const { searchParams } = req.nextUrl
|
||||
const upstream = new URL(`${cfg.base_url}/api-v1/partners/${id}/customers`)
|
||||
const search = searchParams.get('search')
|
||||
const page = searchParams.get('page')
|
||||
const pageSize = searchParams.get('pageSize')
|
||||
if (search) upstream.searchParams.set('search', search)
|
||||
if (page) upstream.searchParams.set('page', page)
|
||||
if (pageSize) upstream.searchParams.set('pageSize', pageSize)
|
||||
|
||||
// ── Proxy request ──────────────────────────────────────────────────────
|
||||
try {
|
||||
const res = await fetch(
|
||||
upstream.toString(),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Api-Key': cfg.api_key,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
}
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
const problem: LicServerProblemDetails = await res.json().catch(() => ({}))
|
||||
return NextResponse.json(problem, { status: res.status })
|
||||
}
|
||||
|
||||
const data: CustomerDtoPagedResult = await res.json()
|
||||
return NextResponse.json(data)
|
||||
|
||||
} catch (err) {
|
||||
console.error('[licserver] partner/:id/customers fetch error:', err)
|
||||
return NextResponse.json(
|
||||
{ error: 'LicServer nicht erreichbar' },
|
||||
{ status: 502 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -262,3 +262,93 @@ export async function syncCompaniesFromLicServer() {
|
||||
return { success: true, count: partners.length }
|
||||
}
|
||||
|
||||
export async function syncCustomersFromLicServer() {
|
||||
const admin = createAdminClient()
|
||||
const cfg = await getLicServerConfigSystem()
|
||||
|
||||
if (!cfg.base_url || !cfg.api_key) {
|
||||
throw new Error('LicServer ist nicht konfiguriert.')
|
||||
}
|
||||
|
||||
// 1. Get all local companies
|
||||
const { data: companies, error: compErr } = await admin
|
||||
.from('companies')
|
||||
.select('id')
|
||||
|
||||
if (compErr) throw compErr
|
||||
if (!companies || companies.length === 0) {
|
||||
return { success: true, count: 0 }
|
||||
}
|
||||
|
||||
let totalSynced = 0
|
||||
const allUpsertRows: any[] = []
|
||||
|
||||
// 2. Fetch customers for each company
|
||||
for (const company of companies) {
|
||||
try {
|
||||
// Workaround for LicServer crash on null/empty search parameter:
|
||||
// We query using common vowels/umlauts in parallel and de-duplicate locally.
|
||||
const searchChars = ['a', 'e', 'i', 'o', 'u', 'ä', 'ö', 'ü', 'y']
|
||||
const customerMap = new Map<string, any>()
|
||||
|
||||
await Promise.all(
|
||||
searchChars.map(async char => {
|
||||
try {
|
||||
const res = await fetch(`${cfg.base_url}/api-v1/partners/${company.id}/customers?pageSize=1000&search=${encodeURIComponent(char)}`, {
|
||||
headers: { 'X-Api-Key': cfg.api_key!, 'Accept': 'application/json' },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
})
|
||||
if (res.ok) {
|
||||
const result = await res.json()
|
||||
const items = result.items || []
|
||||
for (const c of items) {
|
||||
customerMap.set(c.id, c)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[sync-customers] Error fetching with search=${char} for partner ${company.id}:`, e.message || e)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const customers = Array.from(customerMap.values())
|
||||
|
||||
for (const c of customers) {
|
||||
const streetCombined = [c.street, c.houseNumber, c.houseNumberAddon]
|
||||
.filter(Boolean)
|
||||
.map(s => s.trim())
|
||||
.join(' ')
|
||||
|
||||
allUpsertRows.push({
|
||||
id: c.id,
|
||||
partner_id: company.id,
|
||||
company_name: c.name || 'Unbenannt',
|
||||
street: streetCombined || null,
|
||||
zip: c.zipCode || null,
|
||||
city: c.city || null,
|
||||
email: c.email || null,
|
||||
is_anonymized: false,
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[sync-customers] Error fetching customers for partner ${company.id}:`, e.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
if (allUpsertRows.length > 0) {
|
||||
const { error: upsertErr } = await admin
|
||||
.from('end_customers')
|
||||
.upsert(allUpsertRows, { onConflict: 'id' })
|
||||
|
||||
if (upsertErr) throw upsertErr
|
||||
totalSynced = allUpsertRows.length
|
||||
}
|
||||
|
||||
revalidatePath('/my-customers')
|
||||
revalidatePath('/order')
|
||||
revalidatePath('/admin/companies')
|
||||
|
||||
return { success: true, count: totalSynced }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,3 +29,60 @@ export interface LicServerProblemDetails {
|
||||
instance?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface CustomerDto {
|
||||
id: string // UUID
|
||||
name: string | null
|
||||
street: string | null
|
||||
houseNumber: string | null
|
||||
houseNumberAddon: string | null
|
||||
zipCode: string | null
|
||||
city: string | null
|
||||
countryCode: string | null
|
||||
email: string | null
|
||||
partnerId: string | null
|
||||
erpId: string | null
|
||||
}
|
||||
|
||||
export interface CustomerDtoPagedResult {
|
||||
page: number
|
||||
pageSize: number
|
||||
totalCount: number
|
||||
totalPages: number
|
||||
hasNextPage: boolean
|
||||
hasPreviousPage: boolean
|
||||
items: CustomerDto[] | null
|
||||
}
|
||||
|
||||
export interface LocationDto {
|
||||
id: string // UUID
|
||||
name: string | null
|
||||
street: string | null
|
||||
houseNumber: string | null
|
||||
houseNumberAddon: string | null
|
||||
countryCode: string | null
|
||||
postalCode: string | null
|
||||
city: string | null
|
||||
taxNumber: string | null
|
||||
vatIdNumber: string | null
|
||||
}
|
||||
|
||||
export interface CustomerDetailsDto {
|
||||
id: string // UUID
|
||||
name: string | null
|
||||
street: string | null
|
||||
houseNumber: string | null
|
||||
houseNumberAddon: string | null
|
||||
zipCode: string | null
|
||||
city: string | null
|
||||
countryCode: string | null
|
||||
email: string | null
|
||||
partnerId: string | null
|
||||
erpId: string | null
|
||||
partnerName: string | null
|
||||
taxNumber: string | null
|
||||
vatIdNumber: string | null
|
||||
notes: string | null
|
||||
locations: LocationDto[] | null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user