includio-cms 0.23.0 → 0.24.1
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.
- package/API.md +29 -6
- package/CHANGELOG.md +116 -0
- package/DOCS.md +80 -5
- package/ROADMAP.md +2 -0
- package/dist/admin/client/index.d.ts +3 -0
- package/dist/admin/client/index.js +3 -0
- package/dist/admin/client/shop/coupon-edit-page.svelte +44 -0
- package/dist/admin/client/shop/coupon-edit-page.svelte.d.ts +3 -0
- package/dist/admin/client/shop/coupon-form.svelte +170 -0
- package/dist/admin/client/shop/coupon-form.svelte.d.ts +18 -0
- package/dist/admin/client/shop/coupon-new-page.svelte +25 -0
- package/dist/admin/client/shop/coupon-new-page.svelte.d.ts +18 -0
- package/dist/admin/client/shop/coupons-list-page.svelte +135 -0
- package/dist/admin/client/shop/coupons-list-page.svelte.d.ts +3 -0
- package/dist/admin/client/shop/refund-dialog.svelte +161 -0
- package/dist/admin/client/shop/refund-dialog.svelte.d.ts +11 -0
- package/dist/admin/client/shop/shipping-method-edit-page.svelte +3 -6
- package/dist/admin/client/shop/shipping-method-form.svelte +15 -21
- package/dist/admin/client/shop/shipping-method-new-page.svelte +3 -6
- package/dist/admin/client/shop/shipping-methods-list-page.svelte +6 -6
- package/dist/admin/client/shop/shop-order-detail-page.svelte +107 -27
- package/dist/admin/client/shop/shop-orders-list-page.svelte +49 -11
- package/dist/admin/client/shop/shop-products-list-page.svelte +12 -11
- package/dist/admin/components/layout/lang.d.ts +1 -0
- package/dist/admin/components/layout/lang.js +4 -2
- package/dist/admin/components/layout/layout-renderer.svelte +12 -11
- package/dist/admin/components/layout/nav-breadcrumbs.svelte +3 -5
- package/dist/admin/components/layout/nav-shop.svelte +3 -1
- package/dist/admin/components/layout/nav-user.svelte +6 -4
- package/dist/admin/components/layout/site-header.svelte +11 -5
- package/dist/admin/remote/shop.remote.d.ts +122 -3
- package/dist/admin/remote/shop.remote.js +161 -5
- package/dist/core/server/entries/operations/get.bench.d.ts +1 -0
- package/dist/core/server/entries/operations/get.bench.js +68 -0
- package/dist/core/server/entries/operations/get.js +17 -7
- package/dist/core/server/fields/utils/imageStyles.bench.d.ts +1 -0
- package/dist/core/server/fields/utils/imageStyles.bench.js +82 -0
- package/dist/core/server/fields/utils/imageStyles.js +49 -53
- package/dist/core/server/media/operations/backgroundMaintenance.d.ts +6 -0
- package/dist/core/server/media/operations/backgroundMaintenance.js +6 -1
- package/dist/core/server/media/styles/operations/getImageStyle.d.ts +7 -0
- package/dist/core/server/media/styles/operations/getImageStyle.js +24 -0
- package/dist/db-postgres/index.d.ts +1 -1
- package/dist/db-postgres/index.js +27 -0
- package/dist/db-postgres/schema/shop/couponRedemptions.d.ts +97 -0
- package/dist/db-postgres/schema/shop/couponRedemptions.js +21 -0
- package/dist/db-postgres/schema/shop/coupons.d.ts +197 -0
- package/dist/db-postgres/schema/shop/coupons.js +18 -0
- package/dist/db-postgres/schema/shop/index.d.ts +4 -0
- package/dist/db-postgres/schema/shop/index.js +4 -0
- package/dist/db-postgres/schema/shop/product.d.ts +17 -0
- package/dist/db-postgres/schema/shop/product.js +2 -0
- package/dist/db-postgres/schema/shop/refunds.d.ts +214 -0
- package/dist/db-postgres/schema/shop/refunds.js +21 -0
- package/dist/db-postgres/schema/shop/webhookEvents.d.ts +183 -0
- package/dist/db-postgres/schema/shop/webhookEvents.js +22 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/shop/adapters/payu/client.d.ts +9 -0
- package/dist/shop/adapters/payu/client.js +29 -0
- package/dist/shop/adapters/payu/index.js +17 -1
- package/dist/shop/adapters/stripe/index.d.ts +64 -0
- package/dist/shop/adapters/stripe/index.js +169 -0
- package/dist/shop/adapters/stripe/payload.d.ts +38 -0
- package/dist/shop/adapters/stripe/payload.js +90 -0
- package/dist/shop/adapters/stripe/status-map.d.ts +11 -0
- package/dist/shop/adapters/stripe/status-map.js +31 -0
- package/dist/shop/cart/coupon-cookie.d.ts +7 -0
- package/dist/shop/cart/coupon-cookie.js +32 -0
- package/dist/shop/cart/types.d.ts +12 -0
- package/dist/shop/client/index.d.ts +118 -0
- package/dist/shop/client/index.js +39 -1
- package/dist/shop/http/cart-handler.d.ts +8 -0
- package/dist/shop/http/cart-handler.js +60 -1
- package/dist/shop/http/checkout-handler.js +7 -3
- package/dist/shop/http/index.d.ts +1 -1
- package/dist/shop/http/index.js +1 -1
- package/dist/shop/http/retry-payment-handler.js +1 -1
- package/dist/shop/http/webhook-handler.js +19 -1
- package/dist/shop/http/webhook-idempotency.d.ts +16 -0
- package/dist/shop/http/webhook-idempotency.js +51 -0
- package/dist/shop/http/webhook-logic.js +2 -1
- package/dist/shop/index.d.ts +3 -1
- package/dist/shop/index.js +3 -1
- package/dist/shop/pricing.d.ts +15 -0
- package/dist/shop/pricing.js +22 -0
- package/dist/shop/server/cart-hydrate.d.ts +1 -0
- package/dist/shop/server/cart-hydrate.js +58 -10
- package/dist/shop/server/coupons.d.ts +53 -0
- package/dist/shop/server/coupons.js +117 -0
- package/dist/shop/server/email.d.ts +15 -0
- package/dist/shop/server/email.js +46 -3
- package/dist/shop/server/orders.d.ts +1 -0
- package/dist/shop/server/orders.js +120 -54
- package/dist/shop/server/refund.d.ts +32 -0
- package/dist/shop/server/refund.js +140 -0
- package/dist/shop/svelte/InpostPicker.svelte +4 -7
- package/dist/shop/svelte/OrderStatus.svelte +6 -10
- package/dist/shop/svelte/labels.js +4 -2
- package/dist/shop/types.d.ts +41 -1
- package/dist/types/adapters/db.d.ts +16 -0
- package/dist/types/adapters/db.js +8 -1
- package/dist/updates/0.24.0/index.d.ts +2 -0
- package/dist/updates/0.24.0/index.js +20 -0
- package/dist/updates/0.25.0/index.d.ts +2 -0
- package/dist/updates/0.25.0/index.js +89 -0
- package/dist/updates/index.js +65 -1
- package/package.json +7 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Input } from '../../../components/ui/input/index.js';
|
|
3
|
+
import { Button } from '../../../components/ui/button/index.js';
|
|
4
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
5
|
+
|
|
6
|
+
type CouponInput = {
|
|
7
|
+
code: string;
|
|
8
|
+
type: 'percent' | 'fixed';
|
|
9
|
+
value: number;
|
|
10
|
+
minOrderAmount: number | null;
|
|
11
|
+
maxUses: number | null;
|
|
12
|
+
expiresAt: string | null;
|
|
13
|
+
isActive: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
initial?: Partial<CouponInput>;
|
|
18
|
+
submitLabel?: string;
|
|
19
|
+
onSubmit: (input: CouponInput) => Promise<void>;
|
|
20
|
+
onCancel?: () => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let { initial, submitLabel = 'Zapisz', onSubmit, onCancel }: Props = $props();
|
|
24
|
+
|
|
25
|
+
let code = $state(initial?.code ?? '');
|
|
26
|
+
let type = $state<'percent' | 'fixed'>(initial?.type ?? 'percent');
|
|
27
|
+
let value = $state(initial?.value != null ? String(initial.value) : '');
|
|
28
|
+
let minOrderAmountPln = $state(
|
|
29
|
+
initial?.minOrderAmount != null ? (initial.minOrderAmount / 100).toFixed(2) : ''
|
|
30
|
+
);
|
|
31
|
+
let maxUses = $state(initial?.maxUses != null ? String(initial.maxUses) : '');
|
|
32
|
+
let expiresAt = $state(initial?.expiresAt ? initial.expiresAt.slice(0, 10) : '');
|
|
33
|
+
let isActive = $state(initial?.isActive ?? true);
|
|
34
|
+
|
|
35
|
+
let submitting = $state(false);
|
|
36
|
+
let error = $state<string | null>(null);
|
|
37
|
+
|
|
38
|
+
async function handleSubmit(e: Event) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
error = null;
|
|
41
|
+
const numValue = Number(value.replace(',', '.'));
|
|
42
|
+
if (!Number.isFinite(numValue) || numValue <= 0) {
|
|
43
|
+
error = 'Podaj wartość większą od zera.';
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (type === 'percent' && numValue > 100) {
|
|
47
|
+
error = 'Procent nie może przekraczać 100.';
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const minOrderCents = minOrderAmountPln
|
|
51
|
+
? Math.round(Number(minOrderAmountPln.replace(',', '.')) * 100)
|
|
52
|
+
: null;
|
|
53
|
+
if (minOrderCents != null && (!Number.isFinite(minOrderCents) || minOrderCents < 0)) {
|
|
54
|
+
error = 'Minimalna wartość zamówienia musi być nieujemna.';
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const maxUsesNum = maxUses ? Number(maxUses) : null;
|
|
58
|
+
if (maxUsesNum != null && (!Number.isInteger(maxUsesNum) || maxUsesNum <= 0)) {
|
|
59
|
+
error = 'Maksymalna liczba użyć musi być dodatnią liczbą całkowitą.';
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
submitting = true;
|
|
64
|
+
try {
|
|
65
|
+
await onSubmit({
|
|
66
|
+
code: code.trim().toUpperCase(),
|
|
67
|
+
type,
|
|
68
|
+
value: numValue,
|
|
69
|
+
minOrderAmount: minOrderCents,
|
|
70
|
+
maxUses: maxUsesNum,
|
|
71
|
+
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
|
|
72
|
+
isActive
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
error = err instanceof Error ? err.message : 'Nie udało się zapisać.';
|
|
76
|
+
submitting = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<form onsubmit={handleSubmit} class="space-y-5">
|
|
82
|
+
<div class="space-y-2">
|
|
83
|
+
<Label for="coupon-code">Kod</Label>
|
|
84
|
+
<Input
|
|
85
|
+
id="coupon-code"
|
|
86
|
+
bind:value={code}
|
|
87
|
+
placeholder="np. ARIA10"
|
|
88
|
+
required
|
|
89
|
+
pattern="[A-Za-z0-9_-]+"
|
|
90
|
+
maxlength={64}
|
|
91
|
+
aria-describedby="coupon-code-hint"
|
|
92
|
+
/>
|
|
93
|
+
<p id="coupon-code-hint" class="text-muted-foreground text-xs">
|
|
94
|
+
Wielkość liter zostanie ujednolicona do dużych liter. Tylko litery, cyfry, „_”, „-”.
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<fieldset class="space-y-2">
|
|
99
|
+
<legend class="mb-1 text-sm font-semibold">Typ rabatu</legend>
|
|
100
|
+
<label class="flex items-center gap-2 text-sm">
|
|
101
|
+
<input type="radio" name="coupon-type" value="percent" bind:group={type} />
|
|
102
|
+
<span>Procent (% od kwoty netto)</span>
|
|
103
|
+
</label>
|
|
104
|
+
<label class="flex items-center gap-2 text-sm">
|
|
105
|
+
<input type="radio" name="coupon-type" value="fixed" bind:group={type} />
|
|
106
|
+
<span>Kwota stała (PLN)</span>
|
|
107
|
+
</label>
|
|
108
|
+
</fieldset>
|
|
109
|
+
|
|
110
|
+
<div class="space-y-2">
|
|
111
|
+
<Label for="coupon-value">{type === 'percent' ? 'Procent (0–100)' : 'Kwota (PLN)'}</Label>
|
|
112
|
+
<Input
|
|
113
|
+
id="coupon-value"
|
|
114
|
+
type="text"
|
|
115
|
+
inputmode="decimal"
|
|
116
|
+
bind:value
|
|
117
|
+
placeholder={type === 'percent' ? 'np. 10' : 'np. 50,00'}
|
|
118
|
+
required
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="grid grid-cols-2 gap-4">
|
|
123
|
+
<div class="space-y-2">
|
|
124
|
+
<Label for="coupon-min-order">Min. wartość zamówienia (PLN)</Label>
|
|
125
|
+
<Input
|
|
126
|
+
id="coupon-min-order"
|
|
127
|
+
type="text"
|
|
128
|
+
inputmode="decimal"
|
|
129
|
+
bind:value={minOrderAmountPln}
|
|
130
|
+
placeholder="opcjonalnie"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="space-y-2">
|
|
134
|
+
<Label for="coupon-max-uses">Maks. liczba użyć</Label>
|
|
135
|
+
<Input
|
|
136
|
+
id="coupon-max-uses"
|
|
137
|
+
type="number"
|
|
138
|
+
min="1"
|
|
139
|
+
step="1"
|
|
140
|
+
bind:value={maxUses}
|
|
141
|
+
placeholder="bez limitu"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="space-y-2">
|
|
147
|
+
<Label for="coupon-expires-at">Data wygaśnięcia</Label>
|
|
148
|
+
<Input id="coupon-expires-at" type="date" bind:value={expiresAt} />
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<label class="flex items-center gap-2 text-sm">
|
|
152
|
+
<input type="checkbox" bind:checked={isActive} />
|
|
153
|
+
<span>Aktywny (dostępny przy checkoucie)</span>
|
|
154
|
+
</label>
|
|
155
|
+
|
|
156
|
+
{#if error}
|
|
157
|
+
<p class="text-destructive text-sm" role="alert">{error}</p>
|
|
158
|
+
{/if}
|
|
159
|
+
|
|
160
|
+
<div class="flex gap-2">
|
|
161
|
+
<Button type="submit" disabled={submitting}>
|
|
162
|
+
{submitting ? 'Zapisywanie…' : submitLabel}
|
|
163
|
+
</Button>
|
|
164
|
+
{#if onCancel}
|
|
165
|
+
<Button type="button" variant="outline" onclick={onCancel} disabled={submitting}>
|
|
166
|
+
Anuluj
|
|
167
|
+
</Button>
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
</form>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type CouponInput = {
|
|
2
|
+
code: string;
|
|
3
|
+
type: 'percent' | 'fixed';
|
|
4
|
+
value: number;
|
|
5
|
+
minOrderAmount: number | null;
|
|
6
|
+
maxUses: number | null;
|
|
7
|
+
expiresAt: string | null;
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
};
|
|
10
|
+
type Props = {
|
|
11
|
+
initial?: Partial<CouponInput>;
|
|
12
|
+
submitLabel?: string;
|
|
13
|
+
onSubmit: (input: CouponInput) => Promise<void>;
|
|
14
|
+
onCancel?: () => void;
|
|
15
|
+
};
|
|
16
|
+
declare const CouponForm: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type CouponForm = ReturnType<typeof CouponForm>;
|
|
18
|
+
export default CouponForm;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from '$app/navigation';
|
|
3
|
+
import { getRemotes } from '../../../sveltekit/index.js';
|
|
4
|
+
import CouponForm from './coupon-form.svelte';
|
|
5
|
+
|
|
6
|
+
const remotes = getRemotes();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div class="mx-auto max-w-2xl space-y-6 p-6">
|
|
10
|
+
<header>
|
|
11
|
+
<h1 class="text-2xl font-extrabold tracking-tight">Nowy kod rabatowy</h1>
|
|
12
|
+
<p class="text-muted-foreground text-sm">
|
|
13
|
+
<a href="/admin/shop/coupons" class="hover:underline">← Wróć do listy</a>
|
|
14
|
+
</p>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<CouponForm
|
|
18
|
+
submitLabel="Utwórz kod"
|
|
19
|
+
onSubmit={async (input) => {
|
|
20
|
+
await remotes.createCouponCmd(input);
|
|
21
|
+
await goto('/admin/shop/coupons');
|
|
22
|
+
}}
|
|
23
|
+
onCancel={() => goto('/admin/shop/coupons')}
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const CouponNewPage: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type CouponNewPage = InstanceType<typeof CouponNewPage>;
|
|
18
|
+
export default CouponNewPage;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getRemotes } from '../../../sveltekit/index.js';
|
|
3
|
+
import { Button } from '../../../components/ui/button/index.js';
|
|
4
|
+
import PlusIcon from '@tabler/icons-svelte/icons/plus';
|
|
5
|
+
import EditIcon from '@tabler/icons-svelte/icons/edit';
|
|
6
|
+
import TrashIcon from '@tabler/icons-svelte/icons/trash';
|
|
7
|
+
|
|
8
|
+
const remotes = getRemotes();
|
|
9
|
+
const query = $derived(remotes.listCouponsAdmin());
|
|
10
|
+
|
|
11
|
+
function formatValue(type: string, value: string): string {
|
|
12
|
+
const num = Number(value);
|
|
13
|
+
if (type === 'percent') return `${num}%`;
|
|
14
|
+
return new Intl.NumberFormat('pl-PL', {
|
|
15
|
+
style: 'currency',
|
|
16
|
+
currency: 'PLN',
|
|
17
|
+
minimumFractionDigits: 2
|
|
18
|
+
}).format(num);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatDate(d: Date | string | null): string {
|
|
22
|
+
if (!d) return '—';
|
|
23
|
+
return new Intl.DateTimeFormat('pl-PL', {
|
|
24
|
+
dateStyle: 'short'
|
|
25
|
+
}).format(new Date(d));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function handleDelete(id: string, code: string) {
|
|
29
|
+
if (!confirm(`Usunąć kod „${code}”? Tej operacji nie można cofnąć.`)) return;
|
|
30
|
+
try {
|
|
31
|
+
await remotes.deleteCouponCmd(id);
|
|
32
|
+
await query.refresh();
|
|
33
|
+
} catch (err) {
|
|
34
|
+
alert(err instanceof Error ? err.message : 'Nie udało się usunąć kodu.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="flex items-center justify-between gap-4 p-6">
|
|
40
|
+
<div>
|
|
41
|
+
<h1 class="text-2xl font-extrabold tracking-tight">Kody rabatowe</h1>
|
|
42
|
+
<p class="text-muted-foreground text-sm">
|
|
43
|
+
{#if query.ready}
|
|
44
|
+
{query.current?.length ?? 0}
|
|
45
|
+
{(query.current?.length ?? 0) === 1 ? 'kod' : 'kodów'}
|
|
46
|
+
{:else}
|
|
47
|
+
Ładowanie…
|
|
48
|
+
{/if}
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<Button href="/admin/shop/coupons/new">
|
|
52
|
+
<PlusIcon class="mr-1 size-4" />
|
|
53
|
+
Dodaj kod
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{#if query.ready && query.current && query.current.length > 0}
|
|
58
|
+
<div class="px-6 pb-6">
|
|
59
|
+
<div class="border-border bg-card overflow-hidden rounded-xl border">
|
|
60
|
+
<table class="w-full text-sm">
|
|
61
|
+
<thead class="text-muted-foreground bg-muted/50 text-left text-xs uppercase">
|
|
62
|
+
<tr>
|
|
63
|
+
<th class="px-4 py-3 font-semibold">Kod</th>
|
|
64
|
+
<th class="px-4 py-3 font-semibold">Wartość</th>
|
|
65
|
+
<th class="px-4 py-3 font-semibold">Min. zamów.</th>
|
|
66
|
+
<th class="px-4 py-3 font-semibold">Wykorzystano</th>
|
|
67
|
+
<th class="px-4 py-3 font-semibold">Wygasa</th>
|
|
68
|
+
<th class="px-4 py-3 font-semibold">Status</th>
|
|
69
|
+
<th class="px-4 py-3"></th>
|
|
70
|
+
</tr>
|
|
71
|
+
</thead>
|
|
72
|
+
<tbody>
|
|
73
|
+
{#each query.current as c (c.id)}
|
|
74
|
+
<tr class="border-border border-t">
|
|
75
|
+
<td class="px-4 py-3 font-mono font-semibold">{c.code}</td>
|
|
76
|
+
<td class="px-4 py-3">{formatValue(c.type, c.value)}</td>
|
|
77
|
+
<td class="text-muted-foreground px-4 py-3">
|
|
78
|
+
{c.minOrderAmount != null
|
|
79
|
+
? new Intl.NumberFormat('pl-PL', {
|
|
80
|
+
style: 'currency',
|
|
81
|
+
currency: 'PLN',
|
|
82
|
+
minimumFractionDigits: 2
|
|
83
|
+
}).format(c.minOrderAmount / 100)
|
|
84
|
+
: '—'}
|
|
85
|
+
</td>
|
|
86
|
+
<td class="px-4 py-3">
|
|
87
|
+
{c.usedCount}{c.maxUses != null ? ` / ${c.maxUses}` : ''}
|
|
88
|
+
</td>
|
|
89
|
+
<td class="text-muted-foreground px-4 py-3 text-xs">
|
|
90
|
+
{formatDate(c.expiresAt)}
|
|
91
|
+
</td>
|
|
92
|
+
<td class="px-4 py-3">
|
|
93
|
+
{#if c.isActive}
|
|
94
|
+
<span class="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-800"
|
|
95
|
+
>aktywny</span
|
|
96
|
+
>
|
|
97
|
+
{:else}
|
|
98
|
+
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-800">
|
|
99
|
+
wstrzymany
|
|
100
|
+
</span>
|
|
101
|
+
{/if}
|
|
102
|
+
</td>
|
|
103
|
+
<td class="px-4 py-3 text-right">
|
|
104
|
+
<div class="flex justify-end gap-1">
|
|
105
|
+
<Button
|
|
106
|
+
href="/admin/shop/coupons/{c.id}"
|
|
107
|
+
variant="ghost"
|
|
108
|
+
size="sm"
|
|
109
|
+
title="Edytuj"
|
|
110
|
+
>
|
|
111
|
+
<EditIcon class="size-4" />
|
|
112
|
+
</Button>
|
|
113
|
+
<Button
|
|
114
|
+
onclick={() => handleDelete(c.id, c.code)}
|
|
115
|
+
variant="ghost"
|
|
116
|
+
size="sm"
|
|
117
|
+
title="Usuń"
|
|
118
|
+
>
|
|
119
|
+
<TrashIcon class="size-4" />
|
|
120
|
+
</Button>
|
|
121
|
+
</div>
|
|
122
|
+
</td>
|
|
123
|
+
</tr>
|
|
124
|
+
{/each}
|
|
125
|
+
</tbody>
|
|
126
|
+
</table>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
{:else if query.ready}
|
|
130
|
+
<div class="text-muted-foreground p-6 text-sm">
|
|
131
|
+
Brak kodów rabatowych. <a href="/admin/shop/coupons/new" class="text-primary hover:underline"
|
|
132
|
+
>Dodaj pierwszy</a
|
|
133
|
+
>.
|
|
134
|
+
</div>
|
|
135
|
+
{/if}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Dialog from '../../../components/ui/dialog/index.js';
|
|
3
|
+
import { Input } from '../../../components/ui/input/index.js';
|
|
4
|
+
import Label from '../../../components/ui/label/label.svelte';
|
|
5
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
6
|
+
import { getRemotes } from '../../../sveltekit/index.js';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
open: boolean;
|
|
10
|
+
orderId: string;
|
|
11
|
+
currency: string;
|
|
12
|
+
remainingRefundable: number;
|
|
13
|
+
onOpenChange: (open: boolean) => void;
|
|
14
|
+
onRefunded: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
open = $bindable(),
|
|
19
|
+
orderId,
|
|
20
|
+
currency,
|
|
21
|
+
remainingRefundable,
|
|
22
|
+
onOpenChange,
|
|
23
|
+
onRefunded
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
const remotes = getRemotes();
|
|
27
|
+
|
|
28
|
+
let mode = $state<'full' | 'partial'>('full');
|
|
29
|
+
let amountInput = $state('');
|
|
30
|
+
let reason = $state('');
|
|
31
|
+
let submitting = $state(false);
|
|
32
|
+
let error = $state<string | null>(null);
|
|
33
|
+
|
|
34
|
+
const remainingDisplay = $derived(
|
|
35
|
+
new Intl.NumberFormat('pl-PL', {
|
|
36
|
+
style: 'currency',
|
|
37
|
+
currency,
|
|
38
|
+
minimumFractionDigits: 2
|
|
39
|
+
}).format(remainingRefundable / 100)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function reset() {
|
|
43
|
+
mode = 'full';
|
|
44
|
+
amountInput = '';
|
|
45
|
+
reason = '';
|
|
46
|
+
error = null;
|
|
47
|
+
submitting = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleSubmit(e: Event) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
error = null;
|
|
53
|
+
|
|
54
|
+
let amount: number | undefined;
|
|
55
|
+
if (mode === 'partial') {
|
|
56
|
+
const pln = Number(amountInput.replace(',', '.'));
|
|
57
|
+
if (!Number.isFinite(pln) || pln <= 0) {
|
|
58
|
+
error = 'Kwota musi być większa od zera.';
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
amount = Math.round(pln * 100);
|
|
62
|
+
if (amount > remainingRefundable) {
|
|
63
|
+
error = `Kwota nie może przekroczyć pozostałych ${remainingDisplay}.`;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
submitting = true;
|
|
69
|
+
try {
|
|
70
|
+
const result = await remotes.refundOrderCmd({
|
|
71
|
+
orderId,
|
|
72
|
+
amount,
|
|
73
|
+
reason: reason.trim() || undefined
|
|
74
|
+
});
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
error = result.error;
|
|
77
|
+
submitting = false;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
reset();
|
|
81
|
+
onOpenChange(false);
|
|
82
|
+
onRefunded();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
error = err instanceof Error ? err.message : 'Nie udało się wykonać zwrotu.';
|
|
85
|
+
submitting = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<Dialog.Root
|
|
91
|
+
{open}
|
|
92
|
+
onOpenChange={(v) => {
|
|
93
|
+
if (!v) reset();
|
|
94
|
+
onOpenChange(v);
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<Dialog.Content>
|
|
98
|
+
<Dialog.Header>
|
|
99
|
+
<Dialog.Title>Zwrot środków</Dialog.Title>
|
|
100
|
+
<Dialog.Description>
|
|
101
|
+
Pozostała kwota do zwrotu: <strong>{remainingDisplay}</strong>.
|
|
102
|
+
</Dialog.Description>
|
|
103
|
+
</Dialog.Header>
|
|
104
|
+
<form onsubmit={handleSubmit} class="space-y-4">
|
|
105
|
+
<fieldset class="space-y-2">
|
|
106
|
+
<legend class="mb-1 text-sm font-semibold">Typ zwrotu</legend>
|
|
107
|
+
<label class="flex items-center gap-2 text-sm">
|
|
108
|
+
<input type="radio" name="refund-mode" value="full" bind:group={mode} />
|
|
109
|
+
<span>Pełny zwrot ({remainingDisplay})</span>
|
|
110
|
+
</label>
|
|
111
|
+
<label class="flex items-center gap-2 text-sm">
|
|
112
|
+
<input type="radio" name="refund-mode" value="partial" bind:group={mode} />
|
|
113
|
+
<span>Częściowy zwrot</span>
|
|
114
|
+
</label>
|
|
115
|
+
</fieldset>
|
|
116
|
+
|
|
117
|
+
{#if mode === 'partial'}
|
|
118
|
+
<div class="space-y-2">
|
|
119
|
+
<Label for="refund-amount">Kwota (PLN)</Label>
|
|
120
|
+
<Input
|
|
121
|
+
id="refund-amount"
|
|
122
|
+
type="text"
|
|
123
|
+
inputmode="decimal"
|
|
124
|
+
bind:value={amountInput}
|
|
125
|
+
placeholder="np. 49,99"
|
|
126
|
+
required
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
{/if}
|
|
130
|
+
|
|
131
|
+
<div class="space-y-2">
|
|
132
|
+
<Label for="refund-reason">Powód (opcjonalnie)</Label>
|
|
133
|
+
<Input
|
|
134
|
+
id="refund-reason"
|
|
135
|
+
type="text"
|
|
136
|
+
bind:value={reason}
|
|
137
|
+
maxlength={500}
|
|
138
|
+
placeholder="np. anulowanie zamówienia przez klienta"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{#if error}
|
|
143
|
+
<p class="text-destructive text-sm" role="alert">{error}</p>
|
|
144
|
+
{/if}
|
|
145
|
+
|
|
146
|
+
<Dialog.Footer>
|
|
147
|
+
<Button
|
|
148
|
+
type="button"
|
|
149
|
+
variant="outline"
|
|
150
|
+
onclick={() => onOpenChange(false)}
|
|
151
|
+
disabled={submitting}
|
|
152
|
+
>
|
|
153
|
+
Anuluj
|
|
154
|
+
</Button>
|
|
155
|
+
<Button type="submit" disabled={submitting}>
|
|
156
|
+
{submitting ? 'Wykonuję…' : 'Wykonaj zwrot'}
|
|
157
|
+
</Button>
|
|
158
|
+
</Dialog.Footer>
|
|
159
|
+
</form>
|
|
160
|
+
</Dialog.Content>
|
|
161
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
orderId: string;
|
|
4
|
+
currency: string;
|
|
5
|
+
remainingRefundable: number;
|
|
6
|
+
onOpenChange: (open: boolean) => void;
|
|
7
|
+
onRefunded: () => void;
|
|
8
|
+
};
|
|
9
|
+
declare const RefundDialog: import("svelte").Component<Props, {}, "open">;
|
|
10
|
+
type RefundDialog = ReturnType<typeof RefundDialog>;
|
|
11
|
+
export default RefundDialog;
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
import { getRemotes } from '../../../sveltekit/index.js';
|
|
5
5
|
import { Button } from '../../../components/ui/button/index.js';
|
|
6
6
|
import TrashIcon from '@tabler/icons-svelte/icons/trash';
|
|
7
|
-
import ShippingMethodForm, {
|
|
8
|
-
type ShippingFormPayload
|
|
9
|
-
} from './shipping-method-form.svelte';
|
|
7
|
+
import ShippingMethodForm, { type ShippingFormPayload } from './shipping-method-form.svelte';
|
|
10
8
|
|
|
11
9
|
const remotes = getRemotes();
|
|
12
10
|
|
|
@@ -59,9 +57,8 @@
|
|
|
59
57
|
<div class="flex items-start justify-between gap-4">
|
|
60
58
|
<div>
|
|
61
59
|
<h1 class="text-2xl font-extrabold tracking-tight">Edycja metody wysyłki</h1>
|
|
62
|
-
<a
|
|
63
|
-
|
|
64
|
-
class="text-muted-foreground text-sm hover:underline">← Wróć do listy</a
|
|
60
|
+
<a href="/admin/shop/shipping-methods" class="text-muted-foreground text-sm hover:underline"
|
|
61
|
+
>← Wróć do listy</a
|
|
65
62
|
>
|
|
66
63
|
</div>
|
|
67
64
|
<div>
|
|
@@ -69,16 +69,10 @@
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
let names = $state<Record<string, string>>(
|
|
72
|
-
languages.reduce(
|
|
73
|
-
(acc, l) => ({ ...acc, [l]: asRecord(initial?.name)[l] ?? '' }),
|
|
74
|
-
{}
|
|
75
|
-
)
|
|
72
|
+
languages.reduce((acc, l) => ({ ...acc, [l]: asRecord(initial?.name)[l] ?? '' }), {})
|
|
76
73
|
);
|
|
77
74
|
let descriptions = $state<Record<string, string>>(
|
|
78
|
-
languages.reduce(
|
|
79
|
-
(acc, l) => ({ ...acc, [l]: asRecord(initial?.description)[l] ?? '' }),
|
|
80
|
-
{}
|
|
81
|
-
)
|
|
75
|
+
languages.reduce((acc, l) => ({ ...acc, [l]: asRecord(initial?.description)[l] ?? '' }), {})
|
|
82
76
|
);
|
|
83
77
|
type InputMode = 'net' | 'gross';
|
|
84
78
|
// initial.price — teraz PLN (number, netto) z API. Hydratujemy jako gross, żeby user widział dokładnie to co wpisał.
|
|
@@ -86,9 +80,7 @@
|
|
|
86
80
|
const initialVat = Number(initial?.vatRate ?? vatRates[0] ?? 23);
|
|
87
81
|
let inputMode = $state<InputMode>(initialNetPln != null ? 'gross' : 'gross');
|
|
88
82
|
let inputPrice = $state(
|
|
89
|
-
initialNetPln != null
|
|
90
|
-
? (initialNetPln * (1 + initialVat / 100)).toFixed(2)
|
|
91
|
-
: '0.00'
|
|
83
|
+
initialNetPln != null ? (initialNetPln * (1 + initialVat / 100)).toFixed(2) : '0.00'
|
|
92
84
|
);
|
|
93
85
|
let vatRate = $state<number | string>(initial?.vatRate ?? vatRates[0] ?? 23);
|
|
94
86
|
|
|
@@ -118,7 +110,8 @@
|
|
|
118
110
|
let isActive = $state(initial?.isActive ?? true);
|
|
119
111
|
// null/undefined = no restriction (all allowed); otherwise a whitelist.
|
|
120
112
|
let restrictPayments = $state(
|
|
121
|
-
Array.isArray(initial?.allowedPaymentMethods) &&
|
|
113
|
+
Array.isArray(initial?.allowedPaymentMethods) &&
|
|
114
|
+
(initial?.allowedPaymentMethods?.length ?? 0) > 0
|
|
122
115
|
);
|
|
123
116
|
let allowedPaymentIds = $state<string[]>(
|
|
124
117
|
Array.isArray(initial?.allowedPaymentMethods) ? [...(initial?.allowedPaymentMethods ?? [])] : []
|
|
@@ -248,17 +241,19 @@
|
|
|
248
241
|
</select>
|
|
249
242
|
</label>
|
|
250
243
|
</div>
|
|
251
|
-
<div
|
|
244
|
+
<div
|
|
245
|
+
class="bg-muted/40 border-border grid grid-cols-3 gap-2 rounded-lg border p-2.5 text-center text-xs"
|
|
246
|
+
>
|
|
252
247
|
<div>
|
|
253
|
-
<div class="text-muted-foreground font-semibold
|
|
248
|
+
<div class="text-muted-foreground font-semibold tracking-wide uppercase">Netto</div>
|
|
254
249
|
<div class="text-sm font-bold tabular-nums">{formatPln(netPln)} zł</div>
|
|
255
250
|
</div>
|
|
256
251
|
<div class="border-border border-x">
|
|
257
|
-
<div class="text-muted-foreground font-semibold
|
|
252
|
+
<div class="text-muted-foreground font-semibold tracking-wide uppercase">VAT</div>
|
|
258
253
|
<div class="text-sm font-bold tabular-nums">{formatPln(vatPln)} zł</div>
|
|
259
254
|
</div>
|
|
260
255
|
<div>
|
|
261
|
-
<div class="text-muted-foreground font-semibold
|
|
256
|
+
<div class="text-muted-foreground font-semibold tracking-wide uppercase">Brutto</div>
|
|
262
257
|
<div class="text-primary text-sm font-bold tabular-nums">{formatPln(grossPln)} zł</div>
|
|
263
258
|
</div>
|
|
264
259
|
</div>
|
|
@@ -284,8 +279,8 @@
|
|
|
284
279
|
<section class="border-border bg-card space-y-4 rounded-xl border p-6">
|
|
285
280
|
<h2 class="text-lg font-bold">Dozwolone metody płatności</h2>
|
|
286
281
|
<p class="text-muted-foreground text-sm">
|
|
287
|
-
Ogranicz metody płatności dla tej dostawy (np. wyłącz płatność za pobraniem dla
|
|
288
|
-
|
|
282
|
+
Ogranicz metody płatności dla tej dostawy (np. wyłącz płatność za pobraniem dla paczkomatu).
|
|
283
|
+
Jeśli wyłączone — wszystkie skonfigurowane metody są dostępne.
|
|
289
284
|
</p>
|
|
290
285
|
<label class="flex items-center gap-2">
|
|
291
286
|
<Switch bind:checked={restrictPayments} />
|
|
@@ -307,8 +302,7 @@
|
|
|
307
302
|
{/each}
|
|
308
303
|
{#if allowedPaymentIds.length === 0}
|
|
309
304
|
<p class="text-xs text-amber-700">
|
|
310
|
-
Wybierz co najmniej jedną metodę, inaczej checkout dla tej dostawy będzie
|
|
311
|
-
zablokowany.
|
|
305
|
+
Wybierz co najmniej jedną metodę, inaczej checkout dla tej dostawy będzie zablokowany.
|
|
312
306
|
</p>
|
|
313
307
|
{/if}
|
|
314
308
|
</div>
|
|
@@ -327,7 +321,7 @@
|
|
|
327
321
|
</label>
|
|
328
322
|
|
|
329
323
|
{#if carrierType === 'inpost'}
|
|
330
|
-
<div class="space-y-4 border-l-2
|
|
324
|
+
<div class="border-primary/30 space-y-4 border-l-2 pl-4">
|
|
331
325
|
<p class="text-muted-foreground text-sm">
|
|
332
326
|
Wymaga skonfigurowanego adaptera <code>inpostAdapter</code> w <code>cms.config.ts</code>.
|
|
333
327
|
</p>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { goto } from '$app/navigation';
|
|
3
3
|
import { getRemotes } from '../../../sveltekit/index.js';
|
|
4
|
-
import ShippingMethodForm, {
|
|
5
|
-
type ShippingFormPayload
|
|
6
|
-
} from './shipping-method-form.svelte';
|
|
4
|
+
import ShippingMethodForm, { type ShippingFormPayload } from './shipping-method-form.svelte';
|
|
7
5
|
|
|
8
6
|
const remotes = getRemotes();
|
|
9
7
|
const configQuery = $derived(remotes.getShopConfig());
|
|
@@ -31,9 +29,8 @@
|
|
|
31
29
|
<div class="space-y-6 p-6">
|
|
32
30
|
<div>
|
|
33
31
|
<h1 class="text-2xl font-extrabold tracking-tight">Nowa metoda wysyłki</h1>
|
|
34
|
-
<a
|
|
35
|
-
|
|
36
|
-
class="text-muted-foreground text-sm hover:underline">← Wróć do listy</a
|
|
32
|
+
<a href="/admin/shop/shipping-methods" class="text-muted-foreground text-sm hover:underline"
|
|
33
|
+
>← Wróć do listy</a
|
|
37
34
|
>
|
|
38
35
|
</div>
|
|
39
36
|
<ShippingMethodForm
|