@goplusvn/core 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.8 — branding wiring + auth/rbac unification + clean typecheck
4
+
5
+ - **Single source for permission checks (security fix).** `@goerp/core/rbac`
6
+ used to ship its own copy of `checkPermission` / `getUserPermissions` /
7
+ `getCrudPermissionsFromSession` / … which had drifted from `@goerp/core/auth`
8
+ — it still allowed `BYPASS_AUTH` in production and denied admins with no
9
+ explicit permission rows. `rbac` now re-exports these from the hardened `auth`
10
+ module (bypass dev-only, admin-before-empty-guard), so the two can't diverge.
11
+
12
+
13
+ - **Branding via `TenantProvider`.** `Logo`/`LogoCompact` now resolve their image,
14
+ company name and tagline from `props → TenantProvider branding → defaults`, so a
15
+ new app re-skins by wrapping with `<TenantProvider tenant={{ branding }}>` (or
16
+ passing `logoSrc`/`companyName`/`tagline` props) instead of hardcoding. Added a
17
+ non-throwing `useOptionalTenant()` hook and a `tagline` field to `TenantBranding`.
18
+ `TenantProvider` also applies `branding.primaryColor` (an HSL triple) to
19
+ `--primary`/`--accent-primary` at runtime, so the brand color is config-driven
20
+ too. Fully backward compatible (no provider/props → previous GoERP defaults).
21
+ - **Core now type-checks clean (0 errors).** Fixed the `system/categories`
22
+ components that imported the consumer alias `@goerp/core/ui` from inside the
23
+ package (unresolvable in core's own tsconfig → cascaded into implicit-any in
24
+ their event handlers); switched to relative imports.
25
+ - **Standard entity configs.** `@goerp/core/configs/entities` now ships canonical
26
+ `EntityConfig`s (starting with `departmentsConfig`, plus the previously
27
+ unexported `materialCategoriesConfig`) so apps import + spread-to-override
28
+ instead of copy-pasting near-identical configs.
29
+
3
30
  ## 0.1.7 — auth hardening
4
31
 
5
32
  Hardened `@goerp/core/auth` so the Vinh Hoa app (and others) can consolidate
package/PLATFORM.md CHANGED
@@ -31,6 +31,53 @@ Use semantic tokens, never the raw Tailwind palette: `text-muted-foreground` not
31
31
  `text-slate-500`, `bg-card` not `bg-white`, `text-destructive` not `text-red-600`.
32
32
  Editing `base.css` reskins every app at once.
33
33
 
34
+ ## Branding (logo / company name)
35
+
36
+ Wrap the app shell with `TenantProvider` — `Logo`/`LogoCompact` (used by the
37
+ sidebar/header) then render the app's logo, name and tagline from config instead
38
+ of the GoERP defaults. No provider → defaults (backward compatible).
39
+
40
+ ```tsx
41
+ import { TenantProvider } from "@goerp/core/providers"
42
+
43
+ <TenantProvider
44
+ tenant={{
45
+ id: "tanloc",
46
+ name: "Tấn Lộc",
47
+ branding: {
48
+ logo: "/logo.png",
49
+ companyName: "Tấn Lộc",
50
+ tagline: "Suất ăn CN",
51
+ primaryColor: "262 83% 58%", // HSL triple → re-skins --primary at runtime
52
+ favicon: "/favicon.ico",
53
+ },
54
+ currency: "VND",
55
+ }}
56
+ >
57
+ <MainLayout>{children}</MainLayout>
58
+ </TenantProvider>
59
+ ```
60
+
61
+ `primaryColor` (an HSL triple, no `hsl()` wrapper) reskins the whole app's brand
62
+ token at runtime — no globals edit needed. `favicon` is data the app reads in its
63
+ metadata. Read branding anywhere via `useOptionalTenant()` (non-throwing); for a
64
+ one-off, `Logo` also accepts `logoSrc` / `companyName` / `tagline` props.
65
+
66
+ ## Standard entity configs
67
+
68
+ Core ships canonical configs for org entities so apps don't copy-paste them.
69
+ Import and override by spreading:
70
+
71
+ ```ts
72
+ import { departmentsConfig } from "@goerp/core/configs/entities"
73
+
74
+ // use as-is, or tweak:
75
+ export const departments = { ...departmentsConfig, apiEndpoint: "/api/org/departments" }
76
+ ```
77
+
78
+ These drive the core CRUD engine (`@goerp/core/crud`) — declare an `EntityConfig`
79
+ (fields/filters/permissions/features) and you get table/form/import/export.
80
+
34
81
  ## Errors
35
82
 
36
83
  Bind the db-injected factories once, then use `serverError` in every catch block.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.7",
4
+ "version": "0.1.9",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -49,6 +49,7 @@
49
49
  "./utils/date-utils": "./src/utils/date-utils.ts",
50
50
  "./utils/cccd-parser": "./src/utils/cccd-parser.ts",
51
51
  "./configs/status": "./src/configs/status.ts",
52
+ "./configs/entities": "./src/configs/entities/index.ts",
52
53
  "./package.json": "./package.json"
53
54
  },
54
55
  "peerDependencies": {
@@ -0,0 +1,140 @@
1
+ import { Building2 } from "lucide-react";
2
+
3
+ import type { EntityConfig } from "../../types";
4
+
5
+ import { STATUS_OPTIONS } from "../status";
6
+
7
+ /**
8
+ * Canonical "Phòng ban" (department) entity config — the org-structure entity
9
+ * every business app needs. Apps import this instead of copy-pasting it, and
10
+ * override fields by spreading: `{ ...departmentsConfig, apiEndpoint: "..." }`.
11
+ */
12
+ export const departmentsConfig: EntityConfig = {
13
+ name: "department",
14
+ permissionResource: "department",
15
+ label: "Phòng ban",
16
+ pluralLabel: "Phòng ban",
17
+ icon: Building2,
18
+ description: "Quản lý cơ cấu tổ chức phòng ban",
19
+ apiEndpoint: "/api/departments",
20
+ idField: "id",
21
+ displayField: "name",
22
+
23
+ fields: [
24
+ {
25
+ name: "id",
26
+ label: "ID",
27
+ type: "text",
28
+ hideInForm: true,
29
+ hideInTable: true,
30
+ },
31
+ {
32
+ name: "code",
33
+ label: "Mã phòng ban",
34
+ type: "text",
35
+ required: true,
36
+ placeholder: "VD: IT, HR, SALES",
37
+ filterable: true,
38
+ width: 120,
39
+ },
40
+ {
41
+ name: "name",
42
+ label: "Tên phòng ban",
43
+ type: "text",
44
+ required: true,
45
+ placeholder: "Nhập tên phòng ban",
46
+ filterable: true,
47
+ },
48
+ {
49
+ name: "description",
50
+ label: "Mô tả",
51
+ type: "textarea",
52
+ placeholder: "Nhập mô tả phòng ban",
53
+ hideInTable: true,
54
+ },
55
+ {
56
+ name: "parentId",
57
+ label: "Phòng ban cha",
58
+ type: "select",
59
+ placeholder: "Chọn phòng ban cha (nếu có)",
60
+ dataSource: {
61
+ type: "api",
62
+ endpoint: "/api/departments",
63
+ labelField: "name",
64
+ valueField: "id",
65
+ searchable: true,
66
+ },
67
+ hideInTable: true,
68
+ },
69
+ {
70
+ name: "parentName",
71
+ label: "Phòng ban cha",
72
+ type: "text",
73
+ hideInForm: true,
74
+ isDisplayOnly: true,
75
+ },
76
+ {
77
+ name: "order",
78
+ label: "Thứ tự",
79
+ type: "number",
80
+ defaultValue: 0,
81
+ placeholder: "Thứ tự hiển thị",
82
+ width: 80,
83
+ },
84
+ {
85
+ name: "status",
86
+ label: "Trạng thái",
87
+ type: "switch",
88
+ required: true,
89
+ defaultValue: "active",
90
+ filterable: true,
91
+ options: STATUS_OPTIONS,
92
+ },
93
+ {
94
+ name: "createdAt",
95
+ label: "Ngày tạo",
96
+ type: "datetime",
97
+ hideInForm: true,
98
+ width: 120,
99
+ renderCell: (value) =>
100
+ new Date(value as string).toLocaleDateString("vi-VN"),
101
+ },
102
+ ],
103
+
104
+ filters: [
105
+ {
106
+ name: "search",
107
+ label: "Tìm kiếm",
108
+ type: "text",
109
+ field: "name",
110
+ operator: "contains",
111
+ },
112
+ {
113
+ name: "status",
114
+ label: "Trạng thái",
115
+ type: "select",
116
+ field: "status",
117
+ operator: "eq",
118
+ options: [
119
+ { label: "Hoạt động", value: "active" },
120
+ { label: "Vô hiệu hóa", value: "inactive" },
121
+ ],
122
+ },
123
+ ],
124
+
125
+ permissions: {
126
+ create: true,
127
+ read: true,
128
+ update: true,
129
+ delete: true,
130
+ export: true,
131
+ import: true,
132
+ },
133
+
134
+ features: {
135
+ search: true,
136
+ bulkActions: true,
137
+ export: true,
138
+ import: true,
139
+ },
140
+ };
@@ -0,0 +1,4 @@
1
+ // Canonical entity configs shipped by core. Apps import these instead of
2
+ // copy-pasting, and override by spreading: `{ ...departmentsConfig, ... }`.
3
+ export { departmentsConfig } from "./departments.config";
4
+ export { materialCategoriesConfig } from "./material-categories.config";
@@ -23,6 +23,40 @@ import {
23
23
  } from "../../ui/primitives/client";
24
24
  import { DataTable } from "../../ui/data-display/data-table/data-table";
25
25
 
26
+ /**
27
+ * Resolve a relation FK column's label from the relation object the server already
28
+ * included on the row (e.g. `branchId` → `row.branch.name`).
29
+ *
30
+ * Relation columns are configured as a `select` field whose `dataSource` points at
31
+ * `/api/<relation>`; the client resolves the id → label by fetching that endpoint
32
+ * asynchronously. That secondary fetch is the only label source, so the column shows
33
+ * the raw FK id whenever it hasn't resolved yet (first paint), fails, is slow, or is
34
+ * capped by pagination (the endpoint's default page size). A per-config `renderCell`
35
+ * that reads `row.branch?.name` can't help — render functions are stripped when the
36
+ * config is serialized for Client Components.
37
+ *
38
+ * When the entity `include`s the relation, the row already carries the related record,
39
+ * so we can render its label directly with no secondary fetch and no race. The relation
40
+ * key is inferred from the field name (`branchId` → `branch`) and the label field from
41
+ * the `dataSource` (default `name`).
42
+ */
43
+ function resolveIncludedRelationLabel(
44
+ field: { name?: string; dataSource?: { labelField?: string } },
45
+ row: Record<string, unknown> | undefined,
46
+ ): string | undefined {
47
+ const name = field?.name;
48
+ if (!field?.dataSource || !row || !name || !name.endsWith("Id")) {
49
+ return undefined;
50
+ }
51
+ const relationKey = name.slice(0, -2);
52
+ if (!relationKey) return undefined;
53
+ const rel = row[relationKey];
54
+ if (!rel || typeof rel !== "object" || Array.isArray(rel)) return undefined;
55
+ const labelField = field.dataSource.labelField || "name";
56
+ const label = (rel as Record<string, unknown>)[labelField];
57
+ return label != null && label !== "" ? String(label) : undefined;
58
+ }
59
+
26
60
  interface CrudTableProps<TData = Record<string, unknown>> {
27
61
  data: CrudResponse<TData>;
28
62
  loading?: boolean;
@@ -196,7 +230,8 @@ export function CrudTable<TData extends Record<string, unknown>>({
196
230
  );
197
231
  content = option
198
232
  ? option.label
199
- : formatFieldValue(value, field);
233
+ : (resolveIncludedRelationLabel(field, row.original) ??
234
+ formatFieldValue(value, field));
200
235
  }
201
236
  } else {
202
237
  content = formatFieldValue(value, field);
@@ -4,7 +4,14 @@ import React, { createContext, useContext } from "react";
4
4
  export interface TenantBranding {
5
5
  logo?: string;
6
6
  companyName: string;
7
+ /** Subtitle shown under the company name in the logo (e.g. "ERP System"). */
8
+ tagline?: string;
9
+ /**
10
+ * Brand color as an HSL triple, e.g. "262 83% 58%" (no `hsl(...)` wrapper).
11
+ * Applied at runtime to `--primary` / `--accent-primary` by TenantProvider.
12
+ */
7
13
  primaryColor?: string;
14
+ /** Favicon URL — apps read `tenant.branding.favicon` in their metadata. */
8
15
  favicon?: string;
9
16
  }
10
17
 
@@ -29,8 +36,18 @@ interface TenantProviderProps {
29
36
  }
30
37
 
31
38
  export function TenantProvider({ children, tenant }: TenantProviderProps) {
39
+ const { primaryColor } = tenant.branding;
32
40
  return (
33
41
  <TenantContext.Provider value={{ tenant }}>
42
+ {primaryColor && (
43
+ // Re-skin the brand token at runtime so the whole app follows the
44
+ // tenant color. Rendered after the imported base.css → wins the cascade.
45
+ <style
46
+ dangerouslySetInnerHTML={{
47
+ __html: `:root{--primary:${primaryColor};--accent-primary:${primaryColor};}`,
48
+ }}
49
+ />
50
+ )}
34
51
  {children}
35
52
  </TenantContext.Provider>
36
53
  );
@@ -43,3 +60,12 @@ export function useTenantContext() {
43
60
  }
44
61
  return context;
45
62
  }
63
+
64
+ /**
65
+ * Non-throwing variant: returns the tenant config if inside a TenantProvider,
66
+ * otherwise `undefined`. Lets shared components (e.g. Logo) read branding
67
+ * without forcing every app to mount a provider.
68
+ */
69
+ export function useOptionalTenant(): TenantConfig | undefined {
70
+ return useContext(TenantContext)?.tenant;
71
+ }
package/src/rbac/index.ts CHANGED
@@ -116,179 +116,21 @@ interface ExtendedUser {
116
116
  permissions?: Permission[];
117
117
  }
118
118
 
119
- export function getUserPermissions(session: Session | null): Permission[] {
120
- if (!session?.user) return [];
121
- const user = session.user as ExtendedUser;
119
+ // Permission checks now live in a single hardened source: @goerp/core/auth
120
+ // (bypass is dev-only; the admin role is honored BEFORE the empty-permissions
121
+ // guard). Re-exported here so existing @goerp/core/rbac consumers are unchanged
122
+ // and can never diverge from auth again.
123
+ export {
124
+ getUserPermissions,
125
+ getCrudPermissionsFromSession,
126
+ checkPermission,
127
+ hasPermission,
128
+ requirePermission,
129
+ hasRole,
130
+ hasAnyRole,
131
+ isAdmin,
132
+ } from "../auth";
122
133
 
123
- if (!user.permissions) {
124
- return [];
125
- }
126
- return user.permissions;
127
- }
128
-
129
- const BYPASS_AUTH =
130
- process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1";
131
-
132
- const ADMIN_ROLE_CODES = ["admin", "SUPER_ADMIN"];
133
-
134
- export function getCrudPermissionsFromSession(
135
- session: Session | null,
136
- entity: string,
137
- ): {
138
- create: boolean;
139
- view: boolean;
140
- update: boolean;
141
- delete: boolean;
142
- export: boolean;
143
- import: boolean;
144
- approve: boolean;
145
- reject: boolean;
146
- } {
147
- if (BYPASS_AUTH) {
148
- return {
149
- create: true,
150
- view: true,
151
- update: true,
152
- delete: true,
153
- export: true,
154
- import: true,
155
- approve: true,
156
- reject: true,
157
- };
158
- }
159
-
160
- if (!session?.user) {
161
- return {
162
- create: false,
163
- view: false,
164
- update: false,
165
- delete: false,
166
- export: false,
167
- import: false,
168
- approve: false,
169
- reject: false,
170
- };
171
- }
172
-
173
- const user = session.user as ExtendedUser;
174
-
175
- if (!user.id) {
176
- return {
177
- create: false,
178
- view: false,
179
- update: false,
180
- delete: false,
181
- export: false,
182
- import: false,
183
- approve: false,
184
- reject: false,
185
- };
186
- }
187
-
188
- const isAdmin = user.roles?.some((role) => ADMIN_ROLE_CODES.includes(role));
189
- if (isAdmin) {
190
- return {
191
- create: true,
192
- view: true,
193
- update: true,
194
- delete: true,
195
- export: true,
196
- import: true,
197
- approve: true,
198
- reject: true,
199
- };
200
- }
201
-
202
- const permissions = user.permissions || [];
203
- const permissionKeys = new Set(
204
- permissions.map((p) => `${p.resourceCode}:${p.actionCode}`),
205
- );
206
-
207
- const hasPermission = (action: string) => {
208
- const key = `${entity}:${action}`;
209
- return permissionKeys.has(key);
210
- };
211
-
212
- return {
213
- create: hasPermission(getActionCode("create")),
214
- view: hasPermission(getActionCode("view")),
215
- update: hasPermission(getActionCode("update")),
216
- delete: hasPermission(getActionCode("delete")),
217
- export: hasPermission(getActionCode("export")),
218
- import: hasPermission(getActionCode("import")),
219
- approve: hasPermission(getActionCode("approve")),
220
- reject: hasPermission(getActionCode("reject")),
221
- };
222
- }
223
-
224
- export function checkPermission(
225
- session: Session | null,
226
- resourceCode: string,
227
- actionCode: string,
228
- ): boolean {
229
- if (BYPASS_AUTH) {
230
- return true;
231
- }
232
-
233
- if (!session?.user) return false;
234
- const user = session.user as ExtendedUser;
235
-
236
- if (!user.permissions || user.permissions.length === 0) {
237
- return false;
238
- }
239
-
240
- if (user.roles?.some((role) => ADMIN_ROLE_CODES.includes(role))) {
241
- return true;
242
- }
243
-
244
- const permissionKey = `${resourceCode}:${actionCode}`;
245
- return user.permissions.some(
246
- (p) => p.resourceCode === resourceCode && p.actionCode === actionCode,
247
- );
248
- }
249
-
250
- export function hasPermission(
251
- session: Session | null,
252
- resourceCode: string,
253
- actionCode: string,
254
- ): boolean {
255
- return checkPermission(session, resourceCode, actionCode);
256
- }
257
-
258
- export function requirePermission(
259
- session: Session | null,
260
- resourceCode: string,
261
- actionCode: string,
262
- ): void {
263
- if (!checkPermission(session, resourceCode, actionCode)) {
264
- throw new Error(
265
- `Unauthorized: User does not have permission ${actionCode} on ${resourceCode}`,
266
- );
267
- }
268
- }
269
-
270
- export function hasRole(session: Session | null, roleCode: string): boolean {
271
- if (!session?.user) return false;
272
- const user = session.user as ExtendedUser;
273
-
274
- if (!user.roles) {
275
- return false;
276
- }
277
- return user.roles.includes(roleCode);
278
- }
279
-
280
- export function hasAnyRole(
281
- session: Session | null,
282
- roleCodes: string[],
283
- ): boolean {
284
- if (!session?.user) return false;
285
- const user = session.user as ExtendedUser;
286
-
287
- if (!user.roles) {
288
- return false;
289
- }
290
- return roleCodes.some((role) => user.roles!.includes(role));
291
- }
292
134
  export * from "./permission-service";
293
135
  export * from "./role-service";
294
136
  export * from "./resource-service";
@@ -43,7 +43,7 @@ import {
43
43
  SelectValue,
44
44
  Switch,
45
45
  Checkbox,
46
- } from "@goerp/core/ui";
46
+ } from "../../../../ui";
47
47
 
48
48
  interface CategoryListProps {
49
49
  group: SystemCategoryGroup;
@@ -4,7 +4,7 @@ import { useState } from "react";
4
4
  import { Search } from "lucide-react";
5
5
 
6
6
  import type { SystemCategoryGroup } from "../../../types";
7
- import { Input } from "@goerp/core/ui";
7
+ import { Input } from "../../../../ui";
8
8
  import { GroupSidebar } from "./group-sidebar";
9
9
  import { CategoryList } from "./category-list";
10
10
 
@@ -11,7 +11,7 @@ import {
11
11
  DropdownMenuItem,
12
12
  DropdownMenuTrigger,
13
13
  ScrollArea,
14
- } from "@goerp/core/ui";
14
+ } from "../../../../ui";
15
15
  import { cn } from "../../../../utils";
16
16
  // Assuming we'll implement these dialogs or use generic ones
17
17
  // For now, I'll assume we might use a generic form or specific dialogs
@@ -34,7 +34,7 @@ import {
34
34
  FormMessage,
35
35
  Input as FormInput,
36
36
  Textarea,
37
- } from "@goerp/core/ui";
37
+ } from "../../../../ui";
38
38
  import { systemCategoryGroupSchema } from "../../../schemas/system-category-group.schema";
39
39
 
40
40
  interface GroupSidebarProps {
@@ -2,13 +2,24 @@
2
2
 
3
3
  import Image from "next/image";
4
4
 
5
+ import { useOptionalTenant } from "../../providers/tenant-provider";
5
6
  import { cn } from "../../utils";
6
7
 
8
+ // Defaults used when neither props nor a TenantProvider supply branding — keeps
9
+ // the component working out of the box (and backward compatible).
10
+ const DEFAULT_LOGO_SRC = "/images/logos/goeat_logo.png";
11
+ const DEFAULT_COMPANY = "GoERP";
12
+ const DEFAULT_TAGLINE = "ERP System";
13
+
7
14
  interface LogoProps {
8
15
  className?: string;
9
16
  size?: "sm" | "md" | "lg" | "xl" | "2xl";
10
17
  showText?: boolean;
11
18
  collapsed?: boolean;
19
+ /** Direct overrides. Otherwise read from TenantProvider branding, else defaults. */
20
+ logoSrc?: string;
21
+ companyName?: string;
22
+ tagline?: string;
12
23
  }
13
24
 
14
25
  const sizeMap = {
@@ -24,14 +35,21 @@ export function Logo({
24
35
  size = "md",
25
36
  showText = true,
26
37
  collapsed = false,
38
+ logoSrc,
39
+ companyName,
40
+ tagline,
27
41
  }: LogoProps) {
42
+ const branding = useOptionalTenant()?.branding;
43
+ const src = logoSrc ?? branding?.logo ?? DEFAULT_LOGO_SRC;
44
+ const name = companyName ?? branding?.companyName ?? DEFAULT_COMPANY;
45
+ const sub = tagline ?? branding?.tagline ?? DEFAULT_TAGLINE;
28
46
  const dimensions = sizeMap[size];
29
47
 
30
48
  return (
31
49
  <div className={cn("flex items-center gap-2", className)}>
32
50
  <Image
33
- src="/images/logos/goeat_logo.png"
34
- alt="GoERP Logo"
51
+ src={src}
52
+ alt={`${name} Logo`}
35
53
  width={dimensions.width}
36
54
  height={dimensions.height}
37
55
  className="object-contain"
@@ -39,20 +57,34 @@ export function Logo({
39
57
  />
40
58
  {showText && !collapsed && (
41
59
  <div className="flex flex-col leading-none">
42
- <span className="text-base font-bold text-foreground">GoERP</span>
43
- <span className="text-[11px] text-muted-foreground">ERP System</span>
60
+ <span className="text-base font-bold text-foreground">{name}</span>
61
+ {sub && (
62
+ <span className="text-[11px] text-muted-foreground">{sub}</span>
63
+ )}
44
64
  </div>
45
65
  )}
46
66
  </div>
47
67
  );
48
68
  }
49
69
 
50
- export function LogoCompact({ className }: { className?: string }) {
70
+ export function LogoCompact({
71
+ className,
72
+ logoSrc,
73
+ companyName,
74
+ }: {
75
+ className?: string;
76
+ logoSrc?: string;
77
+ companyName?: string;
78
+ }) {
79
+ const branding = useOptionalTenant()?.branding;
80
+ const src = logoSrc ?? branding?.logo ?? DEFAULT_LOGO_SRC;
81
+ const name = companyName ?? branding?.companyName ?? DEFAULT_COMPANY;
82
+
51
83
  return (
52
84
  <div className={cn("flex items-center justify-center", className)}>
53
85
  <Image
54
- src="/images/logos/goeat_logo.png"
55
- alt="GoERP"
86
+ src={src}
87
+ alt={name}
56
88
  width={32}
57
89
  height={32}
58
90
  className="object-contain"