includio-cms 0.34.1 → 0.35.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 (41) hide show
  1. package/API.md +4 -2
  2. package/CHANGELOG.md +19 -0
  3. package/DOCS.md +1 -1
  4. package/dist/admin/client/shop/coupon-edit-page.svelte +1 -0
  5. package/dist/admin/client/shop/coupon-form.svelte +62 -2
  6. package/dist/admin/client/shop/coupon-schema.d.ts +5 -0
  7. package/dist/admin/client/shop/coupon-schema.js +2 -0
  8. package/dist/admin/client/shop/shop-order-detail-page.svelte +72 -2
  9. package/dist/admin/components/fields/date-field.svelte +81 -27
  10. package/dist/admin/components/fields/date-field.svelte.d.ts +3 -0
  11. package/dist/admin/components/fields/datetime-field.svelte +142 -29
  12. package/dist/admin/components/fields/datetime-field.svelte.d.ts +3 -0
  13. package/dist/admin/remote/shop.remote.d.ts +6 -0
  14. package/dist/admin/remote/shop.remote.js +4 -0
  15. package/dist/core/server/generator/generator.js +3 -2
  16. package/dist/db-postgres/schema/shop/coupons.d.ts +20 -0
  17. package/dist/db-postgres/schema/shop/coupons.js +3 -0
  18. package/dist/paraglide/messages/_index.d.ts +36 -3
  19. package/dist/paraglide/messages/_index.js +71 -3
  20. package/dist/paraglide/messages/en.d.ts +5 -0
  21. package/dist/paraglide/messages/en.js +14 -0
  22. package/dist/paraglide/messages/pl.d.ts +5 -0
  23. package/dist/paraglide/messages/pl.js +14 -0
  24. package/dist/shop/client/index.d.ts +1 -0
  25. package/dist/shop/http/order-handler.js +2 -1
  26. package/dist/shop/pricing.d.ts +18 -6
  27. package/dist/shop/pricing.js +33 -8
  28. package/dist/shop/server/coupons.js +3 -2
  29. package/dist/shop/server/email.js +30 -0
  30. package/dist/shop/templates/_partials/items.en.html +10 -0
  31. package/dist/shop/templates/_partials/items.pl.html +10 -0
  32. package/dist/updates/0.35.0/index.d.ts +2 -0
  33. package/dist/updates/0.35.0/index.js +16 -0
  34. package/dist/updates/index.js +3 -1
  35. package/package.json +1 -1
  36. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  37. package/dist/paraglide/messages/hello_world.js +0 -33
  38. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  39. package/dist/paraglide/messages/login_hello.js +0 -34
  40. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  41. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -413,6 +413,7 @@ export declare const listCouponsAdmin: import("@sveltejs/kit").RemoteQueryFuncti
413
413
  code: string;
414
414
  type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
415
415
  value: string;
416
+ appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
416
417
  minOrderAmount: number | null;
417
418
  maxUses: number | null;
418
419
  usedCount: number;
@@ -426,6 +427,7 @@ export declare const getCouponAdmin: import("@sveltejs/kit").RemoteQueryFunction
426
427
  code: string;
427
428
  type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
428
429
  value: string;
430
+ appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
429
431
  minOrderAmount: number | null;
430
432
  maxUses: number | null;
431
433
  usedCount: number;
@@ -438,6 +440,7 @@ export declare const createCouponCmd: import("@sveltejs/kit").RemoteCommand<{
438
440
  code: string;
439
441
  type: "fixed" | "percent";
440
442
  value: number;
443
+ appliesTo?: "net" | "gross" | undefined;
441
444
  minOrderAmount?: number | null | undefined;
442
445
  maxUses?: number | null | undefined;
443
446
  expiresAt?: string | null | undefined;
@@ -451,6 +454,7 @@ export declare const createCouponCmd: import("@sveltejs/kit").RemoteCommand<{
451
454
  expiresAt: Date | null;
452
455
  value: string;
453
456
  type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
457
+ appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
454
458
  minOrderAmount: number | null;
455
459
  maxUses: number | null;
456
460
  usedCount: number;
@@ -460,6 +464,7 @@ export declare const updateCouponCmd: import("@sveltejs/kit").RemoteCommand<{
460
464
  input: {
461
465
  code?: string | undefined;
462
466
  type?: "fixed" | "percent" | undefined;
467
+ appliesTo?: "net" | "gross" | undefined;
463
468
  value?: number | undefined;
464
469
  minOrderAmount?: number | null | undefined;
465
470
  maxUses?: number | null | undefined;
@@ -471,6 +476,7 @@ export declare const updateCouponCmd: import("@sveltejs/kit").RemoteCommand<{
471
476
  code: string;
472
477
  type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
473
478
  value: string;
479
+ appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
474
480
  minOrderAmount: number | null;
475
481
  maxUses: number | null;
476
482
  usedCount: number;
@@ -346,6 +346,7 @@ export const generateBalanceLinkForOrder = command(z.string(), async (orderId) =
346
346
  const couponInputSchema = z.object({
347
347
  code: z.string().min(1).max(64),
348
348
  type: z.enum(['percent', 'fixed']),
349
+ appliesTo: z.enum(['net', 'gross']).optional(),
349
350
  value: z.number().nonnegative().max(1e9),
350
351
  minOrderAmount: z.number().int().nonnegative().nullable().optional(),
351
352
  maxUses: z.number().int().positive().nullable().optional(),
@@ -372,6 +373,7 @@ export const createCouponCmd = command(couponInputSchema, async (input) => {
372
373
  .values({
373
374
  code,
374
375
  type: input.type,
376
+ appliesTo: input.appliesTo ?? 'net',
375
377
  value: String(input.value),
376
378
  minOrderAmount: input.minOrderAmount ?? null,
377
379
  maxUses: input.maxUses ?? null,
@@ -389,6 +391,8 @@ export const updateCouponCmd = command(z.object({ id: z.string(), input: couponI
389
391
  patch.code = input.code.trim().toUpperCase();
390
392
  if (input.type !== undefined)
391
393
  patch.type = input.type;
394
+ if (input.appliesTo !== undefined)
395
+ patch.appliesTo = input.appliesTo;
392
396
  if (input.value !== undefined)
393
397
  patch.value = String(input.value);
394
398
  if (input.minOrderAmount !== undefined)
@@ -178,10 +178,11 @@ function generateAPI(config) {
178
178
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
179
179
  const filePath = join(cmsDir, 'api.ts');
180
180
  let code = `// This file is auto-generated. Do not edit directly.\n\n`;
181
+ const hasForms = !!(config.forms && config.forms.length > 0);
181
182
  code += `
182
183
 
183
- import type { SingleEntryMap, SingleSlug, CollectionEntryMap, CollectionSlug, FormEntryMap, SiteLanguage } from './types';
184
- import { resolveEntry, resolveEntries, countEntries, createFormSubmission, type PopulateConfig } from 'includio-cms/sveltekit/server';
184
+ import type { SingleEntryMap, SingleSlug, CollectionEntryMap, CollectionSlug,${hasForms ? ' FormEntryMap,' : ''} SiteLanguage } from './types';
185
+ import { resolveEntry, resolveEntries, countEntries,${hasForms ? ' createFormSubmission,' : ''} type PopulateConfig } from 'includio-cms/sveltekit/server';
185
186
 
186
187
  `;
187
188
  code += `
@@ -1,4 +1,5 @@
1
1
  export type ShopCouponType = 'percent' | 'fixed';
2
+ export type ShopCouponAppliesTo = 'net' | 'gross';
2
3
  export declare const shopCouponsTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
3
4
  name: "shop_coupons";
4
5
  schema: undefined;
@@ -73,6 +74,25 @@ export declare const shopCouponsTable: import("drizzle-orm/pg-core/table", { wit
73
74
  identity: undefined;
74
75
  generated: undefined;
75
76
  }, {}, {}>;
77
+ appliesTo: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
78
+ name: "applies_to";
79
+ tableName: "shop_coupons";
80
+ dataType: "string";
81
+ columnType: "PgText";
82
+ data: ShopCouponAppliesTo;
83
+ driverParam: string;
84
+ notNull: true;
85
+ hasDefault: true;
86
+ isPrimaryKey: false;
87
+ isAutoincrement: false;
88
+ hasRuntimeDefault: false;
89
+ enumValues: [string, ...string[]];
90
+ baseColumn: never;
91
+ identity: undefined;
92
+ generated: undefined;
93
+ }, {}, {
94
+ $type: ShopCouponAppliesTo;
95
+ }>;
76
96
  minOrderAmount: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
77
97
  name: "min_order_amount";
78
98
  tableName: "shop_coupons";
@@ -6,6 +6,9 @@ export const shopCouponsTable = pgTable('shop_coupons', {
6
6
  type: text('type').$type().notNull(),
7
7
  // percent → integer 0-100 stored as numeric for symmetry; fixed → PLN value (precision 20,6 like product.basePrice)
8
8
  value: numeric('value', { precision: 20, scale: 6 }).notNull(),
9
+ // Whether `value` applies to the net or gross subtotal. Storage of the
10
+ // resulting discount is always net (canonical) — see calculateCouponDiscountNet.
11
+ appliesTo: text('applies_to').$type().notNull().default('net'),
9
12
  minOrderAmount: integer('min_order_amount'),
10
13
  maxUses: integer('max_uses'),
11
14
  usedCount: integer('used_count').notNull().default(0),
@@ -1,3 +1,36 @@
1
- export * from "./hello_world.js";
2
- export * from "./login_hello.js";
3
- export * from "./login_please_login.js";
1
+ export function hello_world(inputs: {
2
+ name: NonNullable<unknown>;
3
+ }, options?: {
4
+ locale?: "en" | "pl";
5
+ }): string;
6
+ /**
7
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
8
+ *
9
+ * - Changing this function will be over-written by the next build.
10
+ *
11
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
12
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
13
+ *
14
+ * @param {{}} inputs
15
+ * @param {{ locale?: "en" | "pl" }} options
16
+ * @returns {string}
17
+ */
18
+ declare function login_hello(inputs?: {}, options?: {
19
+ locale?: "en" | "pl";
20
+ }): string;
21
+ /**
22
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
23
+ *
24
+ * - Changing this function will be over-written by the next build.
25
+ *
26
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
27
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
28
+ *
29
+ * @param {{}} inputs
30
+ * @param {{ locale?: "en" | "pl" }} options
31
+ * @returns {string}
32
+ */
33
+ declare function login_please_login(inputs?: {}, options?: {
34
+ locale?: "en" | "pl";
35
+ }): string;
36
+ export { login_hello as login.hello, login_please_login as login.please_login };
@@ -1,4 +1,72 @@
1
1
  /* eslint-disable */
2
- export * from './hello_world.js'
3
- export * from './login_hello.js'
4
- export * from './login_please_login.js'
2
+ import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from "../runtime.js"
3
+ import * as en from "./en.js"
4
+ import * as pl from "./pl.js"
5
+ /**
6
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
7
+ *
8
+ * - Changing this function will be over-written by the next build.
9
+ *
10
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
11
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
12
+ *
13
+ * @param {{ name: NonNullable<unknown> }} inputs
14
+ * @param {{ locale?: "en" | "pl" }} options
15
+ * @returns {string}
16
+ */
17
+ /* @__NO_SIDE_EFFECTS__ */
18
+ export const hello_world = (inputs, options = {}) => {
19
+ if (experimentalMiddlewareLocaleSplitting && isServer === false) {
20
+ return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
21
+ }
22
+ const locale = options.locale ?? getLocale()
23
+ trackMessageCall("hello_world", locale)
24
+ if (locale === "en") return en.hello_world(inputs)
25
+ return pl.hello_world(inputs)
26
+ };
27
+ /**
28
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
29
+ *
30
+ * - Changing this function will be over-written by the next build.
31
+ *
32
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
33
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
34
+ *
35
+ * @param {{}} inputs
36
+ * @param {{ locale?: "en" | "pl" }} options
37
+ * @returns {string}
38
+ */
39
+ /* @__NO_SIDE_EFFECTS__ */
40
+ const login_hello = (inputs = {}, options = {}) => {
41
+ if (experimentalMiddlewareLocaleSplitting && isServer === false) {
42
+ return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
43
+ }
44
+ const locale = options.locale ?? getLocale()
45
+ trackMessageCall("login_hello", locale)
46
+ if (locale === "en") return en.login_hello(inputs)
47
+ return pl.login_hello(inputs)
48
+ };
49
+ export { login_hello as "login.hello" }
50
+ /**
51
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
52
+ *
53
+ * - Changing this function will be over-written by the next build.
54
+ *
55
+ * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
56
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
57
+ *
58
+ * @param {{}} inputs
59
+ * @param {{ locale?: "en" | "pl" }} options
60
+ * @returns {string}
61
+ */
62
+ /* @__NO_SIDE_EFFECTS__ */
63
+ const login_please_login = (inputs = {}, options = {}) => {
64
+ if (experimentalMiddlewareLocaleSplitting && isServer === false) {
65
+ return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
66
+ }
67
+ const locale = options.locale ?? getLocale()
68
+ trackMessageCall("login_please_login", locale)
69
+ if (locale === "en") return en.login_please_login(inputs)
70
+ return pl.login_please_login(inputs)
71
+ };
72
+ export { login_please_login as "login.please_login" }
@@ -0,0 +1,5 @@
1
+ export const hello_world: (inputs: {
2
+ name: NonNullable<unknown>;
3
+ }) => string;
4
+ export const login_hello: (inputs: {}) => string;
5
+ export const login_please_login: (inputs: {}) => string;
@@ -0,0 +1,14 @@
1
+ /* eslint-disable */
2
+
3
+
4
+ export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
5
+ return `Hello, ${i.name} from en!`
6
+ };
7
+
8
+ export const login_hello = /** @type {(inputs: {}) => string} */ () => {
9
+ return `Welcome back`
10
+ };
11
+
12
+ export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
13
+ return `Login to your account`
14
+ };
@@ -0,0 +1,5 @@
1
+ export const hello_world: (inputs: {
2
+ name: NonNullable<unknown>;
3
+ }) => string;
4
+ export const login_hello: (inputs: {}) => string;
5
+ export const login_please_login: (inputs: {}) => string;
@@ -0,0 +1,14 @@
1
+ /* eslint-disable */
2
+
3
+
4
+ export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
5
+ return `Hello, ${i.name} from pl!`
6
+ };
7
+
8
+ export const login_hello = /** @type {(inputs: {}) => string} */ () => {
9
+ return `Witaj ponownie`
10
+ };
11
+
12
+ export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
13
+ return `Zaloguj się na swoje konto`
14
+ };
@@ -90,6 +90,7 @@ export interface OrderDetailResponse {
90
90
  trackingUrl: string | null;
91
91
  language: string | null;
92
92
  createdAt: string;
93
+ notes: string | null;
93
94
  };
94
95
  items: Array<{
95
96
  id: string;
@@ -76,7 +76,8 @@ export function createOrderHandler() {
76
76
  trackingNumber: order.trackingNumber,
77
77
  trackingUrl,
78
78
  language: order.language,
79
- createdAt: order.createdAt
79
+ createdAt: order.createdAt,
80
+ notes: order.notes ?? null
80
81
  },
81
82
  items: items.map((i) => ({
82
83
  id: i.id,
@@ -21,14 +21,26 @@ export interface CouponDiscountInput {
21
21
  type: 'percent' | 'fixed';
22
22
  /** percent: 0-100; fixed: PLN value (precision 20,6). */
23
23
  value: number;
24
+ /** Whether `value` is applied to the net or gross subtotal. Defaults to 'net'. */
25
+ appliesTo?: 'net' | 'gross';
24
26
  }
25
27
  /**
26
28
  * Compute discount in cents (minor units) applied to a net subtotal.
27
- * - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
28
- * - `fixed`: PLN value converted to cents, capped at subtotalNet (never negative).
29
29
  *
30
- * Discount is calculated on the net subtotal — VAT is then applied to lines
31
- * after the discount factor, so the discount appears proportionally on each
32
- * line. Result is bounded to `[0, subtotalNet]`.
30
+ * Behavior by mode:
31
+ * - `appliesTo='net'` (default):
32
+ * - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
33
+ * - `fixed`: PLN value converted to cents, capped at subtotalNet.
34
+ * - `appliesTo='gross'` (requires `subtotalGross`):
35
+ * - `percent`: discount expressed against the gross subtotal, then converted
36
+ * to net via the aggregate net/gross ratio (handles mixed VAT rates).
37
+ * Mathematically yields the same proportional factor as net-percent —
38
+ * kept explicit for clarity and rounding parity with the fixed-gross path.
39
+ * - `fixed`: PLN value interpreted as a gross amount, capped at subtotalGross,
40
+ * then converted to net via `× subtotalNet / subtotalGross`.
41
+ *
42
+ * Discount storage is always net (canonical) — VAT is applied to lines after
43
+ * the discount factor, so the rebate appears proportionally on each line.
44
+ * Result is bounded to `[0, subtotalNet]`.
33
45
  */
34
- export declare function calculateCouponDiscountNet(subtotalNet: number, coupon: CouponDiscountInput): number;
46
+ export declare function calculateCouponDiscountNet(subtotalNet: number, coupon: CouponDiscountInput, subtotalGross?: number): number;
@@ -49,23 +49,48 @@ export function resolveI18n(value, language, fallback) {
49
49
  }
50
50
  /**
51
51
  * Compute discount in cents (minor units) applied to a net subtotal.
52
- * - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
53
- * - `fixed`: PLN value converted to cents, capped at subtotalNet (never negative).
54
52
  *
55
- * Discount is calculated on the net subtotal — VAT is then applied to lines
56
- * after the discount factor, so the discount appears proportionally on each
57
- * line. Result is bounded to `[0, subtotalNet]`.
53
+ * Behavior by mode:
54
+ * - `appliesTo='net'` (default):
55
+ * - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
56
+ * - `fixed`: PLN value converted to cents, capped at subtotalNet.
57
+ * - `appliesTo='gross'` (requires `subtotalGross`):
58
+ * - `percent`: discount expressed against the gross subtotal, then converted
59
+ * to net via the aggregate net/gross ratio (handles mixed VAT rates).
60
+ * Mathematically yields the same proportional factor as net-percent —
61
+ * kept explicit for clarity and rounding parity with the fixed-gross path.
62
+ * - `fixed`: PLN value interpreted as a gross amount, capped at subtotalGross,
63
+ * then converted to net via `× subtotalNet / subtotalGross`.
64
+ *
65
+ * Discount storage is always net (canonical) — VAT is applied to lines after
66
+ * the discount factor, so the rebate appears proportionally on each line.
67
+ * Result is bounded to `[0, subtotalNet]`.
58
68
  */
59
- export function calculateCouponDiscountNet(subtotalNet, coupon) {
69
+ export function calculateCouponDiscountNet(subtotalNet, coupon, subtotalGross) {
60
70
  if (subtotalNet <= 0)
61
71
  return 0;
72
+ const appliesTo = coupon.appliesTo ?? 'net';
73
+ const useGross = appliesTo === 'gross' && typeof subtotalGross === 'number' && subtotalGross > 0;
62
74
  let raw = 0;
63
75
  if (coupon.type === 'percent') {
64
76
  const pct = Math.max(0, Math.min(100, coupon.value));
65
- raw = Math.round(subtotalNet * (pct / 100));
77
+ if (useGross) {
78
+ const discountGross = Math.round(subtotalGross * (pct / 100));
79
+ raw = Math.round((discountGross * subtotalNet) / subtotalGross);
80
+ }
81
+ else {
82
+ raw = Math.round(subtotalNet * (pct / 100));
83
+ }
66
84
  }
67
85
  else if (coupon.type === 'fixed') {
68
- raw = toCents(Math.max(0, coupon.value));
86
+ const fixedCents = toCents(Math.max(0, coupon.value));
87
+ if (useGross) {
88
+ const cappedGross = Math.min(fixedCents, subtotalGross);
89
+ raw = Math.round((cappedGross * subtotalNet) / subtotalGross);
90
+ }
91
+ else {
92
+ raw = fixedCents;
93
+ }
69
94
  }
70
95
  return Math.max(0, Math.min(raw, subtotalNet));
71
96
  }
@@ -40,8 +40,9 @@ export async function validateCoupon(input) {
40
40
  }
41
41
  const discountNet = calculateCouponDiscountNet(input.subtotalNet, {
42
42
  type: row.type,
43
- value: Number(row.value)
44
- });
43
+ value: Number(row.value),
44
+ appliesTo: (row.appliesTo ?? 'net')
45
+ }, input.subtotalGross);
45
46
  return { row, discountNet };
46
47
  }
47
48
  /**
@@ -70,6 +70,30 @@ export async function sendOrderStatusEmail(orderId, status) {
70
70
  catch (err) {
71
71
  console.error('[shop] Failed to load order coupon for email context:', err);
72
72
  }
73
+ let participants = [];
74
+ if (order.notes) {
75
+ try {
76
+ const parsed = JSON.parse(order.notes);
77
+ if (Array.isArray(parsed?.participants)) {
78
+ participants = parsed.participants
79
+ .map((p) => {
80
+ const fn = typeof p?.firstName === 'string'
81
+ ? p.firstName.trim()
82
+ : '';
83
+ const ln = typeof p?.lastName === 'string'
84
+ ? p.lastName.trim()
85
+ : '';
86
+ if (!fn && !ln)
87
+ return null;
88
+ return { firstName: fn, lastName: ln };
89
+ })
90
+ .filter((p) => p !== null);
91
+ }
92
+ }
93
+ catch {
94
+ // notes nie jest JSON-em (legacy free-form) — pomijamy
95
+ }
96
+ }
73
97
  const lang = (order.language || cms.languages[0] || 'pl');
74
98
  const subjectKey = (lang in STATUS_SUBJECTS[status] ? lang : 'pl');
75
99
  const viewUrl = /^https?:\/\//i.test(shop.orderViewUrl)
@@ -121,6 +145,12 @@ export async function sendOrderStatusEmail(orderId, status) {
121
145
  discountAmount: coupon ? formatPrice(coupon.discountAmount, order.currency) : null,
122
146
  hasDiscount: Boolean(coupon && coupon.discountAmount > 0)
123
147
  },
148
+ participants: participants.map((p, idx) => ({
149
+ number: idx + 1,
150
+ firstName: p.firstName,
151
+ lastName: p.lastName
152
+ })),
153
+ hasParticipants: participants.length > 0,
124
154
  items: items.map((i) => ({
125
155
  name: resolveI18n(i.nameSnapshot?.product
126
156
  ? { pl: i.nameSnapshot.product }
@@ -1,3 +1,13 @@
1
+ {{#if hasParticipants}}
2
+ <table style="width:100%;border-collapse:collapse;font-size:14px;margin-bottom:16px;">
3
+ <thead><tr style="background:#f4f2fa;"><th align="left" colspan="2" style="padding:8px;">Participants</th></tr></thead>
4
+ <tbody>
5
+ {{#each participants}}
6
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #eee;width:24px;color:#5B4A9E;font-weight:700;text-align:right;">{{number}}.</td><td style="padding:6px 8px;border-bottom:1px solid #eee;font-weight:600;">{{firstName}} {{lastName}}</td></tr>
7
+ {{/each}}
8
+ </tbody>
9
+ </table>
10
+ {{/if}}
1
11
  <table style="width:100%;border-collapse:collapse;font-size:14px;">
2
12
  <thead><tr style="background:#f4f2fa;"><th align="left" style="padding:8px;">Item</th><th style="padding:8px;">Qty</th><th align="right" style="padding:8px;">Total</th></tr></thead>
3
13
  <tbody>
@@ -1,3 +1,13 @@
1
+ {{#if hasParticipants}}
2
+ <table style="width:100%;border-collapse:collapse;font-size:14px;margin-bottom:16px;">
3
+ <thead><tr style="background:#f4f2fa;"><th align="left" colspan="2" style="padding:8px;">Uczestnicy</th></tr></thead>
4
+ <tbody>
5
+ {{#each participants}}
6
+ <tr><td style="padding:6px 8px;border-bottom:1px solid #eee;width:24px;color:#5B4A9E;font-weight:700;text-align:right;">{{number}}.</td><td style="padding:6px 8px;border-bottom:1px solid #eee;font-weight:600;">{{firstName}} {{lastName}}</td></tr>
7
+ {{/each}}
8
+ </tbody>
9
+ </table>
10
+ {{/if}}
1
11
  <table style="width:100%;border-collapse:collapse;font-size:14px;">
2
12
  <thead><tr style="background:#f4f2fa;"><th align="left" style="padding:8px;">Pozycja</th><th style="padding:8px;">Ilość</th><th align="right" style="padding:8px;">Suma</th></tr></thead>
3
13
  <tbody>
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,16 @@
1
+ export const update = {
2
+ version: '0.35.0',
3
+ date: '2026-06-05',
4
+ description: 'Kupon `appliesTo` (netto/brutto), uczestnicy w `notes` (admin + email), Calendar+Popover w polach `date`/`datetime` (admin), `api.ts` bez `FormEntryMap` gdy projekt nie ma form. Additive only — domyślne zachowania bez zmian.',
5
+ features: [
6
+ '`shop_coupons` dostaje kolumnę `applies_to` (enum `net`/`gross`, default `net`). `calculateCouponDiscountNet()` (`$lib/shop/pricing.ts`) liczy zniżkę od brutto (konwersja do netto przez średnią ważoną VAT) gdy `appliesTo="gross"` — działa zarówno dla `percent`, jak i `fixed`. Storage rabatu w bazie nadal w netto (canonical). Domyślnie `net` — istniejące kupony zachowują obecne zachowanie.',
7
+ 'Admin coupon form (`coupon-form.svelte`) zyskuje toggle „Rabat dotyczy: netto / brutto" wzorowany na `inputMode` w `shop-field.svelte` — z helper textem objaśniającym wpływ na cenę zamówienia.',
8
+ '`order.notes` jest teraz traktowane jako opcjonalny JSON (`{ org?, orgName?, participants?: Array<{firstName, lastName}> }`) w admin order detail (`shop-order-detail-page.svelte`) oraz w kontekście emaila (`items.pl.html`/`items.en.html` przez `email.ts`). Plain-text notatki nadal renderują się raw — JSON wykrywany przez `try/catch` parse + shape guard.',
9
+ 'Admin pola `date` i `datetime` (`date-field.svelte`, `datetime-field.svelte`) używają `bits-ui` Calendar w `Popover` zamiast natywnych `<input type="date|datetime-local">` — kalendarz z dropdownem miesiąca/roku, dwa Selecty (godzina 00–23, minuta 00/15/30/45) dla datetime, locale `pl-PL`/`en-GB` z `interfaceLanguage`. Output bez zmian: ISO datetime (`datetime`) / `YYYY-MM-DDT00:00:00.000Z` (`date`).'
10
+ ],
11
+ fixes: [
12
+ '`generateAPI()` (`src/lib/core/server/generator/generator.ts`) nie wstrzykuje już importu `FormEntryMap` / `createFormSubmission` w wygenerowanym `api.ts`, gdy projekt nie ma zdefiniowanych form (`config.forms` puste). Wcześniej projekty bez form miały martwy import, który psuł type-check / build.'
13
+ ],
14
+ breakingChanges: [],
15
+ sql: `ALTER TABLE shop_coupons ADD COLUMN IF NOT EXISTS applies_to text NOT NULL DEFAULT 'net';`
16
+ };
@@ -65,6 +65,7 @@ import { update as update0270 } from './0.27.0/index.js';
65
65
  import { update as update0280 } from './0.28.0/index.js';
66
66
  import { update as update0340 } from './0.34.0/index.js';
67
67
  import { update as update0341 } from './0.34.1/index.js';
68
+ import { update as update0350 } from './0.35.0/index.js';
68
69
  export const updates = [
69
70
  update0065,
70
71
  update0066,
@@ -132,7 +133,8 @@ export const updates = [
132
133
  update0270,
133
134
  update0280,
134
135
  update0340,
135
- update0341
136
+ update0341,
137
+ update0350
136
138
  ];
137
139
  export const getUpdatesFrom = (fromVersion) => {
138
140
  const fromParts = fromVersion.split('.').map(Number);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -1,5 +0,0 @@
1
- export function hello_world(inputs: {
2
- name: NonNullable<unknown>;
3
- }, options?: {
4
- locale?: "en" | "pl";
5
- }): string;
@@ -1,33 +0,0 @@
1
- /* eslint-disable */
2
- import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
3
-
4
- const en_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
5
- return `Hello, ${i.name} from en!`
6
- };
7
-
8
- const pl_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
9
- return `Hello, ${i.name} from pl!`
10
- };
11
-
12
- /**
13
- * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
14
- *
15
- * - Changing this function will be over-written by the next build.
16
- *
17
- * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
18
- * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
19
- *
20
- * @param {{ name: NonNullable<unknown> }} inputs
21
- * @param {{ locale?: "en" | "pl" }} options
22
- * @returns {string}
23
- */
24
- /* @__NO_SIDE_EFFECTS__ */
25
- export const hello_world = (inputs, options = {}) => {
26
- if (experimentalMiddlewareLocaleSplitting && isServer === false) {
27
- return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
28
- }
29
- const locale = options.locale ?? getLocale()
30
- trackMessageCall("hello_world", locale)
31
- if (locale === "en") return en_hello_world(inputs)
32
- return pl_hello_world(inputs)
33
- };
@@ -1,16 +0,0 @@
1
- export { login_hello as login.hello };
2
- /**
3
- * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
4
- *
5
- * - Changing this function will be over-written by the next build.
6
- *
7
- * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
8
- * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
9
- *
10
- * @param {{}} inputs
11
- * @param {{ locale?: "en" | "pl" }} options
12
- * @returns {string}
13
- */
14
- declare function login_hello(inputs?: {}, options?: {
15
- locale?: "en" | "pl";
16
- }): string;
@@ -1,34 +0,0 @@
1
- /* eslint-disable */
2
- import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
3
-
4
- const en_login_hello = /** @type {(inputs: {}) => string} */ () => {
5
- return `Welcome back`
6
- };
7
-
8
- const pl_login_hello = /** @type {(inputs: {}) => string} */ () => {
9
- return `Witaj ponownie`
10
- };
11
-
12
- /**
13
- * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
14
- *
15
- * - Changing this function will be over-written by the next build.
16
- *
17
- * - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
18
- * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
19
- *
20
- * @param {{}} inputs
21
- * @param {{ locale?: "en" | "pl" }} options
22
- * @returns {string}
23
- */
24
- /* @__NO_SIDE_EFFECTS__ */
25
- const login_hello = (inputs = {}, options = {}) => {
26
- if (experimentalMiddlewareLocaleSplitting && isServer === false) {
27
- return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
28
- }
29
- const locale = options.locale ?? getLocale()
30
- trackMessageCall("login_hello", locale)
31
- if (locale === "en") return en_login_hello(inputs)
32
- return pl_login_hello(inputs)
33
- };
34
- export { login_hello as "login.hello" }