feat: assign orders to companies directly and support admin reassignment
All checks were successful
Staging Build / build (push) Successful in 3m36s

This commit is contained in:
DanielS
2026-07-04 16:08:31 +02:00
parent 72852a6b08
commit 305af64fd6
7 changed files with 99 additions and 18 deletions

View File

@@ -6,7 +6,7 @@ Diese Datei beschreibt die Architektur, das Datenmodell, die Kernprozesse und da
## 1. Projekt-Überblick ## 1. Projekt-Überblick
Der CASPOS Webshop ist ein B2B-Portal für Vertriebspartner. Partner können hier Software-Pakete und Zusatzmodule (für POS/Kassenlösungen) konfigurieren, Angebote/Anfragen für ihre Endkunden erstellen und Lizenzen verwalten. Der CASPOS Webshop ist ein B2B-Portal für Vertriebspartner. Partner können hier Software-Pakete und Zusatzmodule (für POS/Kassenlösungen) konfigurieren, Anfragen für ihre Endkunden erstellen und Lizenzen verwalten.
--- ---
@@ -128,11 +128,13 @@ RLS ist auf Datenbankebene in Postgres implementiert und erzwingt Datenisolierun
- `USING (partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()))` - `USING (partner_id = (SELECT company_id FROM public.users WHERE id = auth.uid()))`
- Partner sehen und modifizieren nur Endkunden, deren `partner_id` mit der `company_id` des angemeldeten Benutzers übereinstimmt. - Partner sehen und modifizieren nur Endkunden, deren `partner_id` mit der `company_id` des angemeldeten Benutzers übereinstimmt.
- **Bestellungen (`orders`)**: - **Bestellungen (`orders`)**:
- Partner sehen und modifizieren nur Bestellungen von Benutzern, die derselben Company angehören (oder ihre eigenen, falls keine Company zugewiesen ist). - Partner sehen und modifizieren nur Bestellungen von Benutzern, die derselben Company angehören (sie müssen einer company zugewiesen sein).
- Admins haben uneingeschränkten Zugriff. - Admins haben uneingeschränkten Zugriff.
- **Lizenzen (`licenses`)**: - **Lizenzen (`licenses`)**:
- Partner sehen nur Lizenzen von Endkunden, die ihrer Company zugeordnet sind. - Partner sehen nur Lizenzen von Endkunden, die ihrer Company zugeordnet sind.
- **Erstellung: jede bestellung wird der firma des erstellers zugewiesen. Ein admin ist aber fähig einer bestellung nachträglich eine andere firma zuzuweisen.
--- ---
## Kommentarbereich ## Kommentarbereich

View File

@@ -36,13 +36,7 @@ export default async function MyOrdersPage() {
let query = supabase.from('orders').select('*') let query = supabase.from('orders').select('*')
if (dbUser?.company_id) { if (dbUser?.company_id) {
const { data: companyUsers } = await supabase query = query.eq('company_id', dbUser.company_id)
.from('users')
.select('id')
.eq('company_id', dbUser.company_id)
const userIds = (companyUsers ?? []).map(u => u.id)
query = query.in('user_id', userIds)
} else { } else {
query = query.eq('user_id', user.id) query = query.eq('user_id', user.id)
} }

View File

@@ -3,6 +3,7 @@ import { createAdminClient } from '@/lib/supabase/admin'
import { getProducts, getCategories } from '@/lib/actions/products' import { getProducts, getCategories } from '@/lib/actions/products'
import { getEndCustomers } from '@/lib/actions/end-customers' import { getEndCustomers } from '@/lib/actions/end-customers'
import { OrderWizard } from '@/components/order-wizard' import { OrderWizard } from '@/components/order-wizard'
import { getCompanies } from '@/lib/actions/companies'
import { redirect } from 'next/navigation' import { redirect } from 'next/navigation'
import { Suspense } from 'react' import { Suspense } from 'react'
@@ -121,5 +122,21 @@ async function OrderDataWrapper({ orderId }: { orderId?: string }) {
console.error('Fehler beim Laden des Profils:', e) console.error('Fehler beim Laden des Profils:', e)
} }
return <OrderWizard products={products} categories={categories} initialProfile={profile} initialEndCustomers={endCustomers} initialOrder={initialOrder} /> // Fetch companies list for admin
let companies: any[] = []
if (userData?.role === 'admin') {
companies = await getCompanies().catch(() => [])
}
return (
<OrderWizard
products={products}
categories={categories}
initialProfile={profile}
initialEndCustomers={endCustomers}
initialOrder={initialOrder}
isAdmin={userData?.role === 'admin'}
companies={companies}
/>
)
} }

View File

@@ -81,16 +81,23 @@ export function OrderWizard({
initialProfile, initialProfile,
initialEndCustomers, initialEndCustomers,
initialOrder, initialOrder,
isAdmin = false,
companies = [],
}: { }: {
products: Product[] products: Product[]
categories: Category[] categories: Category[]
initialProfile: Profile | null initialProfile: Profile | null
initialEndCustomers: EndCustomer[] initialEndCustomers: EndCustomer[]
initialOrder?: Order | null initialOrder?: Order | null
isAdmin?: boolean
companies?: any[]
}) { }) {
const router = useRouter() const router = useRouter()
const [step, setStep] = useState(initialOrder ? 3 : 1) const [step, setStep] = useState(initialOrder ? 3 : 1)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [selectedCompanyId, setSelectedCompanyId] = useState<string | null>(
initialOrder?.company_id ?? null
)
// Partner-Profil (Fallback) // Partner-Profil (Fallback)
const [customerData] = useState<Partial<Profile>>(initialProfile || {}) const [customerData] = useState<Partial<Profile>>(initialProfile || {})
@@ -414,6 +421,7 @@ export function OrderWizard({
endCustomer: selectedEndCustomer, endCustomer: selectedEndCustomer,
billingInterval: selectedBillingInterval, billingInterval: selectedBillingInterval,
lastLicenseDate: lastLicenseDate || null, lastLicenseDate: lastLicenseDate || null,
companyId: selectedCompanyId,
}) })
} else { } else {
order = await submitOrder({ order = await submitOrder({
@@ -482,6 +490,29 @@ export function OrderWizard({
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
{/* Admin Company Selector */}
{isAdmin && (
<div className="p-4 rounded-xl bg-white/5 border border-white/10 space-y-3">
<Label htmlFor="order-company" className="text-white font-medium flex items-center gap-2">
<Building2 className="w-4 h-4 text-primary" />
Zugeordnetes Unternehmen (Admin-Option)
</Label>
<select
id="order-company"
value={selectedCompanyId || ''}
onChange={(e) => setSelectedCompanyId(e.target.value || null)}
className="flex h-9 w-full rounded-md border border-white/10 bg-slate-900 px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary text-white"
>
<option value="">Keine Firma zugewiesen</option>
{companies.map((c: any) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
</div>
)}
{/* Modus-Toggle */} {/* Modus-Toggle */}
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button

View File

@@ -193,6 +193,7 @@ export async function submitOrder(params: {
.from('orders') .from('orders')
.insert([{ .insert([{
user_id: user.id, user_id: user.id,
company_id: dbUser?.company_id || null,
order_number: orderNumber, order_number: orderNumber,
order_hash: orderHash, order_hash: orderHash,
end_customer_id: endCustomerId ?? null, end_customer_id: endCustomerId ?? null,
@@ -481,9 +482,10 @@ export async function updateOrder(
endCustomer?: EndCustomer | null endCustomer?: EndCustomer | null
billingInterval?: 'one_time' | 'monthly' billingInterval?: 'one_time' | 'monthly'
lastLicenseDate?: string | null lastLicenseDate?: string | null
companyId?: string | null
} }
): Promise<Order> { ): Promise<Order> {
const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate } = params const { selections, moduleQuantities, customerProfile, endCustomerId, endCustomer, billingInterval, lastLicenseDate, companyId } = params
const supabase = await createClient() const supabase = await createClient()
const admin = createAdminClient() const admin = createAdminClient()
@@ -612,15 +614,21 @@ export async function updateOrder(
const orderHash = hashOrderSnapshot(orderSnapshot) const orderHash = hashOrderSnapshot(orderSnapshot)
// 2. Bestellung in DB aktualisieren // 2. Bestellung in DB aktualisieren
const updatePayload: any = {
end_customer_id: endCustomerId ?? null,
total_price: orderSnapshot.total,
customer_data: customerSnapshot,
order_data: orderSnapshot,
order_hash: orderHash,
}
if (isAdmin && companyId !== undefined) {
updatePayload.company_id = companyId
}
const { data: order, error: orderError } = await admin const { data: order, error: orderError } = await admin
.from('orders') .from('orders')
.update({ .update(updatePayload)
end_customer_id: endCustomerId ?? null,
total_price: orderSnapshot.total,
customer_data: customerSnapshot,
order_data: orderSnapshot,
order_hash: orderHash,
})
.eq('id', orderId) .eq('id', orderId)
.select() .select()
.single() .single()

View File

@@ -149,6 +149,7 @@ export type OrderSnapshot = {
export type Order = { export type Order = {
id: string id: string
user_id: string | null user_id: string | null
company_id: string | null
order_number: string order_number: string
order_hash: string | null order_hash: string | null
end_customer_id: string | null end_customer_id: string | null

View File

@@ -0,0 +1,28 @@
-- Migration: Add company_id to public.orders and update RLS policies
-- 1. Add company_id column to orders table
ALTER TABLE public.orders
ADD COLUMN IF NOT EXISTS company_id UUID REFERENCES public.companies(id) ON DELETE SET NULL;
-- 2. Backfill company_id from users for existing orders
UPDATE public.orders o
SET company_id = (SELECT company_id FROM public.users u WHERE u.id = o.user_id)
WHERE o.company_id IS NULL AND o.user_id IS NOT NULL;
-- 3. Recreate RLS policies for public.orders to use orders.company_id
DROP POLICY IF EXISTS "Users can view their own orders" ON public.orders;
CREATE POLICY "Users can view their own orders" ON public.orders FOR SELECT USING (
(SELECT role FROM public.users WHERE id = auth.uid()) = 'admin'
OR company_id = (SELECT company_id FROM public.users WHERE id = auth.uid())
OR auth.uid() = user_id
);
DROP POLICY IF EXISTS "Users can update their own orders" ON public.orders;
CREATE POLICY "Users can update their own orders" ON public.orders FOR UPDATE USING (
(SELECT role FROM public.users WHERE id = auth.uid()) = 'admin'
OR company_id = (SELECT company_id FROM public.users WHERE id = auth.uid())
OR auth.uid() = user_id
);
-- Reload schema cache
NOTIFY pgrst, 'reload schema';