feat: assign orders to companies directly and support admin reassignment
All checks were successful
Staging Build / build (push) Successful in 3m36s
All checks were successful
Staging Build / build (push) Successful in 3m36s
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
Reference in New Issue
Block a user