feat(my-customers): flatten orders into per-device cards
- split orders by device_name into FlattenedDevice entries - strip device prefixes from product and module names - render one DeviceCard per Kasse in the customer accordion - add targeted upgrade URL with orderId, customer_id, device_id - wizard starts at step 3 in upgrade mode with locked customer - mark existing licensed modules as read-only with badge
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { createAdminClient } from '@/lib/supabase/admin'
|
||||
import type { Order, EndCustomer, EndCustomerWithOrders, OrderWithRegisterName } from '@/lib/types'
|
||||
import type { Order, EndCustomer, EndCustomerWithOrders, OrderWithRegisterName, FlattenedDevice, EndCustomerWithDevices, OrderItem } from '@/lib/types'
|
||||
|
||||
/**
|
||||
* Holt alle Bestellanfragen.
|
||||
@@ -74,6 +74,91 @@ export async function getPartnerCustomersWithOrders(): Promise<EndCustomerWithOr
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereinigt Kassen-Präfixe aus Produkt- und Modulnamen.
|
||||
* Entfernt Muster wie "Kasse1:", "kass2:", "kasse 1:" etc.
|
||||
*/
|
||||
function stripDevicePrefix(name: string): string {
|
||||
return name.replace(/^kasse?\s*\d*\s*:\s*/i, '').trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Endkunden mit geflatteteten Kassen (eine Kasse = ein FlattenedDevice).
|
||||
* Jede Order wird nach device_name in einzelne Kassen aufgespalten.
|
||||
* RLS filtert automatisch über get_auth_company_id().
|
||||
*/
|
||||
export async function getPartnerCustomersWithDevices(): Promise<EndCustomerWithDevices[]> {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('end_customers')
|
||||
.select('*, orders(*)')
|
||||
.order('company_name', { ascending: true })
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return (data || []).map((customer: any) => {
|
||||
const rawOrders = customer.orders || []
|
||||
const sortedOrders = [...rawOrders].sort(
|
||||
(a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
)
|
||||
|
||||
const devices: FlattenedDevice[] = []
|
||||
|
||||
for (const order of sortedOrders) {
|
||||
const allItems: OrderItem[] = order.order_data?.items || []
|
||||
const totalOrderPrice: number = order.total_price || 0
|
||||
|
||||
// Gruppiere Items nach device_name
|
||||
const deviceGroups: Record<string, OrderItem[]> = {}
|
||||
for (const item of allItems) {
|
||||
const key = item.device_name || 'Kasse 1'
|
||||
if (!deviceGroups[key]) deviceGroups[key] = []
|
||||
deviceGroups[key].push(item)
|
||||
}
|
||||
|
||||
const deviceKeys = Object.keys(deviceGroups)
|
||||
|
||||
for (const deviceKey of deviceKeys) {
|
||||
const devItems = deviceGroups[deviceKey]
|
||||
|
||||
// Bereinige Produkt- und Modulnamen
|
||||
const cleanedItems: OrderItem[] = devItems.map(item => ({
|
||||
...item,
|
||||
product_name: stripDevicePrefix(item.product_name),
|
||||
selected_modules: (item.selected_modules || []).map(mod => ({
|
||||
...mod,
|
||||
module_name: stripDevicePrefix(mod.module_name),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Anteiliger Preis dieser Kasse (gleichmäßige Aufteilung)
|
||||
const devicePrice = deviceKeys.length > 1
|
||||
? totalOrderPrice / deviceKeys.length
|
||||
: totalOrderPrice
|
||||
|
||||
devices.push({
|
||||
orderId: order.id,
|
||||
orderNumber: order.order_number,
|
||||
orderStatus: order.status,
|
||||
pdfUrl: order.pdf_url,
|
||||
createdAt: order.created_at,
|
||||
deviceId: deviceKey,
|
||||
deviceName: deviceKey,
|
||||
items: cleanedItems,
|
||||
totalPrice: Math.round(devicePrice * 100) / 100,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { orders: _orders, ...customerWithoutOrders } = customer
|
||||
return {
|
||||
...customerWithoutOrders,
|
||||
devices,
|
||||
} as EndCustomerWithDevices
|
||||
})
|
||||
}
|
||||
|
||||
export interface GetCustomersForWizardParams {
|
||||
partnerCompanyId?: string
|
||||
page?: number
|
||||
|
||||
@@ -178,6 +178,30 @@ export type EndCustomerWithOrders = EndCustomer & {
|
||||
orders: OrderWithRegisterName[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine einzelne Kasse, herausgelöst aus einem Order-Snapshot.
|
||||
* Ermöglicht die Darstellung einer Kasse = eine Karte (Flattening).
|
||||
*/
|
||||
export type FlattenedDevice = {
|
||||
orderId: string
|
||||
orderNumber: string
|
||||
orderStatus: Order['status']
|
||||
pdfUrl: string | null
|
||||
createdAt: string
|
||||
/** Eindeutiger Bezeichner der Kasse innerhalb der Order (= device_name) */
|
||||
deviceId: string
|
||||
/** Anzeigename der Kasse, bereinigt von Präfixen */
|
||||
deviceName: string
|
||||
/** Items (Produkte + Module) die zu dieser Kasse gehören */
|
||||
items: OrderItem[]
|
||||
/** Anteiliger Gesamtpreis dieser Kasse */
|
||||
totalPrice: number
|
||||
}
|
||||
|
||||
export type EndCustomerWithDevices = EndCustomer & {
|
||||
devices: FlattenedDevice[]
|
||||
}
|
||||
|
||||
// ─── Lizenz-Output (für Lizenzserver / ERP) ───────────────────────────────────
|
||||
|
||||
export type LicenseOption = {
|
||||
|
||||
Reference in New Issue
Block a user