@hartl-services/medusa-storefront 0.4.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.
@@ -837,7 +837,7 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
837
837
  affiliate_id: string;
838
838
  period_start: string;
839
839
  period_end: string;
840
- status: "rejected" | "draft" | "approved" | "submitted";
840
+ status: "rejected" | "approved" | "draft" | "submitted";
841
841
  draft_order_id: string | null;
842
842
  order_id: string | null;
843
843
  submitted_at: string | null;
@@ -851,6 +851,10 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
851
851
  confirmed_sale_quantity: number;
852
852
  requires_admin_clarification: boolean;
853
853
  non_sale_reason: string | null;
854
+ non_sale_quantities: {
855
+ reason: "expired" | "damaged" | "returned" | "correction";
856
+ quantity: number;
857
+ }[];
854
858
  resolved_unit_price: number | null;
855
859
  discounted_line_total: number | null;
856
860
  currency_code: string | null;
@@ -866,7 +870,7 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
866
870
  affiliate_id: string;
867
871
  period_start: string;
868
872
  period_end: string;
869
- status: "rejected" | "draft" | "approved" | "submitted";
873
+ status: "rejected" | "approved" | "draft" | "submitted";
870
874
  draft_order_id: string | null;
871
875
  order_id: string | null;
872
876
  submitted_at: string | null;
@@ -880,6 +884,10 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
880
884
  confirmed_sale_quantity: number;
881
885
  requires_admin_clarification: boolean;
882
886
  non_sale_reason: string | null;
887
+ non_sale_quantities: {
888
+ reason: "expired" | "damaged" | "returned" | "correction";
889
+ quantity: number;
890
+ }[];
883
891
  resolved_unit_price: number | null;
884
892
  discounted_line_total: number | null;
885
893
  currency_code: string | null;
@@ -896,7 +904,7 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
896
904
  affiliate_id: string;
897
905
  period_start: string;
898
906
  period_end: string;
899
- status: "rejected" | "draft" | "approved" | "submitted";
907
+ status: "rejected" | "approved" | "draft" | "submitted";
900
908
  draft_order_id: string | null;
901
909
  order_id: string | null;
902
910
  submitted_at: string | null;
@@ -910,6 +918,10 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
910
918
  confirmed_sale_quantity: number;
911
919
  requires_admin_clarification: boolean;
912
920
  non_sale_reason: string | null;
921
+ non_sale_quantities: {
922
+ reason: "expired" | "damaged" | "returned" | "correction";
923
+ quantity: number;
924
+ }[];
913
925
  resolved_unit_price: number | null;
914
926
  discounted_line_total: number | null;
915
927
  currency_code: string | null;
@@ -926,7 +938,7 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
926
938
  affiliate_id: string;
927
939
  period_start: string;
928
940
  period_end: string;
929
- status: "rejected" | "draft" | "approved" | "submitted";
941
+ status: "rejected" | "approved" | "draft" | "submitted";
930
942
  draft_order_id: string | null;
931
943
  order_id: string | null;
932
944
  submitted_at: string | null;
@@ -940,6 +952,10 @@ export declare function createStorefrontBrowser(config: StorefrontBrowserConfig)
940
952
  confirmed_sale_quantity: number;
941
953
  requires_admin_clarification: boolean;
942
954
  non_sale_reason: string | null;
955
+ non_sale_quantities: {
956
+ reason: "expired" | "damaged" | "returned" | "correction";
957
+ quantity: number;
958
+ }[];
943
959
  resolved_unit_price: number | null;
944
960
  discounted_line_total: number | null;
945
961
  currency_code: string | null;
@@ -695,7 +695,7 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
695
695
  affiliate_id: string;
696
696
  period_start: string;
697
697
  period_end: string;
698
- status: "rejected" | "draft" | "approved" | "submitted";
698
+ status: "rejected" | "approved" | "draft" | "submitted";
699
699
  draft_order_id: string | null;
700
700
  order_id: string | null;
701
701
  submitted_at: string | null;
@@ -709,6 +709,10 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
709
709
  confirmed_sale_quantity: number;
710
710
  requires_admin_clarification: boolean;
711
711
  non_sale_reason: string | null;
712
+ non_sale_quantities: {
713
+ reason: "expired" | "damaged" | "returned" | "correction";
714
+ quantity: number;
715
+ }[];
712
716
  resolved_unit_price: number | null;
713
717
  discounted_line_total: number | null;
714
718
  currency_code: string | null;
@@ -724,7 +728,7 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
724
728
  affiliate_id: string;
725
729
  period_start: string;
726
730
  period_end: string;
727
- status: "rejected" | "draft" | "approved" | "submitted";
731
+ status: "rejected" | "approved" | "draft" | "submitted";
728
732
  draft_order_id: string | null;
729
733
  order_id: string | null;
730
734
  submitted_at: string | null;
@@ -738,6 +742,10 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
738
742
  confirmed_sale_quantity: number;
739
743
  requires_admin_clarification: boolean;
740
744
  non_sale_reason: string | null;
745
+ non_sale_quantities: {
746
+ reason: "expired" | "damaged" | "returned" | "correction";
747
+ quantity: number;
748
+ }[];
741
749
  resolved_unit_price: number | null;
742
750
  discounted_line_total: number | null;
743
751
  currency_code: string | null;
@@ -754,7 +762,7 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
754
762
  affiliate_id: string;
755
763
  period_start: string;
756
764
  period_end: string;
757
- status: "rejected" | "draft" | "approved" | "submitted";
765
+ status: "rejected" | "approved" | "draft" | "submitted";
758
766
  draft_order_id: string | null;
759
767
  order_id: string | null;
760
768
  submitted_at: string | null;
@@ -768,6 +776,10 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
768
776
  confirmed_sale_quantity: number;
769
777
  requires_admin_clarification: boolean;
770
778
  non_sale_reason: string | null;
779
+ non_sale_quantities: {
780
+ reason: "expired" | "damaged" | "returned" | "correction";
781
+ quantity: number;
782
+ }[];
771
783
  resolved_unit_price: number | null;
772
784
  discounted_line_total: number | null;
773
785
  currency_code: string | null;
@@ -784,7 +796,7 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
784
796
  affiliate_id: string;
785
797
  period_start: string;
786
798
  period_end: string;
787
- status: "rejected" | "draft" | "approved" | "submitted";
799
+ status: "rejected" | "approved" | "draft" | "submitted";
788
800
  draft_order_id: string | null;
789
801
  order_id: string | null;
790
802
  submitted_at: string | null;
@@ -798,6 +810,10 @@ export declare function createQueryOptions({ actions, affiliateApiPaths, cart, r
798
810
  confirmed_sale_quantity: number;
799
811
  requires_admin_clarification: boolean;
800
812
  non_sale_reason: string | null;
813
+ non_sale_quantities: {
814
+ reason: "expired" | "damaged" | "returned" | "correction";
815
+ quantity: number;
816
+ }[];
801
817
  resolved_unit_price: number | null;
802
818
  discounted_line_total: number | null;
803
819
  currency_code: string | null;
@@ -0,0 +1,68 @@
1
+ import { ConsignmentNonSaleReasonSchema } from "@hartl-services/medusa-contracts/schemas/affiliate";
2
+ import type { GetConsignmentInventoryResponse, StoreConsignmentReport, SubmitConsignmentReportRequest } from "@hartl-services/medusa-contracts/types/affiliate-store";
3
+ /** A reason a missing quantity was not sold (the contract's list). */
4
+ export type ConsignmentNonSaleReason = (typeof ConsignmentNonSaleReasonSchema.options)[number];
5
+ /** The reason of a split row: "" while the partner has not chosen one yet. */
6
+ export type ConsignmentSplitReason = "" | "sold" | ConsignmentNonSaleReason;
7
+ /** One part of a line's shortage (expected minus counted) with its reason. */
8
+ export interface ConsignmentSplitRow {
9
+ quantity: number;
10
+ reason: ConsignmentSplitReason;
11
+ }
12
+ /** One stock line of the report form. `expected_quantity` is a reference, never sent. */
13
+ export interface ConsignmentReportFormLine {
14
+ inventory_item_id: string;
15
+ batch_id: string | null;
16
+ expected_quantity: number;
17
+ counted_quantity: number;
18
+ split: ConsignmentSplitRow[];
19
+ }
20
+ /** The monthly report form's values; `toSubmitConsignmentReportRequest` turns them into the request. */
21
+ export interface ConsignmentReportFormValues {
22
+ period_start: string;
23
+ submission_idempotency_key: string;
24
+ counted_batches: ConsignmentReportFormLine[];
25
+ }
26
+ type ConsignmentReportLine = StoreConsignmentReport["lines"][number];
27
+ /** Labels for choosing a reason ("sold" included). */
28
+ export declare const consignmentSplitReasonLabels: Record<"sold" | ConsignmentNonSaleReason, string>;
29
+ /** The same reasons as part of a sentence ("1 verkauft, 1 retourniert"). */
30
+ export declare const consignmentSplitReasonSummaryLabels: Record<"sold" | ConsignmentNonSaleReason, string>;
31
+ /** Options for the reason select, "Verkauft" first, then the contract's reasons in their order. */
32
+ export declare function getConsignmentSplitReasonOptions(): {
33
+ label: string;
34
+ value: "sold" | ConsignmentNonSaleReason;
35
+ }[];
36
+ /** What is missing against the expected quantity; an overcount has no shortage. */
37
+ export declare function getConsignmentShortage(expectedQuantity: number, countedQuantity: number): number;
38
+ /** The default split: the whole shortage sold; no rows without a shortage. */
39
+ export declare function createInitialConsignmentSplit(shortage: number): ConsignmentSplitRow[];
40
+ /**
41
+ * Re-balances a line's split after the counted quantity changed: no shortage
42
+ * clears it, a shortage without rows starts the default row, exactly one row
43
+ * follows the new shortage, several rows stay as the partner set them (the
44
+ * validation then asks to fix the sum).
45
+ */
46
+ export declare function rebalanceConsignmentSplit(split: readonly ConsignmentSplitRow[], shortage: number): ConsignmentSplitRow[];
47
+ /** The row "Weiterer Grund" adds: the part of the shortage not yet assigned, no reason chosen. */
48
+ export declare function createAdditionalConsignmentSplitRow(split: readonly ConsignmentSplitRow[], shortage: number): ConsignmentSplitRow;
49
+ /** The German hint while a line's split is unusable, otherwise null. Lines without a shortage have none. */
50
+ export declare function validateConsignmentLineSplit(line: Pick<ConsignmentReportFormLine, "counted_quantity" | "expected_quantity" | "split">): string | null;
51
+ /** True when every line with a shortage has a usable split. */
52
+ export declare function isConsignmentReportSplitValid(lines: readonly ConsignmentReportFormLine[]): boolean;
53
+ /** The form's starting values: every batch counted as expected, so no shortage and no split. */
54
+ export declare function createConsignmentReportFormValues(inventory: GetConsignmentInventoryResponse, submissionIdempotencyKey: string): ConsignmentReportFormValues;
55
+ /**
56
+ * Maps the form to the request: "Verkauft" rows are not sent (the rest counts
57
+ * as sold), the other rows go merged per reason into `non_sale_quantities`,
58
+ * which is omitted without such rows or without a shortage.
59
+ */
60
+ export declare function toSubmitConsignmentReportRequest(values: ConsignmentReportFormValues): SubmitConsignmentReportRequest;
61
+ /**
62
+ * "Gemeldet: 1 verkauft, 1 retourniert" (approved reports: "Freigegeben: …")
63
+ * for a report line with a shortage; null when there is nothing to say.
64
+ * A line without `non_sale_quantities` but with one of the reasons in
65
+ * `non_sale_reason` assigns the rest of the shortage to that reason.
66
+ */
67
+ export declare function summarizeConsignmentReportLine(status: StoreConsignmentReport["status"], line: ConsignmentReportLine): string | null;
68
+ export {};
@@ -0,0 +1,151 @@
1
+ import { ConsignmentNonSaleReasonSchema } from "@hartl-services/medusa-contracts/schemas/affiliate";
2
+ const nonSaleReasons = ConsignmentNonSaleReasonSchema.options;
3
+ /** Labels for choosing a reason ("sold" included). */
4
+ export const consignmentSplitReasonLabels = {
5
+ sold: "Verkauft",
6
+ damaged: "Beschädigt",
7
+ expired: "Abgelaufen",
8
+ returned: "Retourniert",
9
+ correction: "Korrektur",
10
+ };
11
+ /** The same reasons as part of a sentence ("1 verkauft, 1 retourniert"). */
12
+ export const consignmentSplitReasonSummaryLabels = {
13
+ sold: "verkauft",
14
+ damaged: "beschädigt",
15
+ expired: "abgelaufen",
16
+ returned: "retourniert",
17
+ correction: "korrigiert",
18
+ };
19
+ /** Options for the reason select, "Verkauft" first, then the contract's reasons in their order. */
20
+ export function getConsignmentSplitReasonOptions() {
21
+ return ["sold", ...nonSaleReasons].map((reason) => ({ label: consignmentSplitReasonLabels[reason], value: reason }));
22
+ }
23
+ /** What is missing against the expected quantity; an overcount has no shortage. */
24
+ export function getConsignmentShortage(expectedQuantity, countedQuantity) {
25
+ const shortage = expectedQuantity - countedQuantity;
26
+ return Number.isFinite(shortage) && shortage > 0 ? Math.floor(shortage) : 0;
27
+ }
28
+ /** The default split: the whole shortage sold; no rows without a shortage. */
29
+ export function createInitialConsignmentSplit(shortage) {
30
+ return shortage > 0 ? [{ quantity: shortage, reason: "sold" }] : [];
31
+ }
32
+ /**
33
+ * Re-balances a line's split after the counted quantity changed: no shortage
34
+ * clears it, a shortage without rows starts the default row, exactly one row
35
+ * follows the new shortage, several rows stay as the partner set them (the
36
+ * validation then asks to fix the sum).
37
+ */
38
+ export function rebalanceConsignmentSplit(split, shortage) {
39
+ if (shortage <= 0) {
40
+ return [];
41
+ }
42
+ if (split.length === 0) {
43
+ return createInitialConsignmentSplit(shortage);
44
+ }
45
+ const [only] = split;
46
+ if (split.length === 1 && only) {
47
+ return [{ ...only, quantity: shortage }];
48
+ }
49
+ return split.map((row) => ({ ...row }));
50
+ }
51
+ /** The row "Weiterer Grund" adds: the part of the shortage not yet assigned, no reason chosen. */
52
+ export function createAdditionalConsignmentSplitRow(split, shortage) {
53
+ const assigned = split.reduce((sum, row) => sum + (Number.isFinite(row.quantity) ? row.quantity : 0), 0);
54
+ return { quantity: Math.max(0, shortage - assigned), reason: "" };
55
+ }
56
+ /** The German hint while a line's split is unusable, otherwise null. Lines without a shortage have none. */
57
+ export function validateConsignmentLineSplit(line) {
58
+ const shortage = getConsignmentShortage(line.expected_quantity, line.counted_quantity);
59
+ if (shortage <= 0) {
60
+ return null;
61
+ }
62
+ if (line.split.some((row) => !Number.isInteger(row.quantity) || row.quantity < 1)) {
63
+ return "Jede Teilmenge muss eine ganze Zahl ab 1 sein.";
64
+ }
65
+ if (line.split.some((row) => row.reason === "")) {
66
+ return "Bitte wähle für jede Teilmenge einen Grund.";
67
+ }
68
+ const assigned = line.split.reduce((sum, row) => sum + row.quantity, 0);
69
+ if (assigned !== shortage) {
70
+ return `Die Teilmengen ergeben ${assigned}, die Fehlmenge beträgt ${shortage}. Bitte teile sie genau auf.`;
71
+ }
72
+ return null;
73
+ }
74
+ /** True when every line with a shortage has a usable split. */
75
+ export function isConsignmentReportSplitValid(lines) {
76
+ return lines.every((line) => validateConsignmentLineSplit(line) === null);
77
+ }
78
+ /** The form's starting values: every batch counted as expected, so no shortage and no split. */
79
+ export function createConsignmentReportFormValues(inventory, submissionIdempotencyKey) {
80
+ return {
81
+ period_start: inventory.period_start,
82
+ submission_idempotency_key: submissionIdempotencyKey,
83
+ counted_batches: inventory.inventory.map((line) => ({
84
+ inventory_item_id: line.inventory_item_id,
85
+ batch_id: line.batch_id,
86
+ expected_quantity: line.expected_quantity,
87
+ counted_quantity: line.expected_quantity,
88
+ split: createInitialConsignmentSplit(getConsignmentShortage(line.expected_quantity, line.expected_quantity)),
89
+ })),
90
+ };
91
+ }
92
+ /**
93
+ * Maps the form to the request: "Verkauft" rows are not sent (the rest counts
94
+ * as sold), the other rows go merged per reason into `non_sale_quantities`,
95
+ * which is omitted without such rows or without a shortage.
96
+ */
97
+ export function toSubmitConsignmentReportRequest(values) {
98
+ return {
99
+ period_start: values.period_start,
100
+ submission_idempotency_key: values.submission_idempotency_key,
101
+ counted_batches: values.counted_batches.map((line) => {
102
+ const base = {
103
+ inventory_item_id: line.inventory_item_id,
104
+ batch_id: line.batch_id,
105
+ counted_quantity: line.counted_quantity,
106
+ };
107
+ if (getConsignmentShortage(line.expected_quantity, line.counted_quantity) <= 0) {
108
+ return base;
109
+ }
110
+ const nonSaleQuantities = nonSaleReasons
111
+ .map((reason) => ({
112
+ reason,
113
+ quantity: line.split.filter((row) => row.reason === reason).reduce((sum, row) => sum + row.quantity, 0),
114
+ }))
115
+ .filter((entry) => entry.quantity > 0);
116
+ return nonSaleQuantities.length > 0 ? { ...base, non_sale_quantities: nonSaleQuantities } : base;
117
+ }),
118
+ };
119
+ }
120
+ /**
121
+ * "Gemeldet: 1 verkauft, 1 retourniert" (approved reports: "Freigegeben: …")
122
+ * for a report line with a shortage; null when there is nothing to say.
123
+ * A line without `non_sale_quantities` but with one of the reasons in
124
+ * `non_sale_reason` assigns the rest of the shortage to that reason.
125
+ */
126
+ export function summarizeConsignmentReportLine(status, line) {
127
+ const shortage = getConsignmentShortage(line.expected_quantity, line.counted_quantity);
128
+ if (shortage <= 0) {
129
+ return null;
130
+ }
131
+ const sold = line.confirmed_sale_quantity > 0 ? line.confirmed_sale_quantity : 0;
132
+ const nonSale = line.non_sale_quantities.map((entry) => ({ reason: entry.reason, quantity: entry.quantity }));
133
+ const legacyReason = nonSaleReasons.find((reason) => reason === line.non_sale_reason);
134
+ if (nonSale.length === 0 && legacyReason && shortage - sold > 0) {
135
+ nonSale.push({ reason: legacyReason, quantity: shortage - sold });
136
+ }
137
+ // An approved line can carry a corrected sold quantity next to the original
138
+ // non-sold parts; what neither explains is shown as a correction.
139
+ const remainder = shortage - sold - nonSale.reduce((sum, entry) => sum + entry.quantity, 0);
140
+ if (status === "approved" && remainder > 0) {
141
+ nonSale.push({ reason: "correction", quantity: remainder });
142
+ }
143
+ const parts = [
144
+ ...(sold > 0 ? [`${sold} ${consignmentSplitReasonSummaryLabels.sold}`] : []),
145
+ ...nonSale.map((entry) => `${entry.quantity} ${consignmentSplitReasonSummaryLabels[entry.reason]}`),
146
+ ];
147
+ if (parts.length === 0) {
148
+ return null;
149
+ }
150
+ return `${status === "approved" ? "Freigegeben" : "Gemeldet"}: ${parts.join(", ")}`;
151
+ }
@@ -6,6 +6,7 @@ export * from "./affiliate/affiliate-coupon-label.js";
6
6
  export * from "./affiliate/affiliate-date-range.js";
7
7
  export * from "./affiliate/affiliate-rate-presentation.js";
8
8
  export * from "./affiliate/consignment-line-labels.js";
9
+ export * from "./affiliate/consignment-report-split.js";
9
10
  export * from "./affiliate/partner-company-address.js";
10
11
  export * from "./auth/customer-sign-in.js";
11
12
  export * from "./auth/link-token.js";
@@ -7,6 +7,7 @@ export * from "./affiliate/affiliate-coupon-label.js";
7
7
  export * from "./affiliate/affiliate-date-range.js";
8
8
  export * from "./affiliate/affiliate-rate-presentation.js";
9
9
  export * from "./affiliate/consignment-line-labels.js";
10
+ export * from "./affiliate/consignment-report-split.js";
10
11
  export * from "./affiliate/partner-company-address.js";
11
12
  export * from "./auth/customer-sign-in.js";
12
13
  export * from "./auth/link-token.js";
@@ -2,6 +2,7 @@ import type { AffiliateCouponLifecycleStatus, CreatePartnerPortalSlugRequest, Cr
2
2
  import type { CustomerAddressTaxIdentification } from "@hartl-services/medusa-contracts/types/tax-compliance-store";
3
3
  import type { HttpTypes } from "@medusajs/types";
4
4
  import type { AffiliateDateRange } from "../core/affiliate/affiliate-date-range.js";
5
+ import type { ConsignmentReportFormValues } from "../core/affiliate/consignment-report-split.js";
5
6
  import type { CustomerAddressValues } from "../core/form-values.js";
6
7
  import { useCustomerAddressBook } from "./use-account.js";
7
8
  /**
@@ -527,7 +528,7 @@ export declare function useAffiliateConsignment(): {
527
528
  affiliate_id: string;
528
529
  period_start: string;
529
530
  period_end: string;
530
- status: "rejected" | "draft" | "approved" | "submitted";
531
+ status: "rejected" | "approved" | "draft" | "submitted";
531
532
  draft_order_id: string | null;
532
533
  order_id: string | null;
533
534
  submitted_at: string | null;
@@ -541,6 +542,10 @@ export declare function useAffiliateConsignment(): {
541
542
  confirmed_sale_quantity: number;
542
543
  requires_admin_clarification: boolean;
543
544
  non_sale_reason: string | null;
545
+ non_sale_quantities: {
546
+ reason: "expired" | "damaged" | "returned" | "correction";
547
+ quantity: number;
548
+ }[];
544
549
  resolved_unit_price: number | null;
545
550
  discounted_line_total: number | null;
546
551
  currency_code: string | null;
@@ -555,20 +560,13 @@ export declare function useAffiliateConsignment(): {
555
560
  submitReport(values: SubmitConsignmentReportRequest): Promise<void>;
556
561
  };
557
562
  /**
558
- * The monthly report's starting values: every batch counted as expected, with
559
- * an idempotency key for the submission (null without crypto.randomUUID - the
563
+ * The monthly report's starting values (form values, see
564
+ * `toSubmitConsignmentReportRequest` for the request): every batch counted as
565
+ * expected, so without a shortage split, with an idempotency key for the submission (null without crypto.randomUUID - the
560
566
  * shop then shows a notice instead of the form).
561
567
  */
562
568
  export declare function useConsignmentReportDefaults(inventory: GetConsignmentInventoryResponse): {
563
- defaultValues: {
564
- period_start: string;
565
- submission_idempotency_key: string;
566
- counted_batches: {
567
- inventory_item_id: string;
568
- counted_quantity: number;
569
- batch_id?: string | null | undefined;
570
- }[];
571
- };
569
+ defaultValues: ConsignmentReportFormValues;
572
570
  submissionIdempotencyKey: string | null;
573
571
  };
574
572
  export {};
@@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from "react";
6
6
  import { invalidateAffiliateQueries, resetCustomerScopedQueries } from "../browser/query-invalidation.js";
7
7
  import { getAffiliateApplicationErrorMessage } from "../core/affiliate/affiliate-application-errors.js";
8
8
  import { getCurrentUtcMonthRange } from "../core/affiliate/affiliate-date-range.js";
9
+ import { createConsignmentReportFormValues } from "../core/affiliate/consignment-report-split.js";
9
10
  import { getPartnerCompanyAddressKind, isUsablePartnerIdentification } from "../core/affiliate/partner-company-address.js";
10
11
  import { getErrorMessage } from "../core/errors.js";
11
12
  import { useStorefront } from "./context.js";
@@ -554,20 +555,13 @@ export function useAffiliateConsignment() {
554
555
  };
555
556
  }
556
557
  /**
557
- * The monthly report's starting values: every batch counted as expected, with
558
- * an idempotency key for the submission (null without crypto.randomUUID - the
558
+ * The monthly report's starting values (form values, see
559
+ * `toSubmitConsignmentReportRequest` for the request): every batch counted as
560
+ * expected, so without a shortage split, with an idempotency key for the submission (null without crypto.randomUUID - the
559
561
  * shop then shows a notice instead of the form).
560
562
  */
561
563
  export function useConsignmentReportDefaults(inventory) {
562
564
  const [submissionIdempotencyKey] = useState(createIdempotencyKey);
563
- const defaultValues = {
564
- period_start: inventory.period_start,
565
- submission_idempotency_key: submissionIdempotencyKey ?? "",
566
- counted_batches: inventory.inventory.map((line) => ({
567
- inventory_item_id: line.inventory_item_id,
568
- batch_id: line.batch_id,
569
- counted_quantity: line.expected_quantity,
570
- })),
571
- };
565
+ const defaultValues = createConsignmentReportFormValues(inventory, submissionIdempotencyKey ?? "");
572
566
  return { defaultValues, submissionIdempotencyKey };
573
567
  }
@@ -4,8 +4,8 @@
4
4
  "title": "Customer account",
5
5
  "description": "Sign-in with password or e-mail code, password reset, account verification, address book with UID check, profile, orders, the account workspace layout and the document download route.",
6
6
  "dependencies": [
7
- "@hartl-services/medusa-storefront@^0.4.0",
8
- "@hartl-services/medusa-contracts@^1.3.0",
7
+ "@hartl-services/medusa-storefront@^0.5.0",
8
+ "@hartl-services/medusa-contracts@^1.5.0",
9
9
  "@medusajs/types@^2.19.0",
10
10
  "lucide-react"
11
11
  ],
@@ -4,8 +4,8 @@
4
4
  "title": "Partner portal",
5
5
  "description": "Partner portal: gated registration with UID check, links, coupon codes, commissions with date filter, consignment reports, and its Route Handlers.",
6
6
  "dependencies": [
7
- "@hartl-services/medusa-storefront@^0.4.0",
8
- "@hartl-services/medusa-contracts@^1.3.0",
7
+ "@hartl-services/medusa-storefront@^0.5.0",
8
+ "@hartl-services/medusa-contracts@^1.5.0",
9
9
  "@medusajs/types@^2.19.0",
10
10
  "lucide-react",
11
11
  "react-day-picker"
@@ -134,13 +134,13 @@
134
134
  "path": "registry/items/affiliate/components/affiliate/affiliate-consignment-report-form.tsx",
135
135
  "target": "@components/affiliate/affiliate-consignment-report-form.tsx",
136
136
  "type": "registry:component",
137
- "content": "\"use client\"\n\nimport type { GetConsignmentInventoryResponse, SubmitConsignmentReportRequest } from \"@hartl-services/medusa-contracts/types/affiliate-store\"\n\nimport { fieldControlClassName, fieldLabelClassName } from \"@/components/form/field-styles\"\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { WorkspaceCard } from \"@/components/workspace/workspace-card\"\nimport { useAppForm } from \"@/hooks/use-app-form\"\nimport { getConsignmentLineDisplay } from \"@hartl-services/medusa-storefront/core\"\nimport { useConsignmentReportDefaults } from \"@hartl-services/medusa-storefront/react\"\n\ninterface AffiliateConsignmentReportFormProps {\n inventory: GetConsignmentInventoryResponse\n isPending?: boolean\n onSubmit: (values: SubmitConsignmentReportRequest) => Promise<void>\n}\n\nexport function AffiliateConsignmentReportForm({ inventory, isPending = false, onSubmit }: AffiliateConsignmentReportFormProps) {\n const { defaultValues, submissionIdempotencyKey } = useConsignmentReportDefaults(inventory)\n const form = useAppForm({\n defaultValues,\n onSubmit: async ({ value }) => {\n if (!submissionIdempotencyKey) {\n return\n }\n\n await onSubmit(value)\n },\n })\n\n if (!submissionIdempotencyKey) {\n return (\n <Alert variant=\"destructive\">\n <AlertTitle>Meldung kann nicht vorbereitet werden</AlertTitle>\n <AlertDescription>Dein Browser unterstützt keine sichere Kennung für diese Einreichung. Bitte aktualisiere deinen Browser und versuche es erneut.</AlertDescription>\n </Alert>\n )\n }\n\n return (\n <WorkspaceCard description=\"Trage nur die gezählte Menge je Lagerposition ein. Die erwartete Menge dient als Referenz.\" title=\"Monatsmeldung\">\n <form\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault()\n void form.handleSubmit()\n }}\n >\n <form.AppField mode=\"array\" name=\"counted_batches\">\n {(arrayField) => (\n <fieldset className=\"flex flex-col\">\n <legend className={`${fieldLabelClassName} mb-3`}>Gezählte Lagerbestände</legend>\n {arrayField.state.value.map((line, index) => {\n const quantityId = `counted-quantity-${line.inventory_item_id}-${line.batch_id ?? \"standard\"}`\n const display = getConsignmentLineDisplay(inventory.inventory[index] ?? line, \"de-DE\")\n\n return (\n <div className=\"grid gap-3 border-b py-4 first:pt-0 last:border-b-0 last:pb-0 sm:grid-cols-[minmax(0,1fr)_auto_10rem] sm:items-end sm:gap-6\" key={`${line.inventory_item_id}-${line.batch_id ?? \"\"}`}>\n <div className=\"flex flex-col gap-1\">\n <p className=\"text-sm font-medium\">{display.product ?? `Lagerposition ${line.inventory_item_id}`}</p>\n <p className=\"text-sm text-muted-foreground\">{display.batch ? `Charge ${display.batch}${display.expiryDate ? ` · MHD ${display.expiryDate}` : \"\"}` : \"Ohne Charge\"}</p>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Erwartete Menge: <span className=\"font-medium text-foreground\">{inventory.inventory[index]?.expected_quantity ?? 0}</span>\n </p>\n <form.AppField name={`counted_batches[${index}].counted_quantity`}>\n {(field) => {\n const hasErrors = field.state.meta.errors.length > 0\n\n return (\n <div className=\"flex flex-col gap-2\">\n <label className={fieldLabelClassName} htmlFor={quantityId}>\n Gezählte Menge für {display.product ?? line.inventory_item_id}\n </label>\n <Input\n aria-describedby={hasErrors ? `${quantityId}-error` : undefined}\n aria-invalid={hasErrors || undefined}\n className={fieldControlClassName}\n id={quantityId}\n inputMode=\"numeric\"\n min={0}\n onBlur={field.handleBlur}\n onChange={(event) => {\n const nextValue = event.target.value\n field.handleChange(nextValue === \"\" ? 0 : Number(nextValue))\n }}\n step={1}\n type=\"number\"\n value={field.state.value}\n />\n {hasErrors ? (\n <p className=\"text-sm text-destructive\" id={`${quantityId}-error`}>\n Menge muss eine nicht-negative ganze Zahl sein.\n </p>\n ) : null}\n </div>\n )\n }}\n </form.AppField>\n </div>\n )\n })}\n </fieldset>\n )}\n </form.AppField>\n <form.Subscribe\n selector={(state) => ({\n canSubmit: state.canSubmit,\n isSubmitting: state.isSubmitting,\n })}\n >\n {({ canSubmit, isSubmitting }) => (\n <Button className=\"self-start\" disabled={!canSubmit || isSubmitting || isPending} type=\"submit\">\n {isSubmitting || isPending ? \"Monatsmeldung wird eingereicht…\" : \"Monatsmeldung einreichen\"}\n </Button>\n )}\n </form.Subscribe>\n </form>\n </WorkspaceCard>\n )\n}\n"
137
+ "content": "\"use client\"\n\nimport type { GetConsignmentInventoryResponse, SubmitConsignmentReportRequest } from \"@hartl-services/medusa-contracts/types/affiliate-store\"\n\nimport { fieldControlClassName, fieldLabelClassName } from \"@/components/form/field-styles\"\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { WorkspaceCard } from \"@/components/workspace/workspace-card\"\nimport { useAppForm } from \"@/hooks/use-app-form\"\nimport {\n createAdditionalConsignmentSplitRow,\n getConsignmentLineDisplay,\n getConsignmentShortage,\n getConsignmentSplitReasonOptions,\n isConsignmentReportSplitValid,\n rebalanceConsignmentSplit,\n toSubmitConsignmentReportRequest,\n validateConsignmentLineSplit,\n} from \"@hartl-services/medusa-storefront/core\"\nimport { useConsignmentReportDefaults } from \"@hartl-services/medusa-storefront/react\"\n\nconst splitReasonOptions = getConsignmentSplitReasonOptions()\n\ninterface AffiliateConsignmentReportFormProps {\n inventory: GetConsignmentInventoryResponse\n isPending?: boolean\n onSubmit: (values: SubmitConsignmentReportRequest) => Promise<void>\n}\n\nexport function AffiliateConsignmentReportForm({ inventory, isPending = false, onSubmit }: AffiliateConsignmentReportFormProps) {\n const { defaultValues, submissionIdempotencyKey } = useConsignmentReportDefaults(inventory)\n const form = useAppForm({\n defaultValues,\n validators: {\n onSubmit: ({ value }) => (isConsignmentReportSplitValid(value.counted_batches) ? undefined : \"Bitte teile jede Fehlmenge genau auf.\"),\n },\n onSubmit: async ({ value }) => {\n if (!submissionIdempotencyKey) {\n return\n }\n\n await onSubmit(toSubmitConsignmentReportRequest(value))\n },\n })\n\n if (!submissionIdempotencyKey) {\n return (\n <Alert variant=\"destructive\">\n <AlertTitle>Meldung kann nicht vorbereitet werden</AlertTitle>\n <AlertDescription>Dein Browser unterstützt keine sichere Kennung für diese Einreichung. Bitte aktualisiere deinen Browser und versuche es erneut.</AlertDescription>\n </Alert>\n )\n }\n\n return (\n <WorkspaceCard description=\"Trage die gezählte Menge je Lagerposition ein. Fehlt etwas gegenüber der erwarteten Menge, teile die Fehlmenge nach Gründen auf.\" title=\"Monatsmeldung\">\n <form\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault()\n void form.handleSubmit()\n }}\n >\n <form.AppField mode=\"array\" name=\"counted_batches\">\n {(arrayField) => (\n <fieldset className=\"flex flex-col\">\n <legend className={`${fieldLabelClassName} mb-3`}>Gezählte Lagerbestände</legend>\n {arrayField.state.value.map((line, index) => {\n const quantityId = `counted-quantity-${line.inventory_item_id}-${line.batch_id ?? \"standard\"}`\n const display = getConsignmentLineDisplay(inventory.inventory[index] ?? line, \"de-DE\")\n const splitHintId = `${quantityId}-split-hint`\n\n return (\n <div className=\"flex flex-col gap-4 border-b py-4 first:pt-0 last:border-b-0 last:pb-0\" key={`${line.inventory_item_id}-${line.batch_id ?? \"\"}`}>\n <div className=\"grid gap-3 sm:grid-cols-[minmax(0,1fr)_auto_10rem] sm:items-end sm:gap-6\">\n <div className=\"flex flex-col gap-1\">\n <p className=\"text-sm font-medium\">{display.product ?? `Lagerposition ${line.inventory_item_id}`}</p>\n <p className=\"text-sm text-muted-foreground\">{display.batch ? `Charge ${display.batch}${display.expiryDate ? `, MHD ${display.expiryDate}` : \"\"}` : \"Ohne Charge\"}</p>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n Erwartete Menge: <span className=\"font-medium text-foreground\">{line.expected_quantity}</span>\n </p>\n <form.AppField\n validators={{\n onChange: ({ value }) => (Number.isInteger(value) && value >= 0 ? undefined : \"Menge muss eine nicht-negative ganze Zahl sein.\"),\n }}\n listeners={{\n onChange: ({ fieldApi }) => {\n const expected = fieldApi.form.getFieldValue(`counted_batches[${index}].expected_quantity`)\n const split = fieldApi.form.getFieldValue(`counted_batches[${index}].split`)\n\n fieldApi.form.setFieldValue(`counted_batches[${index}].split`, rebalanceConsignmentSplit(split, getConsignmentShortage(expected, fieldApi.state.value)))\n },\n }}\n name={`counted_batches[${index}].counted_quantity`}\n >\n {(field) => {\n const hasErrors = field.state.meta.errors.length > 0\n\n return (\n <div className=\"flex flex-col gap-2\">\n <label className={fieldLabelClassName} htmlFor={quantityId}>\n Gezählte Menge für {display.product ?? line.inventory_item_id}\n </label>\n <Input\n aria-describedby={hasErrors ? `${quantityId}-error` : undefined}\n aria-invalid={hasErrors || undefined}\n className={fieldControlClassName}\n id={quantityId}\n inputMode=\"numeric\"\n min={0}\n onBlur={field.handleBlur}\n onChange={(event) => {\n const nextValue = event.target.value\n field.handleChange(nextValue === \"\" ? 0 : Number(nextValue))\n }}\n step={1}\n type=\"number\"\n value={field.state.value}\n />\n {hasErrors ? (\n <p className=\"text-sm text-destructive\" id={`${quantityId}-error`}>\n Menge muss eine nicht-negative ganze Zahl sein.\n </p>\n ) : null}\n </div>\n )\n }}\n </form.AppField>\n </div>\n <form.Subscribe selector={(state) => state.values.counted_batches[index]}>\n {(currentLine) => {\n if (!currentLine) {\n return null\n }\n\n const shortage = getConsignmentShortage(currentLine.expected_quantity, currentLine.counted_quantity)\n const splitHint = validateConsignmentLineSplit(currentLine)\n\n if (shortage <= 0) {\n return null\n }\n\n return (\n <form.AppField mode=\"array\" name={`counted_batches[${index}].split`}>\n {(splitField) => (\n <div aria-describedby={splitHint ? splitHintId : undefined} className=\"flex flex-col gap-3 rounded-lg border p-4\" role=\"group\" aria-label={`Fehlmenge aufteilen für ${display.product ?? line.inventory_item_id}`}>\n <p className=\"text-sm font-medium\">Fehlmenge aufteilen: {shortage} fehlen gegenüber der erwarteten Menge.</p>\n {currentLine.split.map((_row, rowIndex) => (\n <div className=\"grid gap-3 sm:grid-cols-[6rem_minmax(0,1fr)_auto] sm:items-end\" key={rowIndex}>\n <form.AppField name={`counted_batches[${index}].split[${rowIndex}].quantity`}>\n {(field) => (\n <div className=\"flex flex-col gap-2\">\n <label className={fieldLabelClassName} htmlFor={`${quantityId}-split-${rowIndex}`}>\n Menge\n </label>\n <Input\n className={fieldControlClassName}\n id={`${quantityId}-split-${rowIndex}`}\n inputMode=\"numeric\"\n min={1}\n onBlur={field.handleBlur}\n onChange={(event) => {\n const nextValue = event.target.value\n field.handleChange(nextValue === \"\" ? 0 : Number(nextValue))\n }}\n step={1}\n type=\"number\"\n value={field.state.value}\n />\n </div>\n )}\n </form.AppField>\n <form.AppField name={`counted_batches[${index}].split[${rowIndex}].reason`}>\n {(field) => <field.FormSelectField label=\"Grund\" options={splitReasonOptions} placeholder=\"Grund wählen\" />}\n </form.AppField>\n <Button onClick={() => void splitField.removeValue(rowIndex)} size=\"sm\" type=\"button\" variant=\"outline\">\n Entfernen\n </Button>\n </div>\n ))}\n <Button className=\"self-start\" onClick={() => splitField.pushValue(createAdditionalConsignmentSplitRow(currentLine.split, shortage))} size=\"sm\" type=\"button\" variant=\"outline\">\n Weiterer Grund\n </Button>\n {splitHint ? (\n <p className=\"text-sm text-destructive\" id={splitHintId} role=\"alert\">\n {splitHint}\n </p>\n ) : null}\n </div>\n )}\n </form.AppField>\n )\n }}\n </form.Subscribe>\n </div>\n )\n })}\n </fieldset>\n )}\n </form.AppField>\n <form.Subscribe\n selector={(state) => ({\n canSubmit: state.canSubmit && isConsignmentReportSplitValid(state.values.counted_batches),\n isSubmitting: state.isSubmitting,\n })}\n >\n {({ canSubmit, isSubmitting }) => (\n <Button className=\"self-start\" disabled={!canSubmit || isSubmitting || isPending} type=\"submit\">\n {isSubmitting || isPending ? \"Monatsmeldung wird eingereicht…\" : \"Monatsmeldung einreichen\"}\n </Button>\n )}\n </form.Subscribe>\n </form>\n </WorkspaceCard>\n )\n}\n"
138
138
  },
139
139
  {
140
140
  "path": "registry/items/affiliate/components/affiliate/affiliate-consignment-reports.tsx",
141
141
  "target": "@components/affiliate/affiliate-consignment-reports.tsx",
142
142
  "type": "registry:component",
143
- "content": "import type { StoreConsignmentReport } from \"@hartl-services/medusa-contracts/types/affiliate-store\"\nimport { getConsignmentLineDisplay } from \"@hartl-services/medusa-storefront/core\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { WorkspaceCard } from \"@/components/workspace/workspace-card\"\n\ntype ReportStatus = StoreConsignmentReport[\"status\"]\n\nconst reportStatusLabels: Record<ReportStatus, string> = {\n draft: \"Entwurf\",\n submitted: \"Eingereicht\",\n approved: \"Genehmigt\",\n rejected: \"Abgelehnt\",\n}\n\nconst reportStatusVariants: Record<ReportStatus, \"default\" | \"destructive\" | \"outline\"> = {\n draft: \"outline\",\n submitted: \"outline\",\n approved: \"default\",\n rejected: \"destructive\",\n}\n\ninterface AffiliateConsignmentReportsProps {\n reports: StoreConsignmentReport[]\n}\n\nfunction formatDate(value: string | null) {\n if (!value) {\n return \"Noch nicht eingereicht\"\n }\n\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) {\n return value\n }\n\n return new Intl.DateTimeFormat(\"de-DE\", {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).format(date)\n}\n\nexport function AffiliateConsignmentReports({ reports }: AffiliateConsignmentReportsProps) {\n if (reports.length === 0) {\n return <WorkspaceCard description=\"Für dein Kommissionslager liegen noch keine Monatsmeldungen vor.\" title=\"Bisherige Meldungen\" />\n }\n\n return (\n <section aria-labelledby=\"consignment-reports-heading\" className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2\">\n <h2 className=\"text-lg font-medium\" id=\"consignment-reports-heading\">\n Bisherige Meldungen\n </h2>\n <p className=\"text-sm text-muted-foreground\">Status und die serverseitig aufgelösten Lagerpositionen deiner Monatsmeldungen.</p>\n </div>\n <div className=\"flex flex-col gap-4\">\n {reports.map((report) => (\n <Card key={report.id}>\n <CardContent>\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex flex-col gap-2\">\n <h3 className=\"font-medium\">\n Zeitraum {report.period_start} bis {report.period_end}\n </h3>\n <p className=\"text-sm text-muted-foreground\">Eingereicht: {formatDate(report.submitted_at)}</p>\n </div>\n <Badge variant={reportStatusVariants[report.status]}>{reportStatusLabels[report.status]}</Badge>\n </div>\n <ul aria-label={`Lagerpositionen für ${report.period_start}`} className=\"flex flex-col\">\n {report.lines.map((line, index) => {\n const display = getConsignmentLineDisplay(line, \"de-DE\")\n\n return (\n <li key={line.id}>\n {index > 0 ? <Separator /> : null}\n <div className=\"flex flex-wrap items-center justify-between gap-x-6 gap-y-2 py-3 text-sm\">\n <div className=\"flex flex-col gap-1\">\n <p className=\"font-medium\">{display.product ?? `Lagerposition ${line.inventory_item_id}`}</p>\n <p className=\"text-muted-foreground\">\n {display.batch ? `Charge ${display.batch}${display.expiryDate ? ` · MHD ${display.expiryDate}` : \"\"}` : \"Ohne Charge\"} · Erwartet: {line.expected_quantity} · Gezählt: {line.counted_quantity}\n </p>\n </div>\n {line.requires_admin_clarification ? <Badge variant=\"destructive\">Klärung erforderlich</Badge> : null}\n </div>\n </li>\n )\n })}\n </ul>\n </CardContent>\n </Card>\n ))}\n </div>\n </section>\n )\n}\n"
143
+ "content": "import type { StoreConsignmentReport } from \"@hartl-services/medusa-contracts/types/affiliate-store\"\nimport { getConsignmentLineDisplay, summarizeConsignmentReportLine } from \"@hartl-services/medusa-storefront/core\"\n\nimport { Badge } from \"@/components/ui/badge\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { WorkspaceCard } from \"@/components/workspace/workspace-card\"\n\ntype ReportStatus = StoreConsignmentReport[\"status\"]\n\nconst reportStatusLabels: Record<ReportStatus, string> = {\n draft: \"Entwurf\",\n submitted: \"Eingereicht\",\n approved: \"Genehmigt\",\n rejected: \"Abgelehnt\",\n}\n\nconst reportStatusVariants: Record<ReportStatus, \"default\" | \"destructive\" | \"outline\"> = {\n draft: \"outline\",\n submitted: \"outline\",\n approved: \"default\",\n rejected: \"destructive\",\n}\n\ninterface AffiliateConsignmentReportsProps {\n reports: StoreConsignmentReport[]\n}\n\nfunction formatDate(value: string | null) {\n if (!value) {\n return \"Noch nicht eingereicht\"\n }\n\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) {\n return value\n }\n\n return new Intl.DateTimeFormat(\"de-DE\", {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).format(date)\n}\n\nexport function AffiliateConsignmentReports({ reports }: AffiliateConsignmentReportsProps) {\n if (reports.length === 0) {\n return <WorkspaceCard description=\"Für dein Kommissionslager liegen noch keine Monatsmeldungen vor.\" title=\"Bisherige Meldungen\" />\n }\n\n return (\n <section aria-labelledby=\"consignment-reports-heading\" className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-2\">\n <h2 className=\"text-lg font-medium\" id=\"consignment-reports-heading\">\n Bisherige Meldungen\n </h2>\n <p className=\"text-sm text-muted-foreground\">Status und die serverseitig aufgelösten Lagerpositionen deiner Monatsmeldungen.</p>\n </div>\n <div className=\"flex flex-col gap-4\">\n {reports.map((report) => (\n <Card key={report.id}>\n <CardContent>\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex flex-col gap-2\">\n <h3 className=\"font-medium\">\n Zeitraum {report.period_start} bis {report.period_end}\n </h3>\n <p className=\"text-sm text-muted-foreground\">Eingereicht: {formatDate(report.submitted_at)}</p>\n </div>\n <Badge variant={reportStatusVariants[report.status]}>{reportStatusLabels[report.status]}</Badge>\n </div>\n <ul aria-label={`Lagerpositionen für ${report.period_start}`} className=\"flex flex-col\">\n {report.lines.map((line, index) => {\n const display = getConsignmentLineDisplay(line, \"de-DE\")\n const splitSummary = summarizeConsignmentReportLine(report.status, line)\n\n return (\n <li key={line.id}>\n {index > 0 ? <Separator /> : null}\n <div className=\"flex flex-wrap items-center justify-between gap-x-6 gap-y-2 py-3 text-sm\">\n <div className=\"flex flex-col gap-1\">\n <p className=\"font-medium\">{display.product ?? `Lagerposition ${line.inventory_item_id}`}</p>\n <p className=\"text-muted-foreground\">{display.batch ? `Charge ${display.batch}${display.expiryDate ? `, MHD ${display.expiryDate}` : \"\"}` : \"Ohne Charge\"}</p>\n <p className=\"text-muted-foreground\">Erwartet {line.expected_quantity}, gezählt {line.counted_quantity}</p>\n {splitSummary ? <p className=\"text-muted-foreground\">{splitSummary}</p> : null}\n </div>\n {line.requires_admin_clarification ? <Badge variant=\"destructive\">Klärung erforderlich</Badge> : null}\n </div>\n </li>\n )\n })}\n </ul>\n </CardContent>\n </Card>\n ))}\n </div>\n </section>\n )\n}\n"
144
144
  },
145
145
  {
146
146
  "path": "registry/items/affiliate/components/affiliate/affiliate-coupon-code-actions.tsx",
@@ -4,7 +4,7 @@
4
4
  "title": "Cart",
5
5
  "description": "Cart widget with popover, line items, totals, cart page, add-to-cart button, variant picker and product price.",
6
6
  "dependencies": [
7
- "@hartl-services/medusa-storefront@^0.4.0",
7
+ "@hartl-services/medusa-storefront@^0.5.0",
8
8
  "@medusajs/types@^2.19.0",
9
9
  "lucide-react"
10
10
  ],
@@ -4,7 +4,7 @@
4
4
  "title": "Checkout",
5
5
  "description": "Checkout page (address, login prompt, shipping and Stripe payment, coupon, UID status) and the order confirmation page.",
6
6
  "dependencies": [
7
- "@hartl-services/medusa-storefront@^0.4.0",
7
+ "@hartl-services/medusa-storefront@^0.5.0",
8
8
  "@medusajs/types@^2.19.0",
9
9
  "@stripe/react-stripe-js@^5.6.1",
10
10
  "@stripe/stripe-js@^8.11.0",
@@ -4,7 +4,7 @@
4
4
  "title": "Storefront form kit",
5
5
  "description": "The shop's TanStack form hook (useAppForm) with unstyled field components; the package's hooks hand their formOptions to it.",
6
6
  "dependencies": [
7
- "@hartl-services/medusa-storefront@^0.4.0",
7
+ "@hartl-services/medusa-storefront@^0.5.0",
8
8
  "@tanstack/react-form@^1.33.5",
9
9
  "input-otp",
10
10
  "lucide-react"
@@ -7,8 +7,8 @@
7
7
  "title": "Storefront setup",
8
8
  "description": "Wires the storefront package into a Next shop: server and browser instances, the Server Action wrappers, texts, error reporting and the provider root for the [countryCode] layout.",
9
9
  "dependencies": [
10
- "@hartl-services/medusa-storefront@^0.4.0",
11
- "@hartl-services/medusa-contracts@^1.3.0",
10
+ "@hartl-services/medusa-storefront@^0.5.0",
11
+ "@hartl-services/medusa-contracts@^1.5.0",
12
12
  "@medusajs/framework@^2.19.0",
13
13
  "@medusajs/js-sdk@^2.19.0",
14
14
  "@medusajs/types@^2.19.0",
@@ -73,7 +73,7 @@
73
73
  "title": "Storefront form kit",
74
74
  "description": "The shop's TanStack form hook (useAppForm) with unstyled field components; the package's hooks hand their formOptions to it.",
75
75
  "dependencies": [
76
- "@hartl-services/medusa-storefront@^0.4.0",
76
+ "@hartl-services/medusa-storefront@^0.5.0",
77
77
  "@tanstack/react-form@^1.33.5",
78
78
  "input-otp",
79
79
  "lucide-react"
@@ -197,7 +197,7 @@
197
197
  "title": "Cart",
198
198
  "description": "Cart widget with popover, line items, totals, cart page, add-to-cart button, variant picker and product price.",
199
199
  "dependencies": [
200
- "@hartl-services/medusa-storefront@^0.4.0",
200
+ "@hartl-services/medusa-storefront@^0.5.0",
201
201
  "@medusajs/types@^2.19.0",
202
202
  "lucide-react"
203
203
  ],
@@ -306,8 +306,8 @@
306
306
  "title": "Customer account",
307
307
  "description": "Sign-in with password or e-mail code, password reset, account verification, address book with UID check, profile, orders, the account workspace layout and the document download route.",
308
308
  "dependencies": [
309
- "@hartl-services/medusa-storefront@^0.4.0",
310
- "@hartl-services/medusa-contracts@^1.3.0",
309
+ "@hartl-services/medusa-storefront@^0.5.0",
310
+ "@hartl-services/medusa-contracts@^1.5.0",
311
311
  "@medusajs/types@^2.19.0",
312
312
  "lucide-react"
313
313
  ],
@@ -516,7 +516,7 @@
516
516
  "title": "Checkout",
517
517
  "description": "Checkout page (address, login prompt, shipping and Stripe payment, coupon, UID status) and the order confirmation page.",
518
518
  "dependencies": [
519
- "@hartl-services/medusa-storefront@^0.4.0",
519
+ "@hartl-services/medusa-storefront@^0.5.0",
520
520
  "@medusajs/types@^2.19.0",
521
521
  "@stripe/react-stripe-js@^5.6.1",
522
522
  "@stripe/stripe-js@^8.11.0",
@@ -661,8 +661,8 @@
661
661
  "title": "Partner portal",
662
662
  "description": "Partner portal: gated registration with UID check, links, coupon codes, commissions with date filter, consignment reports, and its Route Handlers.",
663
663
  "dependencies": [
664
- "@hartl-services/medusa-storefront@^0.4.0",
665
- "@hartl-services/medusa-contracts@^1.3.0",
664
+ "@hartl-services/medusa-storefront@^0.5.0",
665
+ "@hartl-services/medusa-contracts@^1.5.0",
666
666
  "@medusajs/types@^2.19.0",
667
667
  "lucide-react",
668
668
  "react-day-picker"
@@ -4,8 +4,8 @@
4
4
  "title": "Storefront setup",
5
5
  "description": "Wires the storefront package into a Next shop: server and browser instances, the Server Action wrappers, texts, error reporting and the provider root for the [countryCode] layout.",
6
6
  "dependencies": [
7
- "@hartl-services/medusa-storefront@^0.4.0",
8
- "@hartl-services/medusa-contracts@^1.3.0",
7
+ "@hartl-services/medusa-storefront@^0.5.0",
8
+ "@hartl-services/medusa-contracts@^1.5.0",
9
9
  "@medusajs/framework@^2.19.0",
10
10
  "@medusajs/js-sdk@^2.19.0",
11
11
  "@medusajs/types@^2.19.0",
@@ -91,7 +91,7 @@ export declare function createAffiliate(context: ServerContext, pluginFeatures:
91
91
  affiliate_id: string;
92
92
  period_start: string;
93
93
  period_end: string;
94
- status: "rejected" | "draft" | "approved" | "submitted";
94
+ status: "rejected" | "approved" | "draft" | "submitted";
95
95
  draft_order_id: string | null;
96
96
  order_id: string | null;
97
97
  submitted_at: string | null;
@@ -105,6 +105,10 @@ export declare function createAffiliate(context: ServerContext, pluginFeatures:
105
105
  confirmed_sale_quantity: number;
106
106
  requires_admin_clarification: boolean;
107
107
  non_sale_reason: string | null;
108
+ non_sale_quantities: {
109
+ reason: "expired" | "damaged" | "returned" | "correction";
110
+ quantity: number;
111
+ }[];
108
112
  resolved_unit_price: number | null;
109
113
  discounted_line_total: number | null;
110
114
  currency_code: string | null;
@@ -199,7 +199,7 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
199
199
  affiliate_id: string;
200
200
  period_start: string;
201
201
  period_end: string;
202
- status: "rejected" | "draft" | "approved" | "submitted";
202
+ status: "rejected" | "approved" | "draft" | "submitted";
203
203
  draft_order_id: string | null;
204
204
  order_id: string | null;
205
205
  submitted_at: string | null;
@@ -213,6 +213,10 @@ export declare function createStorefrontServer(config: StorefrontServerConfig):
213
213
  confirmed_sale_quantity: number;
214
214
  requires_admin_clarification: boolean;
215
215
  non_sale_reason: string | null;
216
+ non_sale_quantities: {
217
+ reason: "expired" | "damaged" | "returned" | "correction";
218
+ quantity: number;
219
+ }[];
216
220
  resolved_unit_price: number | null;
217
221
  discounted_line_total: number | null;
218
222
  currency_code: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hartl-services/medusa-storefront",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Headless Medusa storefront logic, Next.js server/browser adapters and React hooks for shops built from the @hartl-services/medusa-* plugins.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -45,7 +45,7 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@hartl-services/medusa-contracts": "^1.3.0",
48
+ "@hartl-services/medusa-contracts": "^1.5.0",
49
49
  "server-only": "^0.0.1"
50
50
  },
51
51
  "peerDependencies": {