@vendure-platform/create 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +7 -7
  3. package/dist/add-to-existing.js +1 -583
  4. package/dist/assets/storefront/messages/de.json +2 -0
  5. package/dist/assets/storefront/messages/en.json +2 -0
  6. package/dist/assets/storefront/package.json +1 -1
  7. package/dist/assets/storefront/src/app/[locale]/(catalog)/product/[slug]/page.tsx +7 -2
  8. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/activity-feed.tsx +152 -0
  9. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/add-item-search.tsx +130 -0
  10. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/add-member-form.tsx +124 -0
  11. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/members-panel.tsx +240 -0
  12. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/page.tsx +47 -0
  13. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/rename-dialog.tsx +96 -0
  14. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/[id]/shared-cart-detail-client.tsx +555 -0
  15. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/actions.ts +260 -0
  16. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/new/new-shared-cart-form.tsx +77 -0
  17. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/new/page.tsx +47 -0
  18. package/dist/assets/storefront/src/app/[locale]/account/shared-carts/page.tsx +263 -0
  19. package/dist/assets/storefront/src/app/[locale]/cart/cart.tsx +12 -1
  20. package/dist/assets/storefront/src/app/[locale]/cart/order-summary.tsx +23 -0
  21. package/dist/assets/storefront/src/components/commerce/account-nav-items.ts +2 -0
  22. package/dist/assets/storefront/src/components/commerce/add-to-shared-cart.tsx +225 -0
  23. package/dist/assets/storefront/src/components/commerce/product-info.tsx +14 -1
  24. package/dist/assets/storefront/src/components/commerce/shared-cart-role.ts +39 -0
  25. package/dist/assets/storefront/src/components/commerce/shared-cart-status-badge.tsx +24 -0
  26. package/dist/assets/storefront/src/graphql-env.d.ts +28 -15
  27. package/dist/assets/storefront/src/lib/vendure/api.ts +8 -4
  28. package/dist/assets/storefront/src/lib/vendure/mutations.ts +96 -0
  29. package/dist/assets/storefront/src/lib/vendure/queries.ts +142 -0
  30. package/dist/assets/storefront/src/schema.graphql +321 -61
  31. package/dist/bin/run.js +1 -119
  32. package/dist/cli.js +1 -309
  33. package/dist/git-init.js +1 -60
  34. package/dist/index.js +1 -19
  35. package/dist/plugin-registry.js +1 -203
  36. package/dist/registry.js +1 -29
  37. package/dist/scaffold-shared.js +1 -124
  38. package/dist/scaffold-storefront-only.js +1 -101
  39. package/dist/scaffolder.js +1 -203
  40. package/dist/sort-package-json.js +1 -26
  41. package/dist/storefront-copier.js +1 -149
  42. package/dist/template-renderer.js +1 -221
  43. package/dist/types.js +1 -3
  44. package/package.json +5 -4
  45. package/dist/add-to-existing.js.map +0 -1
  46. package/dist/assets/storefront/src/lib/quantity.test.js +0 -20
  47. package/dist/bin/run.js.map +0 -1
  48. package/dist/cli.js.map +0 -1
  49. package/dist/git-init.js.map +0 -1
  50. package/dist/index.js.map +0 -1
  51. package/dist/plugin-registry.js.map +0 -1
  52. package/dist/registry.js.map +0 -1
  53. package/dist/scaffold-shared.js.map +0 -1
  54. package/dist/scaffold-storefront-only.js.map +0 -1
  55. package/dist/scaffolder.js.map +0 -1
  56. package/dist/sort-package-json.js.map +0 -1
  57. package/dist/storefront-copier.js.map +0 -1
  58. package/dist/template-renderer.js.map +0 -1
  59. package/dist/types.js.map +0 -1
@@ -0,0 +1,260 @@
1
+ 'use server';
2
+
3
+ import {client} from '@/lib/vendure/api';
4
+ import {
5
+ CreateSharedCartMutation,
6
+ UpdateSharedCartMutation,
7
+ DeleteSharedCartMutation,
8
+ AddItemToSharedCartMutation,
9
+ AdjustSharedCartLineMutation,
10
+ RemoveSharedCartLineMutation,
11
+ AddSharedCartMemberMutation,
12
+ RemoveSharedCartMemberMutation,
13
+ SetSharedCartMemberRoleMutation,
14
+ TransferSharedCartOwnershipMutation,
15
+ InitiateSharedCartCheckoutMutation,
16
+ } from '@/lib/vendure/mutations';
17
+ import {GetSharedCartQuery, GetMySharedCartsQuery, SearchVariantsQuery, GetShopCompanyUsersQuery} from '@/lib/vendure/queries';
18
+ import {CompanyUserFragment} from '@/lib/vendure/fragments';
19
+ import {getActiveCustomer} from '@/lib/vendure/actions';
20
+ import {roleAtLeast} from '@/components/commerce/shared-cart-role';
21
+ import {readFragment, type ResultOf} from '@/graphql';
22
+ import {revalidatePath, updateTag} from 'next/cache';
23
+
24
+ export type SharedCartRole = 'view' | 'edit' | 'checkout';
25
+ type ActionResult = {success: boolean; message?: string; id?: string};
26
+
27
+ function invalidateCart() {
28
+ updateTag('cart');
29
+ updateTag('active-order');
30
+ }
31
+
32
+ function revalidateCart(id: string) {
33
+ revalidatePath(`/account/shared-carts/${id}`);
34
+ revalidatePath('/account/shared-carts');
35
+ }
36
+
37
+ // --- Read (server-action wrapper for the client poll) ----------------------
38
+
39
+ export type SharedCartData = NonNullable<ResultOf<typeof GetSharedCartQuery>['sharedCart']>;
40
+
41
+ export async function fetchSharedCart(id: string): Promise<SharedCartData | null> {
42
+ const [result, error] = await client.query(GetSharedCartQuery, {id});
43
+ if (error) return null;
44
+ return result.data.sharedCart ?? null;
45
+ }
46
+
47
+ // --- Lifecycle -------------------------------------------------------------
48
+
49
+ export async function createSharedCart(name: string, description?: string): Promise<ActionResult> {
50
+ const [result, error] = await client.mutate(CreateSharedCartMutation, {
51
+ input: {name, description: description || undefined},
52
+ });
53
+ if (error) return {success: false, message: error.message};
54
+ revalidatePath('/account/shared-carts');
55
+ return {success: true, id: result.data.createSharedCart.id};
56
+ }
57
+
58
+ /**
59
+ * "Share my cart": create a blank shared cart, then copy the active order's
60
+ * lines into it. Non-atomic by design for v1 — if a line fails to copy we keep
61
+ * the cart and surface which step failed, so the user lands on a usable cart
62
+ * rather than losing the whole action.
63
+ */
64
+ export async function shareMyCart(
65
+ name: string,
66
+ description: string | undefined,
67
+ lines: Array<{productVariantId: string; quantity: number}>,
68
+ ): Promise<ActionResult> {
69
+ const created = await createSharedCart(name, description);
70
+ if (!created.success || !created.id) return created;
71
+ for (const line of lines) {
72
+ const [, err] = await client.mutate(AddItemToSharedCartMutation, {
73
+ sharedCartId: created.id,
74
+ productVariantId: line.productVariantId,
75
+ quantity: line.quantity,
76
+ });
77
+ if (err) {
78
+ return {success: false, message: `Cart created, but an item couldn't be copied: ${err.message}`, id: created.id};
79
+ }
80
+ }
81
+ revalidatePath('/account/shared-carts');
82
+ return {success: true, id: created.id};
83
+ }
84
+
85
+ export async function renameSharedCart(id: string, name: string, description?: string): Promise<ActionResult> {
86
+ const [, error] = await client.mutate(UpdateSharedCartMutation, {
87
+ input: {id, name, description: description || undefined},
88
+ });
89
+ if (error) return {success: false, message: error.message};
90
+ revalidateCart(id);
91
+ return {success: true};
92
+ }
93
+
94
+ export async function deleteSharedCart(id: string): Promise<ActionResult> {
95
+ const [, error] = await client.mutate(DeleteSharedCartMutation, {id});
96
+ if (error) return {success: false, message: error.message};
97
+ revalidatePath('/account/shared-carts');
98
+ return {success: true};
99
+ }
100
+
101
+ // --- Lines (role >= edit; server enforces) ---------------------------------
102
+
103
+ export async function addItemToSharedCart(sharedCartId: string, productVariantId: string, quantity: number): Promise<ActionResult> {
104
+ const [, error] = await client.mutate(AddItemToSharedCartMutation, {sharedCartId, productVariantId, quantity});
105
+ if (error) return {success: false, message: error.message};
106
+ revalidateCart(sharedCartId);
107
+ return {success: true};
108
+ }
109
+
110
+ /**
111
+ * Add several lines at once — powers the "Add to shared cart" picker when it
112
+ * carries a whole cart (the /cart page) rather than a single product. Non-atomic
113
+ * like {@link shareMyCart}: if a line fails we stop and report which step broke,
114
+ * leaving the earlier lines in place rather than rolling the cart back.
115
+ */
116
+ export async function addItemsToSharedCart(
117
+ sharedCartId: string,
118
+ lines: Array<{productVariantId: string; quantity: number}>,
119
+ ): Promise<ActionResult> {
120
+ for (const line of lines) {
121
+ const [, error] = await client.mutate(AddItemToSharedCartMutation, {
122
+ sharedCartId,
123
+ productVariantId: line.productVariantId,
124
+ quantity: line.quantity,
125
+ });
126
+ if (error) return {success: false, message: error.message};
127
+ }
128
+ revalidateCart(sharedCartId);
129
+ return {success: true};
130
+ }
131
+
132
+ export async function adjustSharedCartLine(sharedCartId: string, lineId: string, quantity: number): Promise<ActionResult> {
133
+ const [, error] = await client.mutate(AdjustSharedCartLineMutation, {sharedCartId, lineId, quantity});
134
+ if (error) return {success: false, message: error.message};
135
+ revalidateCart(sharedCartId);
136
+ return {success: true};
137
+ }
138
+
139
+ export async function removeSharedCartLine(sharedCartId: string, lineId: string): Promise<ActionResult> {
140
+ const [, error] = await client.mutate(RemoveSharedCartLineMutation, {sharedCartId, lineId});
141
+ if (error) return {success: false, message: error.message};
142
+ revalidateCart(sharedCartId);
143
+ return {success: true};
144
+ }
145
+
146
+ export type EditableSharedCart = {id: string; name: string};
147
+
148
+ /**
149
+ * Open carts the viewer can add items to (owner, or member with role >= edit).
150
+ * Powers the product-page "Add to shared cart" picker.
151
+ */
152
+ export async function fetchEditableOpenSharedCarts(): Promise<EditableSharedCart[]> {
153
+ const [customer, [result, error]] = await Promise.all([
154
+ getActiveCustomer(),
155
+ client.query(GetMySharedCartsQuery, {options: {take: 100, filter: {status: {eq: 'Open'}}}}),
156
+ ]);
157
+ if (!customer || error) return [];
158
+ return result.data.mySharedCarts.items
159
+ .filter(cart => {
160
+ if (cart.owner.id === customer.id) return true;
161
+ return roleAtLeast(cart.members.find(m => m.customer.id === customer.id)?.role, 'edit');
162
+ })
163
+ .map(cart => ({id: cart.id, name: cart.name}));
164
+ }
165
+
166
+ export type VariantSearchResult = {
167
+ productVariantId: string;
168
+ productName: string;
169
+ variantName: string;
170
+ sku: string;
171
+ imageUrl?: string;
172
+ price: number;
173
+ currencyCode: string;
174
+ inStock: boolean;
175
+ };
176
+
177
+ export async function searchVariantsForSharedCart(term: string): Promise<VariantSearchResult[]> {
178
+ const [result, error] = await client.query(SearchVariantsQuery, {
179
+ input: {term: term.trim(), take: 10, groupByProduct: false},
180
+ });
181
+ if (error) return [];
182
+ return result.data.search.items
183
+ .filter(item => item.price.__typename === 'SinglePrice')
184
+ .map(item => ({
185
+ productVariantId: item.productVariantId,
186
+ productName: item.productName,
187
+ variantName: item.productVariantName,
188
+ sku: item.sku,
189
+ imageUrl: item.productAsset?.preview ?? undefined,
190
+ price: (item.price as {__typename: 'SinglePrice'; value: number}).value,
191
+ currencyCode: item.currencyCode,
192
+ inStock: item.inStock,
193
+ }));
194
+ }
195
+
196
+ // --- Members & governance (owner OR company-admin; server enforces) --------
197
+
198
+ export type CompanyMemberOption = {
199
+ customerId: string;
200
+ firstName: string;
201
+ lastName: string;
202
+ emailAddress: string;
203
+ };
204
+
205
+ export async function fetchCompanyMembers(): Promise<CompanyMemberOption[]> {
206
+ const [result, error] = await client.query(GetShopCompanyUsersQuery, {});
207
+ if (error) return [];
208
+ return result.data.shopCompanyUsers
209
+ .map(user => readFragment(CompanyUserFragment, user))
210
+ .map(user => ({
211
+ customerId: user.customer.id,
212
+ firstName: user.customer.firstName,
213
+ lastName: user.customer.lastName,
214
+ emailAddress: user.customer.emailAddress,
215
+ }));
216
+ }
217
+
218
+
219
+ export async function addSharedCartMember(sharedCartId: string, customerId: string, role: SharedCartRole): Promise<ActionResult> {
220
+ const [, error] = await client.mutate(AddSharedCartMemberMutation, {input: {sharedCartId, customerId, role}});
221
+ if (error) return {success: false, message: error.message};
222
+ revalidateCart(sharedCartId);
223
+ return {success: true};
224
+ }
225
+
226
+ export async function removeSharedCartMember(sharedCartId: string, customerId: string): Promise<ActionResult> {
227
+ const [, error] = await client.mutate(RemoveSharedCartMemberMutation, {sharedCartId, customerId});
228
+ if (error) return {success: false, message: error.message};
229
+ revalidateCart(sharedCartId);
230
+ return {success: true};
231
+ }
232
+
233
+ export async function setSharedCartMemberRole(sharedCartId: string, customerId: string, role: SharedCartRole): Promise<ActionResult> {
234
+ const [, error] = await client.mutate(SetSharedCartMemberRoleMutation, {input: {sharedCartId, customerId, role}});
235
+ if (error) return {success: false, message: error.message};
236
+ revalidateCart(sharedCartId);
237
+ return {success: true};
238
+ }
239
+
240
+ export async function transferSharedCartOwnership(sharedCartId: string, newOwnerCustomerId: string): Promise<ActionResult> {
241
+ const [, error] = await client.mutate(TransferSharedCartOwnershipMutation, {sharedCartId, newOwnerCustomerId});
242
+ if (error) return {success: false, message: error.message};
243
+ revalidateCart(sharedCartId);
244
+ return {success: true};
245
+ }
246
+
247
+ // --- Checkout hand-off (role >= checkout; server enforces) -----------------
248
+
249
+ /**
250
+ * Lock the shared cart and adopt its order as the viewer's active order. The
251
+ * caller then sends the buyer to the normal /checkout flow; approval, if the
252
+ * company requires it, runs through the existing approval UI.
253
+ */
254
+ export async function initiateSharedCartCheckout(sharedCartId: string): Promise<ActionResult> {
255
+ const [, error] = await client.mutate(InitiateSharedCartCheckoutMutation, {sharedCartId});
256
+ if (error) return {success: false, message: error.message};
257
+ invalidateCart();
258
+ revalidateCart(sharedCartId);
259
+ return {success: true};
260
+ }
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+
3
+ import {useState, useTransition} from 'react';
4
+ import {useRouter} from '@/i18n/navigation';
5
+ import {toast} from 'sonner';
6
+ import {Button} from '@/components/ui/button';
7
+ import {Input} from '@/components/ui/input';
8
+ import {Textarea} from '@/components/ui/textarea';
9
+ import {Label} from '@/components/ui/label';
10
+ import {Loader2} from 'lucide-react';
11
+ import {createSharedCart} from '../actions';
12
+
13
+ interface NewSharedCartFormProps {
14
+ defaultName: string;
15
+ }
16
+
17
+ export function NewSharedCartForm({defaultName}: NewSharedCartFormProps) {
18
+ const router = useRouter();
19
+ const [name, setName] = useState(defaultName);
20
+ const [description, setDescription] = useState('');
21
+ const [isPending, startTransition] = useTransition();
22
+
23
+ const trimmed = name.trim();
24
+
25
+ function handleSubmit(e: React.FormEvent) {
26
+ e.preventDefault();
27
+ if (!trimmed) return;
28
+ startTransition(async () => {
29
+ const result = await createSharedCart(trimmed, description.trim() || undefined);
30
+ if (!result.success || !result.id) {
31
+ toast.error(result.message ?? 'Could not create the shared cart.');
32
+ return;
33
+ }
34
+ router.push(`/account/shared-carts/${result.id}`);
35
+ });
36
+ }
37
+
38
+ return (
39
+ <form onSubmit={handleSubmit} className="max-w-xl space-y-6">
40
+ <div className="space-y-2">
41
+ <Label htmlFor="shared-cart-name">Name</Label>
42
+ <Input
43
+ id="shared-cart-name"
44
+ value={name}
45
+ onChange={e => setName(e.target.value)}
46
+ placeholder="e.g. Q3 office supplies"
47
+ autoFocus
48
+ maxLength={255}
49
+ required
50
+ />
51
+ <p className="text-xs text-muted-foreground">Give the cart a name your colleagues will recognise.</p>
52
+ </div>
53
+
54
+ <div className="space-y-2">
55
+ <Label htmlFor="shared-cart-description">Description (optional)</Label>
56
+ <Textarea
57
+ id="shared-cart-description"
58
+ value={description}
59
+ onChange={e => setDescription(e.target.value)}
60
+ placeholder="What is this cart for?"
61
+ rows={3}
62
+ maxLength={1000}
63
+ />
64
+ </div>
65
+
66
+ <div className="flex items-center gap-3">
67
+ <Button type="submit" disabled={!trimmed || isPending}>
68
+ {isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
69
+ Create shared cart
70
+ </Button>
71
+ <Button type="button" variant="ghost" onClick={() => router.push('/account/shared-carts')} disabled={isPending}>
72
+ Cancel
73
+ </Button>
74
+ </div>
75
+ </form>
76
+ );
77
+ }
@@ -0,0 +1,47 @@
1
+ import type {Metadata} from 'next';
2
+ import {client} from '@/lib/vendure/api';
3
+ import {readFragment} from '@/graphql';
4
+ import {GetActiveCustomerQuery} from '@/lib/vendure/queries';
5
+ import {ActiveCustomerFragment} from '@/lib/vendure/fragments';
6
+ import {getCompanyAccount} from '@/lib/vendure/actions';
7
+ import {canCreateSharedCarts} from '@/components/commerce/shared-cart-role';
8
+ import {redirect, Link} from '@/i18n/navigation';
9
+ import {ArrowLeft} from 'lucide-react';
10
+ import {NewSharedCartForm} from './new-shared-cart-form';
11
+
12
+ export const metadata: Metadata = {
13
+ title: 'New Shared Cart',
14
+ };
15
+
16
+ export default async function NewSharedCartPage() {
17
+ const [[customerResult, customerError], companyAccount] = await Promise.all([
18
+ client.query(GetActiveCustomerQuery, {}),
19
+ getCompanyAccount(),
20
+ ]);
21
+ if (customerError || !customerResult.data.activeCustomer) {
22
+ return await redirect('/sign-in');
23
+ }
24
+
25
+ if (!canCreateSharedCarts(companyAccount?.permissions)) {
26
+ return await redirect('/account/shared-carts');
27
+ }
28
+ const customer = readFragment(ActiveCustomerFragment, customerResult.data.activeCustomer);
29
+ const defaultName = `${customer.firstName}'s shared cart`;
30
+
31
+ return (
32
+ <div>
33
+ <Link
34
+ href="/account/shared-carts"
35
+ className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
36
+ >
37
+ <ArrowLeft className="h-4 w-4" />
38
+ Shared carts
39
+ </Link>
40
+ <h1 className="text-3xl font-bold mb-1">New shared cart</h1>
41
+ <p className="text-sm text-muted-foreground mb-6">
42
+ Create an empty cart, then invite colleagues and add items.
43
+ </p>
44
+ <NewSharedCartForm defaultName={defaultName} />
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,263 @@
1
+ import type {Metadata} from 'next';
2
+ import {Suspense} from 'react';
3
+ import {client} from '@/lib/vendure/api';
4
+ import {GetMySharedCartsQuery, GetCompanySharedCartsQuery} from '@/lib/vendure/queries';
5
+ import {getActiveCustomer, getCompanyAccount} from '@/lib/vendure/actions';
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from '@/components/ui/table';
14
+ import {Pagination} from '@/components/shared/pagination';
15
+ import {Badge} from '@/components/ui/badge';
16
+ import {ErrorState} from '@/components/shared/error-state';
17
+ import {ArrowRight, Plus, Users, ShoppingCart} from 'lucide-react';
18
+ import {Button} from '@/components/ui/button';
19
+ import {Price} from '@/components/commerce/price';
20
+ import {SharedCartStatusBadge} from '@/components/commerce/shared-cart-status-badge';
21
+ import {roleLabel, canCreateSharedCarts, canManageSharedCarts} from '@/components/commerce/shared-cart-role';
22
+ import {formatDate} from '@/lib/format';
23
+ import {cn} from '@/lib/utils';
24
+ import {setRequestLocale} from 'next-intl/server';
25
+ import {Link, redirect} from '@/i18n/navigation';
26
+
27
+ export const metadata: Metadata = {
28
+ title: 'Shared Carts',
29
+ };
30
+
31
+ const ITEMS_PER_PAGE = 10;
32
+
33
+ export default function SharedCartsPage(props: {
34
+ params: Promise<{locale: string}>;
35
+ searchParams: Promise<Record<string, string | string[] | undefined>>;
36
+ }) {
37
+ return (
38
+ <Suspense
39
+ fallback={
40
+ <div>
41
+ <div className="flex items-center justify-between mb-6">
42
+ <h1 className="text-3xl font-bold">Shared Carts</h1>
43
+ </div>
44
+ <p className="text-sm text-muted-foreground">Loading shared carts…</p>
45
+ </div>
46
+ }
47
+ >
48
+ <SharedCartsContent params={props.params} searchParamsPromise={props.searchParams} />
49
+ </Suspense>
50
+ );
51
+ }
52
+
53
+ async function SharedCartsContent({
54
+ params,
55
+ searchParamsPromise,
56
+ }: {
57
+ params: Promise<{locale: string}>;
58
+ searchParamsPromise: Promise<Record<string, string | string[] | undefined>>;
59
+ }) {
60
+ const {locale} = await params;
61
+ setRequestLocale(locale);
62
+ const [searchParams, activeCustomer, companyAccount] = await Promise.all([
63
+ searchParamsPromise,
64
+ getActiveCustomer(),
65
+ getCompanyAccount(),
66
+ ]);
67
+ if (!activeCustomer) return await redirect('/sign-in');
68
+ const canCreate = canCreateSharedCarts(companyAccount?.permissions);
69
+ const isAdmin = canManageSharedCarts(companyAccount?.permissions);
70
+
71
+ // Company admins get a second "All company carts" view; everyone else only ever sees their own.
72
+ const scopeParam = searchParams.scope;
73
+ const scope =
74
+ isAdmin && (Array.isArray(scopeParam) ? scopeParam[0] : scopeParam) === 'company'
75
+ ? 'company'
76
+ : 'mine';
77
+
78
+ const pageParam = searchParams.page;
79
+ const parsedPage = parseInt(Array.isArray(pageParam) ? pageParam[0] : pageParam || '1', 10);
80
+ const currentPage = Number.isFinite(parsedPage) && parsedPage > 0 ? parsedPage : 1;
81
+ const skip = (currentPage - 1) * ITEMS_PER_PAGE;
82
+
83
+ const options = {take: ITEMS_PER_PAGE, skip, sort: {updatedAt: 'DESC' as never}};
84
+ const [result, error] =
85
+ scope === 'company'
86
+ ? await client.query(GetCompanySharedCartsQuery, {options})
87
+ : await client.query(GetMySharedCartsQuery, {options});
88
+
89
+ if (error) {
90
+ return (
91
+ <div>
92
+ <Header canCreate={canCreate} />
93
+ {isAdmin && <ScopeTabs scope={scope} />}
94
+ <ErrorState message="We couldn't load your shared carts. Please try again later." />
95
+ </div>
96
+ );
97
+ }
98
+
99
+ const list = 'companySharedCarts' in result.data ? result.data.companySharedCarts : result.data.mySharedCarts;
100
+ const carts = list.items;
101
+ const totalItems = list.totalItems;
102
+ const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
103
+
104
+ return (
105
+ <div>
106
+ <Header canCreate={canCreate} />
107
+ {isAdmin && <ScopeTabs scope={scope} />}
108
+
109
+ {carts.length === 0 ? (
110
+ <EmptyState canCreate={canCreate} scope={scope} />
111
+ ) : (
112
+ <div className="border rounded-lg overflow-hidden">
113
+ <Table>
114
+ <TableHeader className="bg-muted">
115
+ <TableRow>
116
+ <TableHead>Name</TableHead>
117
+ <TableHead>Status</TableHead>
118
+ <TableHead>Your role</TableHead>
119
+ <TableHead>Owner</TableHead>
120
+ <TableHead className="text-center">Members</TableHead>
121
+ <TableHead className="text-center">Items</TableHead>
122
+ <TableHead className="text-right">Total</TableHead>
123
+ <TableHead>Updated</TableHead>
124
+ <TableHead className="w-12" />
125
+ </TableRow>
126
+ </TableHeader>
127
+ <TableBody>
128
+ {carts.map(cart => {
129
+ const isOwner = cart.owner.id === activeCustomer.id;
130
+ const membership = cart.members.find(m => m.customer.id === activeCustomer.id);
131
+ const yourRole = isOwner ? 'owner' : membership?.role;
132
+ return (
133
+ <TableRow key={cart.id} className="hover:bg-muted/50">
134
+ <TableCell className="font-medium">
135
+ <Link
136
+ href={`/account/shared-carts/${cart.id}`}
137
+ className="hover:underline underline-offset-4"
138
+ >
139
+ {cart.name}
140
+ </Link>
141
+ </TableCell>
142
+ <TableCell>
143
+ <SharedCartStatusBadge status={cart.status} />
144
+ </TableCell>
145
+ <TableCell>
146
+ {yourRole ? (
147
+ <Badge variant={isOwner ? 'default' : 'outline'}>
148
+ {isOwner ? 'Owner' : roleLabel(yourRole)}
149
+ </Badge>
150
+ ) : (
151
+ <span className="text-muted-foreground">—</span>
152
+ )}
153
+ </TableCell>
154
+ <TableCell>
155
+ {cart.owner.firstName} {cart.owner.lastName}
156
+ </TableCell>
157
+ <TableCell className="text-center tabular-nums">{cart.members.length}</TableCell>
158
+ <TableCell className="text-center tabular-nums">{cart.order.totalQuantity}</TableCell>
159
+ <TableCell className="text-right">
160
+ <Price value={cart.order.totalWithTax} currencyCode={cart.order.currencyCode} taxLabel="incl" />
161
+ </TableCell>
162
+ <TableCell className="text-muted-foreground whitespace-nowrap">
163
+ {formatDate(cart.updatedAt, locale)}
164
+ </TableCell>
165
+ <TableCell>
166
+ <Button asChild variant="ghost" size="icon">
167
+ <Link href={`/account/shared-carts/${cart.id}`} aria-label={`Open ${cart.name}`}>
168
+ <ArrowRight className="h-4 w-4" />
169
+ </Link>
170
+ </Button>
171
+ </TableCell>
172
+ </TableRow>
173
+ );
174
+ })}
175
+ </TableBody>
176
+ </Table>
177
+ </div>
178
+ )}
179
+
180
+ {totalPages > 1 && (
181
+ <div className="mt-6">
182
+ <Pagination currentPage={currentPage} totalPages={totalPages} />
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ function Header({canCreate}: {canCreate: boolean}) {
190
+ return (
191
+ <div className="flex flex-wrap items-start justify-between gap-4 mb-6">
192
+ <div>
193
+ <h1 className="text-3xl font-bold">Shared Carts</h1>
194
+ <p className="text-sm text-muted-foreground mt-1 max-w-prose">
195
+ Build orders together with your colleagues. Add items, set who can edit, and hand off to
196
+ checkout when you&apos;re ready.
197
+ </p>
198
+ </div>
199
+ {canCreate && (
200
+ <Button asChild>
201
+ <Link href="/account/shared-carts/new">
202
+ <Plus className="h-4 w-4 mr-2" />
203
+ New shared cart
204
+ </Link>
205
+ </Button>
206
+ )}
207
+ </div>
208
+ );
209
+ }
210
+
211
+ // Segmented control letting a company admin switch between their own carts and every cart in the
212
+ // company. Rendered only for admins; the query and access check are enforced server-side.
213
+ function ScopeTabs({scope}: {scope: 'mine' | 'company'}) {
214
+ const tabClass = (active: boolean) =>
215
+ cn(
216
+ 'px-3 py-1.5 text-sm font-medium rounded-md transition-colors',
217
+ active ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground',
218
+ );
219
+ return (
220
+ <div className="mb-6 inline-flex rounded-lg border bg-muted p-1">
221
+ <Link href="/account/shared-carts" className={tabClass(scope === 'mine')}>
222
+ My carts
223
+ </Link>
224
+ <Link href="/account/shared-carts?scope=company" className={tabClass(scope === 'company')}>
225
+ All company carts
226
+ </Link>
227
+ </div>
228
+ );
229
+ }
230
+
231
+ function EmptyState({canCreate, scope}: {canCreate: boolean; scope: 'mine' | 'company'}) {
232
+ return (
233
+ <div className="border border-dashed rounded-lg py-16 px-6 text-center">
234
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
235
+ <Users className="h-6 w-6 text-muted-foreground" />
236
+ </div>
237
+ <h2 className="text-lg font-semibold">No shared carts yet</h2>
238
+ <p className="text-sm text-muted-foreground mt-1 mx-auto max-w-sm">
239
+ {scope === 'company'
240
+ ? 'No one in your company has created a shared cart yet.'
241
+ : canCreate
242
+ ? 'Start a cart your team can build together, or share your current cart from the cart page.'
243
+ : "You're not a member of any shared carts yet. A colleague can add you to one."}
244
+ </p>
245
+ {canCreate && (
246
+ <div className="mt-6 flex items-center justify-center gap-3">
247
+ <Button asChild>
248
+ <Link href="/account/shared-carts/new">
249
+ <Plus className="h-4 w-4 mr-2" />
250
+ New shared cart
251
+ </Link>
252
+ </Button>
253
+ <Button asChild variant="outline">
254
+ <Link href="/cart">
255
+ <ShoppingCart className="h-4 w-4 mr-2" />
256
+ Go to cart
257
+ </Link>
258
+ </Button>
259
+ </div>
260
+ )}
261
+ </div>
262
+ );
263
+ }
@@ -19,6 +19,11 @@ import {getScenarioConfig} from "@/config/scenario";
19
19
  // configurator BOMs) without forking the whole cart orchestration.
20
20
  const CartItems = getScenarioConfig().overrides?.cart?.CartItems ?? DefaultCartItems;
21
21
 
22
+ // Scenario-provided banner slot, rendered above the cart content. Used by
23
+ // b2b-wholesale to surface an active PunchOut session ("Send to Procurement"
24
+ // CTA). Null when the active scenario doesn't register one.
25
+ const ScenarioCartBanner = getScenarioConfig().overrides?.cart?.Banner ?? null;
26
+
22
27
  export async function Cart() {
23
28
  const [ctx, t] = await Promise.all([
24
29
  getVendureContext(),
@@ -67,7 +72,12 @@ async function CartCached({
67
72
 
68
73
  // Handle empty cart case
69
74
  if (!activeOrder || activeOrder.lines.length === 0) {
70
- return <CartItems activeOrder={null}/>;
75
+ return (
76
+ <div className="space-y-6">
77
+ {ScenarioCartBanner && <ScenarioCartBanner />}
78
+ <CartItems activeOrder={null} />
79
+ </div>
80
+ );
71
81
  }
72
82
 
73
83
  // Order is awaiting approval — show a message instead of editable cart
@@ -115,6 +125,7 @@ async function CartCached({
115
125
 
116
126
  return (
117
127
  <div className="space-y-6">
128
+ {ScenarioCartBanner && <ScenarioCartBanner />}
118
129
  {wasApproved && (
119
130
  <p className="text-sm text-muted-foreground">
120
131
  {approvedOrderWarning}