@carlonicora/nextjs-jsonapi 1.34.0 → 1.36.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 (83) hide show
  1. package/dist/{BlockNoteEditor-DFHJWZZB.mjs → BlockNoteEditor-KFUTQVUK.mjs} +3 -3
  2. package/dist/{BlockNoteEditor-TS5AN6W2.js → BlockNoteEditor-N3J42SBY.js} +13 -13
  3. package/dist/{BlockNoteEditor-TS5AN6W2.js.map → BlockNoteEditor-N3J42SBY.js.map} +1 -1
  4. package/dist/BlockNoteEditor-YOAJRPWU.css +29 -0
  5. package/dist/BlockNoteEditor-YOAJRPWU.css.map +1 -0
  6. package/dist/billing/index.css +29 -0
  7. package/dist/billing/index.css.map +1 -0
  8. package/dist/billing/index.d.mts +1 -1
  9. package/dist/billing/index.d.ts +1 -1
  10. package/dist/billing/index.js +374 -358
  11. package/dist/billing/index.js.map +1 -1
  12. package/dist/billing/index.mjs +22 -6
  13. package/dist/billing/index.mjs.map +1 -1
  14. package/dist/{chunk-GFRODCD7.mjs → chunk-DLJTN632.mjs} +1242 -981
  15. package/dist/chunk-DLJTN632.mjs.map +1 -0
  16. package/dist/{chunk-TZRAOUAR.js → chunk-NNCTRU4O.js} +29 -1
  17. package/dist/chunk-NNCTRU4O.js.map +1 -0
  18. package/dist/{chunk-UPA67DQF.js → chunk-S2RZBQP4.js} +977 -716
  19. package/dist/chunk-S2RZBQP4.js.map +1 -0
  20. package/dist/{chunk-HWQBSVBT.mjs → chunk-YZV24UWN.mjs} +29 -1
  21. package/dist/chunk-YZV24UWN.mjs.map +1 -0
  22. package/dist/client/index.css +29 -0
  23. package/dist/client/index.css.map +1 -0
  24. package/dist/client/index.js +3 -3
  25. package/dist/client/index.mjs +2 -2
  26. package/dist/components/index.css +29 -0
  27. package/dist/components/index.css.map +1 -0
  28. package/dist/components/index.d.mts +4 -1
  29. package/dist/components/index.d.ts +4 -1
  30. package/dist/components/index.js +5 -3
  31. package/dist/components/index.js.map +1 -1
  32. package/dist/components/index.mjs +4 -2
  33. package/dist/contexts/index.css +29 -0
  34. package/dist/contexts/index.css.map +1 -0
  35. package/dist/contexts/index.d.mts +9 -2
  36. package/dist/contexts/index.d.ts +9 -2
  37. package/dist/contexts/index.js +9 -3
  38. package/dist/contexts/index.js.map +1 -1
  39. package/dist/contexts/index.mjs +8 -2
  40. package/dist/core/index.d.mts +6 -4
  41. package/dist/core/index.d.ts +6 -4
  42. package/dist/core/index.js +2 -2
  43. package/dist/core/index.mjs +1 -1
  44. package/dist/index.d.mts +2 -2
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +2 -2
  47. package/dist/index.mjs +1 -1
  48. package/dist/onboarding.interface-Djyl9qYu.d.mts +71 -0
  49. package/dist/onboarding.interface-Djyl9qYu.d.ts +71 -0
  50. package/dist/{s3.service-DcqkGrKD.d.ts → s3.service-DXkDoMf1.d.ts} +3 -0
  51. package/dist/{s3.service-ag6M_7GO.d.mts → s3.service-hnTPVTm2.d.mts} +3 -0
  52. package/dist/server/index.d.mts +1 -1
  53. package/dist/server/index.d.ts +1 -1
  54. package/dist/server/index.js +3 -3
  55. package/dist/server/index.mjs +1 -1
  56. package/dist/{stripe-subscription.interface-_VWPY2AA.d.mts → stripe-subscription.interface-C8uhCYIZ.d.mts} +2 -0
  57. package/dist/{stripe-subscription.interface-Dm__xmvE.d.ts → stripe-subscription.interface-DK7BJaNd.d.ts} +2 -0
  58. package/package.json +3 -1
  59. package/src/components/index.ts +1 -0
  60. package/src/components/navigations/Header.tsx +2 -32
  61. package/src/contexts/index.ts +1 -0
  62. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +22 -6
  63. package/src/features/billing/stripe-price/data/stripe-price.interface.ts +2 -0
  64. package/src/features/billing/stripe-price/data/stripe-price.ts +9 -0
  65. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +3 -0
  66. package/src/features/company/components/forms/CompanyDeleter.tsx +157 -70
  67. package/src/features/company/contexts/CompanyContext.tsx +1 -1
  68. package/src/features/company/data/company.service.ts +23 -0
  69. package/src/features/onboarding/components/OnboardingCard.tsx +45 -0
  70. package/src/features/onboarding/components/index.ts +1 -0
  71. package/src/features/onboarding/contexts/OnboardingContext.tsx +213 -0
  72. package/src/features/onboarding/contexts/index.ts +2 -0
  73. package/src/features/onboarding/index.ts +3 -0
  74. package/src/features/onboarding/interfaces/index.ts +1 -0
  75. package/src/features/onboarding/interfaces/onboarding.interface.ts +93 -0
  76. package/src/features/onboarding/styles/onboarding.css +38 -0
  77. package/src/features/user/contexts/CurrentUserContext.tsx +35 -20
  78. package/src/shadcnui/ui/card.tsx +22 -47
  79. package/dist/chunk-GFRODCD7.mjs.map +0 -1
  80. package/dist/chunk-HWQBSVBT.mjs.map +0 -1
  81. package/dist/chunk-TZRAOUAR.js.map +0 -1
  82. package/dist/chunk-UPA67DQF.js.map +0 -1
  83. /package/dist/{BlockNoteEditor-DFHJWZZB.mjs.map → BlockNoteEditor-KFUTQVUK.mjs.map} +0 -0
@@ -38,6 +38,7 @@ type PriceFormValues = {
38
38
  usageType?: "licensed" | "metered";
39
39
  nickname?: string;
40
40
  active: boolean;
41
+ isTrial: boolean;
41
42
  description?: string;
42
43
  features: string[];
43
44
  token: string;
@@ -75,6 +76,7 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
75
76
  usageType: z.enum(["licensed", "metered"]).optional(),
76
77
  nickname: z.string().optional(),
77
78
  active: z.boolean(),
79
+ isTrial: z.boolean(),
78
80
  description: z.string().optional(),
79
81
  features: z.array(z.string()),
80
82
  token: z.string(),
@@ -102,6 +104,7 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
102
104
  usageType: price?.recurring?.usageType || "licensed",
103
105
  nickname: price?.nickname || "",
104
106
  active: price?.active ?? true,
107
+ isTrial: price?.isTrial ?? false,
105
108
  description: price?.description || "",
106
109
  features: price?.features || [],
107
110
  token: price?.token?.toString() ?? "",
@@ -126,6 +129,7 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
126
129
  usageType: price?.recurring?.usageType || "licensed",
127
130
  nickname: price?.nickname || "",
128
131
  active: price?.active ?? true,
132
+ isTrial: price?.isTrial ?? false,
129
133
  description: price?.description || "",
130
134
  features: price?.features || [],
131
135
  token: price?.token?.toString() ?? "",
@@ -145,15 +149,15 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
145
149
  const unitAmountInCents = Math.round(values.unitAmount * 100);
146
150
 
147
151
  if (isEditMode) {
148
- // Update existing price (nickname, description, features, token can be updated - Stripe fields are limited)
152
+ // Update existing price (nickname, description, features, token, isTrial can be updated - Stripe fields are limited)
149
153
  await StripePriceService.updatePrice({
150
154
  id: price.id,
151
155
  nickname: values.nickname || undefined,
152
156
  description: values.description || undefined,
153
157
  features: values.features.filter((f) => f.trim()) || undefined,
154
158
  token: values.token ? parseInt(values.token, 10) : undefined,
155
- // Only include featureIds for recurring prices (one-time prices don't support platform features)
156
- ...(price?.priceType === "recurring" ? { featureIds: values.featureIds } : {}),
159
+ // Only include isTrial and featureIds for recurring prices
160
+ ...(price?.priceType === "recurring" ? { isTrial: values.isTrial, featureIds: values.featureIds } : {}),
157
161
  });
158
162
  } else {
159
163
  // Create new price
@@ -190,9 +194,12 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
190
194
  createInput.token = parseInt(values.token, 10);
191
195
  }
192
196
 
193
- // Add platform feature IDs only for recurring prices (Neo4j only, not sent to Stripe)
194
- if (isRecurring && values.featureIds.length > 0) {
195
- createInput.featureIds = values.featureIds;
197
+ // Add isTrial and platform feature IDs only for recurring prices (Neo4j only, not sent to Stripe)
198
+ if (isRecurring) {
199
+ createInput.isTrial = values.isTrial;
200
+ if (values.featureIds.length > 0) {
201
+ createInput.featureIds = values.featureIds;
202
+ }
196
203
  }
197
204
 
198
205
  await StripePriceService.createPrice(createInput);
@@ -403,6 +410,15 @@ export function PriceEditor({ productId, price, open, onOpenChange, onSuccess }:
403
410
 
404
411
  <FormCheckbox form={form} id="active" name="Active" />
405
412
 
413
+ {isRecurring && (
414
+ <FormCheckbox
415
+ form={form}
416
+ id="isTrial"
417
+ name="Trial Price"
418
+ description="Mark this as the trial subscription plan (only one price should be marked as trial)"
419
+ />
420
+ )}
421
+
406
422
  <CommonEditorButtons isEdit={isEditMode} form={form} disabled={isSubmitting} setOpen={onOpenChange} />
407
423
  </form>
408
424
  </Form>
@@ -21,6 +21,7 @@ export interface StripePriceInterface extends ApiDataInterface {
21
21
  get description(): string | undefined;
22
22
  get features(): string[] | undefined;
23
23
  get token(): number | undefined;
24
+ get isTrial(): boolean | undefined;
24
25
  get priceFeatures(): FeatureInterface[]; // Platform Feature entities linked to this price
25
26
  }
26
27
 
@@ -49,5 +50,6 @@ export type StripePriceInput = {
49
50
  description?: string;
50
51
  features?: string[];
51
52
  token?: number;
53
+ isTrial?: boolean;
52
54
  featureIds?: string[]; // Feature entity IDs to link (Neo4j only, NOT sent to Stripe)
53
55
  };
@@ -18,6 +18,7 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
18
18
  private _description?: string;
19
19
  private _features?: string[];
20
20
  private _token?: number;
21
+ private _isTrial?: boolean;
21
22
  private _priceFeatures: FeatureInterface[] = []; // Platform Feature entities
22
23
 
23
24
  get stripePriceId(): string {
@@ -80,6 +81,10 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
80
81
  return this._token;
81
82
  }
82
83
 
84
+ get isTrial(): boolean | undefined {
85
+ return this._isTrial;
86
+ }
87
+
83
88
  get priceFeatures(): FeatureInterface[] {
84
89
  return this._priceFeatures;
85
90
  }
@@ -120,6 +125,7 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
120
125
  : undefined;
121
126
 
122
127
  this._token = data.jsonApi.attributes.token;
128
+ this._isTrial = data.jsonApi.attributes.isTrial;
123
129
 
124
130
  // Hydrate product relationship
125
131
  this._product = this._readIncluded(data, "product", Modules.StripeProduct) as StripeProductInterface;
@@ -169,6 +175,9 @@ export class StripePrice extends AbstractApiData implements StripePriceInterface
169
175
  if ("token" in data && data.token !== undefined) {
170
176
  response.data.attributes.token = data.token;
171
177
  }
178
+ if ("isTrial" in data && data.isTrial !== undefined) {
179
+ response.data.attributes.isTrial = data.isTrial;
180
+ }
172
181
 
173
182
  // Convert featureIds to JSON:API relationships format
174
183
  if (data.featureIds && data.featureIds.length > 0) {
@@ -84,6 +84,9 @@ export function ProductPricingList({
84
84
  pricesToFilter = pricesToFilter.filter((price) => price.priceType !== "one_time");
85
85
  }
86
86
 
87
+ // Filter out trial prices - users shouldn't manually select trial plans
88
+ pricesToFilter = pricesToFilter.filter((price) => !price.isTrial);
89
+
87
90
  const filteredPrices = getFilteredPrices(pricesToFilter, selectedInterval);
88
91
 
89
92
  if (filteredPrices.length === 0) {
@@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
5
5
  import { useState } from "react";
6
6
  import { errorToast } from "../../../../components";
7
7
  import { Modules } from "../../../../core";
8
- import { useI18nRouter } from "../../../../i18n";
8
+ import { usePageUrlGenerator } from "../../../../hooks";
9
9
  import { Action } from "../../../../permissions";
10
10
  import { getRoleId } from "../../../../roles";
11
11
  import {
@@ -19,6 +19,7 @@ import {
19
19
  Input,
20
20
  Label,
21
21
  } from "../../../../shadcnui";
22
+ import { AuthService } from "../../../auth/data";
22
23
  import { UserInterface } from "../../../user";
23
24
  import { useCurrentUserContext } from "../../../user/contexts";
24
25
  import { CompanyInterface } from "../../data";
@@ -28,97 +29,183 @@ type CompanyDeleterProps = {
28
29
  company: CompanyInterface;
29
30
  };
30
31
 
31
- function CompanyDeleterInternal({ company }: CompanyDeleterProps) {
32
+ type CompanyDeleterInternalProps = CompanyDeleterProps & {
33
+ isAdministrator: boolean;
34
+ };
35
+
36
+ function CompanyDeleterInternal({ company, isAdministrator }: CompanyDeleterInternalProps) {
32
37
  const t = useTranslations();
33
- const router = useI18nRouter();
38
+ const generateUrl = usePageUrlGenerator();
34
39
  const [open, setOpen] = useState<boolean>(false);
40
+ const [finalWarningOpen, setFinalWarningOpen] = useState<boolean>(false);
35
41
  const [isDeleting, setIsDeleting] = useState<boolean>(false);
36
42
  const [companyName, setCompanyName] = useState<string>("");
37
43
 
38
- const handleDelete = async () => {
44
+ const handleProceedToFinalWarning = () => {
45
+ console.log("[CompanyDeleter] Proceeding to final warning dialog");
46
+ console.log("[CompanyDeleter] Company name entered:", companyName);
47
+ console.log("[CompanyDeleter] Expected company name:", company.name);
48
+ setOpen(false);
49
+ setFinalWarningOpen(true);
50
+ };
51
+
52
+ const handleFinalDelete = async () => {
53
+ console.log("[CompanyDeleter] handleFinalDelete called");
54
+ console.log("[CompanyDeleter] isAdministrator:", isAdministrator);
55
+ console.log("[CompanyDeleter] company.id:", company.id);
56
+ console.log("[CompanyDeleter] company.name:", company.name);
57
+
39
58
  setIsDeleting(true);
40
59
  try {
41
- await CompanyService.delete({ companyId: company.id });
42
- router.push("/");
60
+ if (isAdministrator) {
61
+ console.log("[CompanyDeleter] Using Administrator delete endpoint");
62
+ await CompanyService.delete({ companyId: company.id });
63
+ } else {
64
+ console.log("[CompanyDeleter] Using CompanyAdministrator self-delete endpoint");
65
+ await CompanyService.selfDelete({ companyId: company.id });
66
+ }
67
+ console.log("[CompanyDeleter] Delete successful, logging out user");
68
+ await AuthService.logout();
69
+ window.location.href = generateUrl({ page: `/` });
43
70
  } catch (error) {
71
+ console.error("[CompanyDeleter] Delete failed with error:", error);
72
+ console.error("[CompanyDeleter] Error details:", JSON.stringify(error, null, 2));
44
73
  errorToast({ title: t(`common.errors.delete`), error: error });
74
+ setIsDeleting(false);
45
75
  }
46
- setIsDeleting(false);
76
+ };
77
+
78
+ const handleGoBack = () => {
79
+ setFinalWarningOpen(false);
80
+ setCompanyName("");
47
81
  };
48
82
 
49
83
  return (
50
- <AlertDialog open={open} onOpenChange={setOpen}>
51
- <AlertDialogTrigger>
52
- <Button size="sm" variant={"destructive"}>
53
- <Trash2Icon className="mr-3 h-3.5 w-3.5" />
54
- {t(`ui.buttons.delete`)}
55
- </Button>
56
- </AlertDialogTrigger>
57
- <AlertDialogContent className={`flex max-h-[70vh] max-w-3xl flex-col overflow-y-auto`}>
58
- <AlertDialogHeader>
59
- <AlertDialogTitle>
60
- {t(`common.delete.title`, { type: t(`entities.companies`, { count: 1 }) })}
61
- </AlertDialogTitle>
62
- <AlertDialogDescription>
63
- {t(`common.delete.subtitle`, { type: t(`entities.companies`, { count: 1 }) })}
64
- </AlertDialogDescription>
65
- </AlertDialogHeader>
66
- <div className="text-destructive p-4 text-sm">
67
- {t(`common.delete.description`, { type: t(`entities.companies`, { count: 1 }) })}
68
- </div>
69
- <div className="flex w-full flex-col gap-y-2">
70
- <div>{t(`common.delete.confirmation`, { type: t(`entities.companies`, { count: 1 }) })}</div>
71
- <div className="flex w-full flex-col">
72
- <Label className="flex items-center">
73
- {t(`company.fields.name.label`)}
74
- <span className="text-destructive ml-2 font-semibold">*</span>
75
- </Label>
76
- <Input
77
- className={`w-full`}
78
- placeholder={t(`company.fields.name.placeholder`)}
79
- onChange={(e) => setCompanyName(e.target.value)}
80
- />
81
- </div>
82
- </div>
83
- <div className="flex justify-end">
84
+ <>
85
+ {/* Dialog 1: Company Name Confirmation */}
86
+ <AlertDialog open={open} onOpenChange={setOpen}>
87
+ <AlertDialogTrigger>
84
88
  <Button
85
- className="mr-2"
86
- variant={"outline"}
87
- type={`button`}
88
- onClick={() => setOpen(false)}
89
- disabled={isDeleting}
89
+ render={<div />}
90
+ nativeButton={false}
91
+ size="sm"
92
+ variant="ghost"
93
+ className="text-muted-foreground hover:text-destructive"
90
94
  >
91
- {t(`ui.buttons.cancel`)}
95
+ <Trash2Icon />
92
96
  </Button>
93
- <Button
94
- type="submit"
95
- onClick={(e) => {
96
- e.preventDefault();
97
- handleDelete();
98
- }}
99
- variant={"destructive"}
100
- disabled={company.name !== companyName || isDeleting}
101
- >
102
- {isDeleting ? (
103
- <>
104
- {t(`ui.buttons.deleting`)}
105
- <LoaderCircleIcon className="animate-spin-slow h-5 w-5" />
106
- </>
107
- ) : (
108
- t(`ui.buttons.delete`)
109
- )}
110
- </Button>
111
- </div>
112
- </AlertDialogContent>
113
- </AlertDialog>
97
+ </AlertDialogTrigger>
98
+ <AlertDialogContent className={`flex max-h-[70vh] max-w-3xl flex-col overflow-y-auto`}>
99
+ <AlertDialogHeader>
100
+ <AlertDialogTitle>
101
+ {t(`common.delete.title`, { type: t(`entities.companies`, { count: 1 }) })}
102
+ </AlertDialogTitle>
103
+ <AlertDialogDescription>
104
+ {t(`common.delete.subtitle`, { type: t(`entities.companies`, { count: 1 }) })}
105
+ </AlertDialogDescription>
106
+ </AlertDialogHeader>
107
+ <div className="text-destructive p-4 text-sm">
108
+ {t(`common.delete.description`, { type: t(`entities.companies`, { count: 1 }) })}
109
+ </div>
110
+ <div className="flex w-full flex-col gap-y-2">
111
+ <div>{t(`common.delete.confirmation`, { type: t(`entities.companies`, { count: 1 }) })}</div>
112
+ <div className="flex w-full flex-col">
113
+ <Label className="flex items-center">
114
+ {t(`company.fields.name.label`)}
115
+ <span className="text-destructive ml-2 font-semibold">*</span>
116
+ </Label>
117
+ <Input
118
+ className={`w-full`}
119
+ placeholder={t(`company.fields.name.placeholder`)}
120
+ onChange={(e) => setCompanyName(e.target.value)}
121
+ />
122
+ </div>
123
+ </div>
124
+ <div className="flex justify-end">
125
+ <Button className="mr-2" variant={"outline"} type={`button`} onClick={() => setOpen(false)}>
126
+ {t(`ui.buttons.cancel`)}
127
+ </Button>
128
+ <Button
129
+ type="submit"
130
+ onClick={(e) => {
131
+ e.preventDefault();
132
+ handleProceedToFinalWarning();
133
+ }}
134
+ variant={"destructive"}
135
+ disabled={company.name !== companyName}
136
+ >
137
+ {t(`ui.buttons.delete`)}
138
+ </Button>
139
+ </div>
140
+ </AlertDialogContent>
141
+ </AlertDialog>
142
+
143
+ {/* Dialog 2: Final Warning */}
144
+ <AlertDialog open={finalWarningOpen} onOpenChange={setFinalWarningOpen}>
145
+ <AlertDialogContent className={`flex max-h-[70vh] max-w-3xl flex-col overflow-y-auto`}>
146
+ <AlertDialogHeader>
147
+ <AlertDialogTitle>{t(`common.delete.finalWarning.title`)}</AlertDialogTitle>
148
+ <AlertDialogDescription>{t(`common.delete.finalWarning.subtitle`)}</AlertDialogDescription>
149
+ </AlertDialogHeader>
150
+ <div className="bg-destructive/10 border-destructive text-destructive rounded-md border p-4 text-sm">
151
+ <p className="mb-3 font-semibold">{t(`common.delete.finalWarning.description`)}</p>
152
+ <ul className="list-disc space-y-1 pl-5">
153
+ <li>{t(`common.delete.finalWarning.bullet1`)}</li>
154
+ <li>{t(`common.delete.finalWarning.bullet2`)}</li>
155
+ <li>{t(`common.delete.finalWarning.bullet3`)}</li>
156
+ <li>{t(`common.delete.finalWarning.bullet4`)}</li>
157
+ <li>{t(`common.delete.finalWarning.bullet5`)}</li>
158
+ </ul>
159
+ </div>
160
+ <div className="flex justify-end">
161
+ <Button className="mr-2" variant={"outline"} type={`button`} onClick={handleGoBack} disabled={isDeleting}>
162
+ {t(`ui.buttons.go_back`)}
163
+ </Button>
164
+ <Button
165
+ type="submit"
166
+ onClick={(e) => {
167
+ e.preventDefault();
168
+ handleFinalDelete();
169
+ }}
170
+ variant={"destructive"}
171
+ disabled={isDeleting}
172
+ >
173
+ {isDeleting ? (
174
+ <>
175
+ {t(`ui.buttons.deleting`)}
176
+ <LoaderCircleIcon className="animate-spin-slow h-5 w-5" />
177
+ </>
178
+ ) : (
179
+ t(`ui.buttons.delete_forever`)
180
+ )}
181
+ </Button>
182
+ </div>
183
+ </AlertDialogContent>
184
+ </AlertDialog>
185
+ </>
114
186
  );
115
187
  }
116
188
 
117
189
  export function CompanyDeleter({ company }: CompanyDeleterProps) {
118
190
  const { hasPermissionToModule, hasRole } = useCurrentUserContext<UserInterface>();
119
191
 
120
- if (!hasRole(getRoleId().Administrator) && !hasPermissionToModule({ module: Modules.Company, action: Action.Delete }))
192
+ const isAdministrator = hasRole(getRoleId().Administrator);
193
+ const isCompanyAdministrator = hasRole(getRoleId().CompanyAdministrator);
194
+ const hasDeletePermission = hasPermissionToModule({ module: Modules.Company, action: Action.Delete });
195
+
196
+ console.log("[CompanyDeleter] Role check:", {
197
+ isAdministrator,
198
+ isCompanyAdministrator,
199
+ hasDeletePermission,
200
+ companyId: company.id,
201
+ companyName: company.name,
202
+ });
203
+
204
+ if (!isAdministrator && !isCompanyAdministrator && !hasDeletePermission) {
205
+ console.log("[CompanyDeleter] Access denied - no delete permissions");
121
206
  return null;
207
+ }
122
208
 
123
- return <CompanyDeleterInternal company={company} />;
209
+ console.log("[CompanyDeleter] Access granted, rendering with isAdministrator:", isAdministrator);
210
+ return <CompanyDeleterInternal company={company} isAdministrator={isAdministrator} />;
124
211
  }
@@ -62,7 +62,7 @@ export const CompanyProvider = ({ children, dehydratedCompany }: CompanyProvider
62
62
 
63
63
  if (
64
64
  company &&
65
- hasRole(getRoleId().Administrator) &&
65
+ (hasRole(getRoleId().Administrator) || hasRole(getRoleId().CompanyAdministrator)) &&
66
66
  hasPermissionToModule({ module: Modules.Company, action: Action.Delete })
67
67
  )
68
68
  functions.push(<CompanyDeleter key="companyDeleter" company={company} />);
@@ -31,6 +31,29 @@ export class CompanyService extends AbstractService {
31
31
  });
32
32
  }
33
33
 
34
+ static async selfDelete(params: { companyId: string }): Promise<void> {
35
+ const endpoint = new EndpointCreator({
36
+ endpoint: Modules.Company,
37
+ id: params.companyId,
38
+ childEndpoint: "self-delete",
39
+ }).generate();
40
+
41
+ console.log("[CompanyService.selfDelete] Called with companyId:", params.companyId);
42
+ console.log("[CompanyService.selfDelete] Generated endpoint:", endpoint);
43
+
44
+ try {
45
+ await this.callApi({
46
+ type: Modules.Company,
47
+ method: HttpMethod.DELETE,
48
+ endpoint: endpoint,
49
+ });
50
+ console.log("[CompanyService.selfDelete] API call successful");
51
+ } catch (error) {
52
+ console.error("[CompanyService.selfDelete] API call failed:", error);
53
+ throw error;
54
+ }
55
+ }
56
+
34
57
  static async create(params: CompanyInput): Promise<CompanyInterface> {
35
58
  return this.callApi({
36
59
  type: Modules.Company,
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import { X } from "lucide-react";
4
+ import { Button, Card, CardContent, CardHeader, CardTitle } from "../../../shadcnui";
5
+ import { OnboardingCardRenderProps } from "../interfaces";
6
+
7
+ // Matches a360ai ShepherdCard design exactly
8
+ export function OnboardingCard({
9
+ step,
10
+ currentIndex,
11
+ totalSteps,
12
+ labels,
13
+ onNext,
14
+ onPrevious,
15
+ onClose,
16
+ isFirst,
17
+ isLast,
18
+ }: OnboardingCardRenderProps) {
19
+ return (
20
+ <Card className="w-[320px] relative" data-shepherd-card>
21
+ <Button
22
+ variant="ghost"
23
+ size="icon"
24
+ className="absolute right-2 top-2"
25
+ onClick={onClose}
26
+ aria-label={labels.close}
27
+ >
28
+ <X className="h-4 w-4" />
29
+ </Button>
30
+ <CardHeader>
31
+ <CardTitle className="flex items-center gap-2">{step.title}</CardTitle>
32
+ <p className="text-xs text-muted-foreground">{labels.stepCounter(currentIndex + 1, totalSteps)}</p>
33
+ </CardHeader>
34
+ <CardContent className="space-y-4">
35
+ <div>{step.content}</div>
36
+ <div className="flex justify-between">
37
+ <Button variant="outline" onClick={onPrevious} disabled={isFirst}>
38
+ {labels.previous}
39
+ </Button>
40
+ <Button onClick={isLast ? onClose : onNext}>{isLast ? labels.finish : labels.next}</Button>
41
+ </div>
42
+ </CardContent>
43
+ </Card>
44
+ );
45
+ }
@@ -0,0 +1 @@
1
+ export * from "./OnboardingCard";