Przejdź do głównej zawartości

Konto klienta

Kompletny przewodnik po zarządzaniu kontem klienta w storefront: profil, adresy, historia zamówień.

Dostepne hooki

Zapytania

HookTypOpis
fetchCustomerQuery (server)Pobiera profil klienta. Czyta cookie customerAccessToken automatycznie (lub przyjmuje token jako argument). Client: useCustomer.

Metody AuthClient (plain async, bez React Query)

MetodaReturnsDescription
getCustomer()Promise<Customer | null>Profil zalogowanego klienta. null gdy brak sesji (brak Bearer token / cookie).
getAddresses()Promise<MailingAddress[] | null>Pełna lista zapisanych adresów z address booka — taxId, vatNumber, pickupPoint, isDefault. null gdy brak auth context (symetrycznie z getCustomer). Użyj w address picker UI dla zalogowanego klienta.

Mutacje profilu

HookTypOpis
useCustomerUpdateMutationAktualizacja imienia, nazwiska, telefonu

Mutacje adresow

HookTypOpis
useCustomerAddressCreateMutationDodanie nowego adresu
useCustomerAddressUpdateMutationPełna podmiana istniejącego adresu — wyślij komplet pól (to nie jest częściowa łatka).
useCustomerAddressDeleteMutationUsuniecie adresu
useCustomerDefaultAddressUpdateMutationUstawienie adresu domyslnego
Adres PL wymaga numeru budynku

useCustomerAddressCreate i useCustomerAddressUpdate przyjmują pełny adres. Dla adresu polskiego (country: PL) bez punktu odbioru pole buildingNumber jest wymagane — numer budynku jest potrzebny do doręczenia przez kuriera oraz na fakturze. Jego brak zwraca userErrors[{ code: 'BUILDING_NUMBER_REQUIRED', field: ['address', 'buildingNumber'] }].

useCustomerAddressUpdate to pełna podmiana adresu (a nie częściowa łatka) — wyślij komplet pól (również buildingNumber dla adresu PL), ponieważ wartości nieprzysłane zostaną nadpisane.

Import

// Server Components (helpery async — fetch*, czytają cookie customerAccessToken)
import {
fetchCustomer,
} from '@/lib/graphql/server';

// Client Components (SDK hooks — use*)
import {
useCustomer,
useCustomerUpdate,
useCustomerAddressCreate,
useCustomerAddressUpdate,
useCustomerAddressDelete,
useCustomerDefaultAddressUpdate,
} from '@/lib/graphql/hooks';

Wywołanie w kontekście (Server / Client / Raw)

import { fetchCustomer } from '@/lib/graphql/server';

const data = await fetchCustomer();
Alternatywny wzorzec Client Components

Przyklady kodu w tej sekcji uzywaja niskopoziomowego wzorca z useExecute() hook + useMutation z @tanstack/react-query. Hook useExecute() pochodzi z @/lib/graphql/client i czyta StorefrontClient z SDK Context (middleware pipeline z auth, currency itp.). Mozna rowniez uzyc SDK hookow mutacji (np. useCustomerUpdate()) bezposrednio — obie metody sa rownowazne.

Typy GraphQL

Typy generowane ze schematu (SDK 15.0+)

Publiczne typy GraphQL eksportowane z @doswiftly/storefront-sdk (Customer, Order, MailingAddress itd.) są generowane ze schematu GraphQL przez pnpm codegen. Pola nullable i typy enum poniżej odzwierciedlają faktyczny kontrakt schematu.

Customer

interface Customer {
id: string;
email: string;
firstName?: string | null;
lastName?: string | null;
displayName: string; // firstName + lastName lub email (non-nullable)
phone?: string | null;
isEmailVerified: boolean;
emailMarketing: EmailMarketingState; // typed enum (poprzednio acceptsMarketing: boolean)
defaultAddress?: MailingAddress | null;
orderCount: string; // UnsignedInt64 — JSON-serializowany jako string
totalSpent: Money; // { amount, currencyCode } (non-nullable)
createdAt: string;
updatedAt: string;
}

Pola addresses (lista adresów) i orders (paginowana lista zamówień) nie są częścią domyślnego fragmentu Customer w SDK — pobierasz je jawnie we własnym zapytaniu jako pola customer { addresses { ... } orders(first: 20) { nodes { ... } pageInfo { ... } totalCount } }.

MailingAddress

interface MailingAddress {
id: string;
firstName?: string | null;
lastName?: string | null;
name?: string | null; // sformatowane imię i nazwisko
company?: string | null;
streetLine1?: string | null; // ulica i numer
streetLine2?: string | null; // mieszkanie, pietro
buildingNumber?: string | null; // numer budynku jako osobne pole (nakladka na streetLine1)
flatNumber?: string | null; // numer mieszkania jako osobne pole (towarzysz buildingNumber)
city?: string | null;
state?: string | null; // wojewodztwo/stan
stateCode?: string | null; // kod wojewodztwa
country?: string | null; // kraj w zapisanej postaci (zwykle kod); do logiki uzywaj countryCode
countryCode?: CountryCode | null; // ISO 3166-1 alpha-2 (np. "PL"); "ZZ" = zapisany kraj nierozpoznany, oryginal w `country`
postalCode?: string | null; // kod pocztowy
phone?: string | null;
isDefault: boolean; // czy to adres domyslny
}
Międzynarodowe nazewnictwo adresów (od API 5.0)

Pola adresowe używają neutralnej terminologii: streetLine1/streetLine2/state/stateCode/postalCode (poprzednio address1/address2/province/provinceCode/zip). Szczegóły: Migracja do Storefront API 5.0.

Order

interface Order {
id: string;
orderNumber: string; // np. "#1001"
accessToken: string; // opaque per-order token (klucz do OrderByToken query)
status: StorefrontOrderStatus; // typed enum lifecycle zamówienia
paymentStatus: OrderPaymentStatus; // typed enum (poprzednio financialStatus: string)
fulfillmentStatus: OrderFulfillmentStatus; // typed enum
processedAt: string; // data zamowienia
totals: { // OrderTotals — zagniezdzony wrapper kosztow
total: Money;
subtotal: Money;
totalTax?: Money | null;
totalShipping?: Money | null;
};
shippingAddress?: MailingAddress | null;
itemCount: number; // liczba pozycji (poprzednio lineItemsCount)
canCreatePayment: boolean;
paymentMethodType: PaymentMethodType;
}
Zagnieżdżone totals + typed enumy statusów (od API 5.0)

Order używa zagnieżdżonego wrappera totals zamiast płaskich totalPrice/subtotalPrice/totalTax/totalShipping, a pola statusów są typed enumami. Szczegóły: Migracja do Storefront API 5.0. Więcej o polu accessToken i guest order summary: Zamówienia.

Zamówienia z konfigurowalnymi produktami

Jeśli klient skonfigurował produkt w koszyku (np. drukarka z finiszerem/podstawą/podajnikiem), w historii zamówień lineItems ma strukturę parent + N children połączonych przez parentOrderItemId. Storefront powinien grupować pozycje aby pokazać "Drukarka + 3 komponenty" jako jeden blok.

Konsumpcja w checkoucie: SDK — Checkout.

MailingAddressInput

interface MailingAddressInput {
firstName?: string;
lastName?: string;
company?: string;
streetLine1?: string; // ulica i numer
streetLine2?: string; // mieszkanie, pietro
buildingNumber?: string; // numer budynku jako osobne pole (nakladka na streetLine1) — wymagany dla adresu PL bez punktu odbioru
flatNumber?: string; // numer mieszkania jako osobne pole (opcjonalny towarzysz buildingNumber)
city?: string;
state?: string; // wojewodztwo/stan
country?: CountryCode; // ISO 3166-1 alpha-2 (np. "PL")
postalCode?: string; // kod pocztowy
phone?: string;
taxId?: string; // NIP per-adres (B2B — dane firmowe per adres dostawy)
vatNumber?: string; // numer VAT UE per-adres (B2B cross-border)
}

CustomerUpdateInput

interface CustomerUpdateInput {
firstName?: string;
lastName?: string;
phone?: string;
acceptsMarketing?: boolean; // true → SUBSCRIBED, false → UNSUBSCRIBED, brak → bez zmiany
// Pola B2B (gdy klient ma konto firmowe):
customerType?: 'INDIVIDUAL' | 'COMPANY';
companyName?: string; // wymagane gdy customerType = COMPANY
taxId?: string; // NIP (10 cyfr)
vatNumber?: string; // numer VAT UE (np. PL1234567890)
regon?: string; // REGON (9 lub 14 cyfr)
}

Przyklady kodu

Strona konta (Server Component)

// app/account/page.tsx
import { cookies } from 'next/headers';
import { fetchCustomer } from '@/lib/graphql/server';
import { redirect } from 'next/navigation';

export default async function AccountPage() {
const cookieStore = await cookies();
const token = cookieStore.get('customerAccessToken')?.value;

if (!token) {
redirect('/auth/login?redirect=/account');
}

// fetchCustomer czyta cookie `customerAccessToken` automatycznie (Bearer w nagłówku)
const data = await fetchCustomer();
const customer = data.customer;

return (
<div className="space-y-8">
{/* Powitanie */}
<div>
<h1 className="text-3xl font-bold">
Witaj, {customer.displayName}
</h1>
<p className="text-muted-foreground">{customer.email}</p>
</div>

{/* Podsumowanie */}
<div className="grid grid-cols-3 gap-4">
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">Zamowienia</p>
<p className="text-2xl font-bold">{customer.orderCount}</p>
</div>
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">Laczne wydatki</p>
<p className="text-2xl font-bold">
{customer.totalSpent.amount} {customer.totalSpent.currencyCode}
</p>
</div>
<div className="border rounded-lg p-4">
<p className="text-sm text-muted-foreground">Adres domyslny</p>
<p className="text-sm">
{customer.defaultAddress
? `${customer.defaultAddress.city}, ${customer.defaultAddress.country}`
: 'Brak'}
</p>
</div>
</div>

{/* Szybkie linki */}
<div className="grid grid-cols-2 gap-4">
<a href="/account/orders" className="border rounded-lg p-6 hover:border-primary">
<h2 className="font-semibold">Historia zamowien</h2>
<p className="text-sm text-muted-foreground">
Przegladaj swoje zamowienia i sledz przesylki
</p>
</a>
<a href="/account/addresses" className="border rounded-lg p-6 hover:border-primary">
<h2 className="font-semibold">Adresy</h2>
<p className="text-sm text-muted-foreground">
Zarzadzaj adresami wysylki i rozliczen
</p>
</a>
<a href="/account/settings" className="border rounded-lg p-6 hover:border-primary">
<h2 className="font-semibold">Ustawienia konta</h2>
<p className="text-sm text-muted-foreground">
Edytuj dane osobowe i preferencje
</p>
</a>
</div>
</div>
);
}

Edycja profilu (Client Component)

'use client';

import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useExecute } from '@/lib/graphql/client';
import { CustomerUpdateDocument, type CustomerUpdateMutation } from '@/generated/graphql';
import { getAuthToken } from '@/lib/auth/cookies';
import { toast } from 'sonner';

interface ProfileFormProps {
customer: {
firstName?: string;
lastName?: string;
phone?: string;
};
}

export function ProfileForm({ customer }: ProfileFormProps) {
const queryClient = useQueryClient();
const execute = useExecute();

const [form, setForm] = useState({
firstName: customer.firstName || '',
lastName: customer.lastName || '',
phone: customer.phone || '',
});

const updateMutation = useMutation({
mutationFn: async () => {
const token = getAuthToken();
if (!token) throw new Error('Nie jestes zalogowany');

return execute<CustomerUpdateMutation>(
CustomerUpdateDocument.toString(),
{
customer: {
firstName: form.firstName,
lastName: form.lastName,
phone: form.phone || undefined,
},
customerAccessToken: token,
},
);
},
onSuccess: (data) => {
const errors = data.customerUpdate.userErrors;
if (errors?.length > 0) {
toast.error(errors[0].message);
return;
}
toast.success('Profil zaktualizowany');
queryClient.invalidateQueries({ queryKey: ['Customer'] });
},
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
updateMutation.mutate();
};

return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-lg">
<h2 className="text-xl font-bold">Dane osobowe</h2>

<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Imie</label>
<input
type="text"
value={form.firstName}
onChange={(e) => setForm({ ...form, firstName: e.target.value })}
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Nazwisko</label>
<input
type="text"
value={form.lastName}
onChange={(e) => setForm({ ...form, lastName: e.target.value })}
className="w-full border rounded px-3 py-2"
/>
</div>
</div>

<div>
<label className="block text-sm font-medium mb-1">Telefon</label>
<input
type="tel"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="+48 123 456 789"
className="w-full border rounded px-3 py-2"
/>
</div>

<button
type="submit"
disabled={updateMutation.isPending}
className="bg-primary text-white px-6 py-2 rounded-lg disabled:opacity-50"
>
{updateMutation.isPending ? 'Zapisywanie...' : 'Zapisz zmiany'}
</button>
</form>
);
}

Zarzadzanie adresami

'use client';

import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useExecute } from '@/lib/graphql/client';
import {
CustomerAddressCreateDocument,
CustomerAddressUpdateDocument,
CustomerAddressDeleteDocument,
CustomerDefaultAddressUpdateDocument,
type CustomerAddressCreateMutation,
type CustomerAddressUpdateMutation,
type CustomerAddressDeleteMutation,
type CustomerDefaultAddressUpdateMutation,
} from '@/generated/graphql';
import { getAuthToken } from '@/lib/auth/cookies';
import { toast } from 'sonner';

interface AddressManagerProps {
addresses: MailingAddress[];
}

export function AddressManager({ addresses }: AddressManagerProps) {
const queryClient = useQueryClient();
const execute = useExecute();
const [editingId, setEditingId] = useState<string | null>(null);
const [showForm, setShowForm] = useState(false);

// Dodaj nowy adres
const createMutation = useMutation({
mutationFn: async (address: MailingAddressInput) => {
const token = getAuthToken();
if (!token) throw new Error('Nie jestes zalogowany');

return execute<CustomerAddressCreateMutation>(
CustomerAddressCreateDocument.toString(),
{ address, customerAccessToken: token },
);
},
onSuccess: (data) => {
const errors = data.customerAddressCreate.userErrors;
if (errors?.length > 0) {
toast.error(errors[0].message);
return;
}
toast.success('Adres dodany');
setShowForm(false);
queryClient.invalidateQueries({ queryKey: ['Customer'] });
},
});

// Aktualizuj adres
const updateMutation = useMutation({
mutationFn: async ({ id, address }: { id: string; address: MailingAddressInput }) => {
const token = getAuthToken();
if (!token) throw new Error('Nie jestes zalogowany');

return execute<CustomerAddressUpdateMutation>(
CustomerAddressUpdateDocument.toString(),
{ id, address, customerAccessToken: token },
);
},
onSuccess: (data) => {
const errors = data.customerAddressUpdate.userErrors;
if (errors?.length > 0) {
toast.error(errors[0].message);
return;
}
toast.success('Adres zaktualizowany');
setEditingId(null);
queryClient.invalidateQueries({ queryKey: ['Customer'] });
},
});

// Usun adres
const deleteMutation = useMutation({
mutationFn: async (addressId: string) => {
const token = getAuthToken();
if (!token) throw new Error('Nie jestes zalogowany');

return execute<CustomerAddressDeleteMutation>(
CustomerAddressDeleteDocument.toString(),
{ id: addressId, customerAccessToken: token },
);
},
onSuccess: (data) => {
const errors = data.customerAddressDelete.userErrors;
if (errors?.length > 0) {
toast.error(errors[0].message);
return;
}
toast.success('Adres usuniety');
queryClient.invalidateQueries({ queryKey: ['Customer'] });
},
});

// Ustaw domyslny
const setDefaultMutation = useMutation({
mutationFn: async (addressId: string) => {
const token = getAuthToken();
if (!token) throw new Error('Nie jestes zalogowany');

return execute<CustomerDefaultAddressUpdateMutation>(
CustomerDefaultAddressUpdateDocument.toString(),
{ addressId, customerAccessToken: token },
);
},
onSuccess: (data) => {
const errors = data.customerDefaultAddressUpdate.userErrors;
if (errors?.length > 0) {
toast.error(errors[0].message);
return;
}
toast.success('Adres domyslny zmieniony');
queryClient.invalidateQueries({ queryKey: ['Customer'] });
},
});

return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold">Twoje adresy</h2>
<button
onClick={() => setShowForm(true)}
className="bg-primary text-white px-4 py-2 rounded-lg"
>
Dodaj adres
</button>
</div>

{/* Formularz nowego adresu */}
{showForm && (
<AddressForm
onSubmit={(address) => createMutation.mutate(address)}
onCancel={() => setShowForm(false)}
isLoading={createMutation.isPending}
/>
)}

{/* Lista adresow */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{addresses.map((address) => (
<div key={address.id} className="border rounded-lg p-4 relative">
{/* Badge domyslny */}
{address.isDefault && (
<span className="absolute top-2 right-2 text-xs bg-primary/10 text-primary px-2 py-1 rounded">
Domyslny
</span>
)}

{editingId === address.id ? (
<AddressForm
initialData={address}
onSubmit={(data) =>
updateMutation.mutate({ id: address.id, address: data })
}
onCancel={() => setEditingId(null)}
isLoading={updateMutation.isPending}
/>
) : (
<>
{/* Dane adresu */}
<p className="font-medium">
{address.firstName} {address.lastName}
</p>
{address.company && (
<p className="text-sm text-muted-foreground">{address.company}</p>
)}
<p className="text-sm">{address.streetLine1}</p>
{address.streetLine2 && (
<p className="text-sm">{address.streetLine2}</p>
)}
<p className="text-sm">
{address.postalCode} {address.city}
</p>
<p className="text-sm">{address.country}</p>
{address.phone && (
<p className="text-sm text-muted-foreground">{address.phone}</p>
)}

{/* Akcje */}
<div className="flex gap-3 mt-3 pt-3 border-t">
<button
onClick={() => setEditingId(address.id)}
className="text-sm text-primary hover:underline"
>
Edytuj
</button>
{!address.isDefault && (
<button
onClick={() => setDefaultMutation.mutate(address.id)}
className="text-sm text-primary hover:underline"
>
Ustaw jako domyslny
</button>
)}
<button
onClick={() => {
if (confirm('Czy na pewno chcesz usunac ten adres?')) {
deleteMutation.mutate(address.id);
}
}}
className="text-sm text-destructive hover:underline"
>
Usun
</button>
</div>
</>
)}
</div>
))}
</div>

{addresses.length === 0 && (
<p className="text-center text-muted-foreground py-8">
Nie masz jeszcze zadnych adresow.
</p>
)}
</div>
);
}

Formularz adresu (podkomponent)

interface AddressFormProps {
initialData?: Partial<MailingAddress>;
onSubmit: (address: MailingAddressInput) => void;
onCancel: () => void;
isLoading: boolean;
}

function AddressForm({ initialData, onSubmit, onCancel, isLoading }: AddressFormProps) {
const [form, setForm] = useState({
firstName: initialData?.firstName || '',
lastName: initialData?.lastName || '',
company: initialData?.company || '',
streetLine1: initialData?.streetLine1 || '',
streetLine2: initialData?.streetLine2 || '',
city: initialData?.city || '',
postalCode: initialData?.postalCode || '',
// ZZ = zapisany kraj nierozpoznany — nie podstawiaj go do formularza, kupujacy wybiera kraj ponownie
countryCode: initialData?.countryCode && initialData.countryCode !== 'ZZ' ? initialData.countryCode : 'PL',
state: initialData?.state || '',
phone: initialData?.phone || '',
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({
firstName: form.firstName,
lastName: form.lastName,
company: form.company || undefined,
streetLine1: form.streetLine1,
streetLine2: form.streetLine2 || undefined,
city: form.city,
postalCode: form.postalCode,
country: form.countryCode as MailingAddressInput['country'], // pole wejsciowe nazywa sie `country`
state: form.state || undefined,
phone: form.phone || undefined,
});
};

return (
<form onSubmit={handleSubmit} className="space-y-4 border rounded-lg p-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Imie</label>
<input
type="text"
value={form.firstName}
onChange={(e) => setForm({ ...form, firstName: e.target.value })}
required
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Nazwisko</label>
<input
type="text"
value={form.lastName}
onChange={(e) => setForm({ ...form, lastName: e.target.value })}
required
className="w-full border rounded px-3 py-2"
/>
</div>
</div>

<div>
<label className="block text-sm font-medium mb-1">Firma (opcjonalnie)</label>
<input
type="text"
value={form.company}
onChange={(e) => setForm({ ...form, company: e.target.value })}
className="w-full border rounded px-3 py-2"
/>
</div>

<div>
<label className="block text-sm font-medium mb-1">Ulica i numer</label>
<input
type="text"
value={form.streetLine1}
onChange={(e) => setForm({ ...form, streetLine1: e.target.value })}
required
className="w-full border rounded px-3 py-2"
/>
</div>

<div>
<label className="block text-sm font-medium mb-1">Mieszkanie, pietro (opcjonalnie)</label>
<input
type="text"
value={form.streetLine2}
onChange={(e) => setForm({ ...form, streetLine2: e.target.value })}
className="w-full border rounded px-3 py-2"
/>
</div>

<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Kod pocztowy</label>
<input
type="text"
value={form.postalCode}
onChange={(e) => setForm({ ...form, postalCode: e.target.value })}
required
placeholder="00-000"
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Miasto</label>
<input
type="text"
value={form.city}
onChange={(e) => setForm({ ...form, city: e.target.value })}
required
className="w-full border rounded px-3 py-2"
/>
</div>
</div>

<div>
<label className="block text-sm font-medium mb-1">Kraj</label>
<select
value={form.countryCode}
onChange={(e) => setForm({ ...form, countryCode: e.target.value })}
className="w-full border rounded px-3 py-2"
>
<option value="PL">Polska</option>
<option value="DE">Niemcy</option>
<option value="CZ">Czechy</option>
<option value="SK">Slowacja</option>
</select>
</div>

<div>
<label className="block text-sm font-medium mb-1">Telefon (opcjonalnie)</label>
<input
type="tel"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="+48 123 456 789"
className="w-full border rounded px-3 py-2"
/>
</div>

<div className="flex gap-3">
<button
type="submit"
disabled={isLoading}
className="bg-primary text-white px-6 py-2 rounded-lg disabled:opacity-50"
>
{isLoading ? 'Zapisywanie...' : 'Zapisz adres'}
</button>
<button
type="button"
onClick={onCancel}
className="border px-6 py-2 rounded-lg"
>
Anuluj
</button>
</div>
</form>
);
}

Lista zamowien

// app/account/orders/page.tsx
import { cookies } from 'next/headers';
import { fetchCustomer } from '@/lib/graphql/server';
import { redirect } from 'next/navigation';

export default async function OrdersPage() {
const cookieStore = await cookies();
const token = cookieStore.get('customerAccessToken')?.value;

if (!token) {
redirect('/auth/login?redirect=/account/orders');
}

// `orders` nie jest w domyślnym fragmencie Customer (patrz wyżej) — pobierz je własnym
// zapytaniem (raw) zawierającym `customer { orders(first: 20) { edges { node { ... } } } }`.
const data = await fetchCustomer();
const orders = data.customer.orders.edges.map((e) => e.node);

return (
<div>
<h1 className="text-2xl font-bold mb-6">Historia zamowien</h1>

{orders.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground mb-4">
Nie masz jeszcze zadnych zamowien.
</p>
<a href="/products" className="text-primary hover:underline">
Przegladaj produkty
</a>
</div>
) : (
<div className="space-y-4">
{orders.map((order) => (
<a
key={order.id}
href={`/account/orders/${order.id}`}
className="block border rounded-lg p-4 hover:border-primary transition-colors"
>
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-lg">{order.orderNumber}</span>
<span className="text-sm text-muted-foreground">
{new Date(order.processedAt).toLocaleDateString('pl-PL')}
</span>
</div>

<div className="flex items-center gap-4">
{/* Status platnosci */}
<StatusBadge
status={order.paymentStatus}
map={{
PAID: { label: 'Oplacone', color: 'green' },
PENDING: { label: 'Oczekuje', color: 'yellow' },
REFUNDED: { label: 'Zwrocone', color: 'red' },
PARTIALLY_REFUNDED: { label: 'Czesciowy zwrot', color: 'orange' },
}}
/>

{/* Status realizacji */}
<StatusBadge
status={order.fulfillmentStatus}
map={{
FULFILLED: { label: 'Zrealizowane', color: 'green' },
UNFULFILLED: { label: 'W realizacji', color: 'yellow' },
PARTIALLY_FULFILLED: { label: 'Czesciowo zrealizowane', color: 'orange' },
}}
/>

{/* Kwota */}
<span className="ml-auto font-semibold">
{order.totals.total.amount} {order.totals.total.currencyCode}
</span>
</div>

{/* Adres wysylki */}
{order.shippingAddress && (
<p className="text-sm text-muted-foreground mt-2">
Wysylka: {order.shippingAddress.city}, {order.shippingAddress.country}
</p>
)}

{/* Liczba pozycji */}
<p className="text-sm text-muted-foreground">
{order.itemCount} {order.itemCount === 1 ? 'pozycja' : 'pozycji'}
</p>
</a>
))}
</div>
)}

{/* Paginacja */}
{data.customer.orders.pageInfo.hasNextPage && (
<div className="text-center mt-6">
<p className="text-sm text-muted-foreground">
Wyswietlono {orders.length} z {data.customer.orders.totalCount} zamowien
</p>
</div>
)}
</div>
);
}

// Komponent badge statusu
function StatusBadge({
status,
map,
}: {
status: string;
map: Record<string, { label: string; color: string }>;
}) {
const config = map[status] || { label: status, color: 'gray' };
const colorClasses: Record<string, string> = {
green: 'bg-green-100 text-green-800',
yellow: 'bg-yellow-100 text-yellow-800',
red: 'bg-red-100 text-red-800',
orange: 'bg-orange-100 text-orange-800',
gray: 'bg-gray-100 text-gray-800',
};

return (
<span className={`text-xs px-2 py-1 rounded ${colorClasses[config.color]}`}>
{config.label}
</span>
);
}

Wzorzec Server vs Client

Dane konta klienta najlepiej pobierac w Server Components (SSR):

OperacjaTyp komponentuHook/Metoda
Pobieranie profiluServerfetchCustomer (z server.ts)
Pobieranie zamowienServerwłasne zapytanie z customer { orders } (raw — poza domyślnym fragmentem)
Pobieranie adresowServerwłasne zapytanie z customer { addresses } (raw — poza domyślnym fragmentem)
Edycja profiluClientCustomerUpdateDocument + useMutation
Dodawanie adresuClientCustomerAddressCreateDocument + useMutation
Aktualizacja adresuClientCustomerAddressUpdateDocument + useMutation
Usuniecie adresuClientCustomerAddressDeleteDocument + useMutation
Zmiana domyslnegoClientCustomerDefaultAddressUpdateDocument + useMutation

Dane sa pobierane na serwerze (SSR/RSC), a mutacje wykonywane z poziomu Client Components z uzyciem useMutation z @tanstack/react-query.