@ews-admin/global-design-system 1.1.21 → 1.1.23

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.
@@ -0,0 +1,32 @@
1
+ export type Environment = "local" | "dev" | "qa" | "prod";
2
+ export interface EnvConfigOverrides {
3
+ bffUrl?: string;
4
+ loginBffUrl?: string;
5
+ appUrl?: string;
6
+ loginUrl?: string;
7
+ dashboardUrl?: string;
8
+ adminUrl?: string;
9
+ }
10
+ export interface EnvConfig {
11
+ env: Environment;
12
+ bffUrl: string;
13
+ loginBffUrl: string;
14
+ appUrl: string;
15
+ loginUrl: string;
16
+ dashboardUrl: string;
17
+ adminUrl: string;
18
+ }
19
+ /**
20
+ * Creates an environment configuration with all derived URLs.
21
+ *
22
+ * For deployed environments (dev, qa, prod), only the environment name is needed
23
+ * — all URLs follow a predictable convention.
24
+ *
25
+ * For local development, sensible defaults are provided but can be overridden
26
+ * (e.g. to point BFF URL at a mock server).
27
+ *
28
+ * @param env - The environment name (VITE_ENV value)
29
+ * @param overrides - Optional URL overrides (e.g. for local mock servers)
30
+ */
31
+ export declare function createEnvConfig(env: string, overrides?: EnvConfigOverrides): EnvConfig;
32
+ //# sourceMappingURL=env-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-config.d.ts","sourceRoot":"","sources":["../../src/utils/env-config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAsCD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,kBAAkB,GAC7B,SAAS,CASX"}
@@ -1,4 +1,6 @@
1
1
  import { type ClassValue } from "clsx";
2
+ export { createEnvConfig } from "./env-config";
3
+ export type { EnvConfig, EnvConfigOverrides, Environment, } from "./env-config";
2
4
  /**
3
5
  * Default currency for price formatting
4
6
  */
@@ -49,4 +51,23 @@ export declare const formatNumeric: (value: string) => string;
49
51
  * @returns Boolean indicating if the phone number is valid
50
52
  */
51
53
  export declare function isValidPhoneNumber(value: string): boolean;
54
+ /**
55
+ * Blood type enum matching backend BloodTypeEnum values
56
+ */
57
+ export declare enum BloodType {
58
+ A_POSITIVE = "A+",
59
+ A_NEGATIVE = "A-",
60
+ B_POSITIVE = "B+",
61
+ B_NEGATIVE = "B-",
62
+ AB_POSITIVE = "AB+",
63
+ AB_NEGATIVE = "AB-",
64
+ O_POSITIVE = "O+",
65
+ O_NEGATIVE = "O-",
66
+ UNKNOWN = "UNKNOWN"
67
+ }
68
+ /**
69
+ * Ordered list of all blood type values.
70
+ * Use this to build select/dropdown options.
71
+ */
72
+ export declare const BLOOD_TYPES: BloodType[];
52
73
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAChE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMzD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB;;GAEG;AACH,eAAO,MAAM,QAAQ,QAAQ,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAW,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,GACnC,MAAM,CAQR;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAChE,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,MAAM,GACX,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,CAMlC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAQ,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAE7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAMzD;AAED;;GAEG;AACH,oBAAY,SAAS;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,WAAW,QAAQ;IACnB,WAAW,QAAQ;IACnB,UAAU,OAAO;IACjB,UAAU,OAAO;IACjB,OAAO,YAAY;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,EAUlC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ews-admin/global-design-system",
3
- "version": "1.1.21",
3
+ "version": "1.1.23",
4
4
  "description": "EWS Global Design System - Reusable components for EWS applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -53,6 +53,14 @@ export interface InputProps
53
53
  * Icon to display after the input
54
54
  */
55
55
  rightIcon?: React.ReactNode;
56
+ /**
57
+ * Text or node to display as a left addon (e.g., currency "FCFA")
58
+ */
59
+ leftAddon?: React.ReactNode;
60
+ /**
61
+ * Text or node to display as a right addon inside the input (e.g., currency "FCFA")
62
+ */
63
+ rightAddon?: React.ReactNode;
56
64
  /**
57
65
  * Whether the input should take full width
58
66
  */
@@ -86,6 +94,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
86
94
  showPasswordToggle = false,
87
95
  required = false,
88
96
  countryCodeSelect,
97
+ leftAddon,
98
+ rightAddon,
89
99
  id,
90
100
  type = "text",
91
101
  ...props
@@ -274,13 +284,20 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
274
284
  </div>
275
285
  </div>
276
286
  )}
277
- {leftIcon && !countryCodeSelect && (
287
+ {leftIcon && !countryCodeSelect && !leftAddon && (
278
288
  <div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
279
289
  <span className={cn("text-ews-gray-400", iconSizes[size])}>
280
290
  {leftIcon}
281
291
  </span>
282
292
  </div>
283
293
  )}
294
+ {leftAddon && !countryCodeSelect && !leftIcon && (
295
+ <div className="flex absolute inset-y-0 left-0 items-center pointer-events-none overflow-hidden rounded-l-md">
296
+ <span className="flex items-center h-full px-3 text-sm font-medium whitespace-nowrap bg-ews-gray-50 border-r border-ews-gray-300 text-ews-gray-600">
297
+ {leftAddon}
298
+ </span>
299
+ </div>
300
+ )}
284
301
  <input
285
302
  id={inputId}
286
303
  type={actualType}
@@ -289,13 +306,22 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
289
306
  variants[actualVariant],
290
307
  sizes[size],
291
308
  countryCodeSelect && "pl-24",
292
- leftIcon && !countryCodeSelect && "pl-10",
309
+ leftIcon && !countryCodeSelect && !leftAddon && "pl-10",
310
+ leftAddon && !countryCodeSelect && !leftIcon && "pl-16",
293
311
  (rightIcon || shouldShowPasswordToggle) && "pr-10",
312
+ rightAddon && !rightIcon && !shouldShowPasswordToggle && "pr-16",
294
313
  className
295
314
  )}
296
315
  ref={ref}
297
316
  {...props}
298
317
  />
318
+ {rightAddon && !rightIcon && !shouldShowPasswordToggle && (
319
+ <div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
320
+ <span className="text-sm font-medium text-ews-gray-500">
321
+ {rightAddon}
322
+ </span>
323
+ </div>
324
+ )}
299
325
  {rightIcon && !shouldShowPasswordToggle && (
300
326
  <div className="flex absolute inset-y-0 right-0 items-center pr-3 pointer-events-none">
301
327
  <span className={cn("text-ews-gray-400", iconSizes[size])}>
@@ -0,0 +1,231 @@
1
+ import { Pencil, Trash } from "lucide-react";
2
+ import React, { useEffect, useRef, useState } from "react";
3
+
4
+ import { cn } from "../../utils";
5
+ import { Modal } from "../Modal/Modal";
6
+
7
+ export interface ProfileImageUploadProps {
8
+ /** Current image URL */
9
+ imageUrl: string;
10
+ /** Alt text for accessibility */
11
+ altText: string;
12
+ /** Disable editing */
13
+ readOnly?: boolean;
14
+ /** Avatar diameter */
15
+ size?: "sm" | "md" | "lg";
16
+ /**
17
+ * Upload progress 0–100.
18
+ * Progress bar is hidden when 0; auto-hides 1 s after reaching 100.
19
+ */
20
+ uploadProgress?: number;
21
+ /**
22
+ * When true the progress bar stays visible in an indeterminate (pulsing)
23
+ * state for phases with no measurable progress (pre-upload / post-upload).
24
+ * The bar auto-hides 1 s after both `isLoading` is false and progress reached 100.
25
+ */
26
+ isLoading?: boolean;
27
+ /**
28
+ * Whether to show the delete button.
29
+ * Pass `false` when `imageUrl` is a default/fallback image.
30
+ */
31
+ showDeleteButton?: boolean;
32
+ /** File input accept attribute (default: "image/*") */
33
+ accept?: string;
34
+ /** Max allowed file size in MB (default: 3) */
35
+ maxFileSizeMB?: number;
36
+ /** Called when a valid file is selected */
37
+ onFileSelect: (file: File) => void;
38
+ /** Called when the selected file exceeds `maxFileSizeMB` */
39
+ onFileSizeExceeded?: () => void;
40
+ /** Called when the user confirms deletion in the modal */
41
+ onDeleteConfirm: () => void;
42
+ /** Deletion confirmation modal — title text */
43
+ deleteConfirmTitle: string;
44
+ /** Deletion confirmation modal — body text */
45
+ deleteConfirmMessage: string;
46
+ /** Deletion confirmation modal — primary button label */
47
+ deleteConfirmLabel: string;
48
+ /** Deletion confirmation modal — secondary / cancel label */
49
+ cancelLabel: string;
50
+ }
51
+
52
+ const SIZE_CLASSES: Record<"sm" | "md" | "lg", string> = {
53
+ sm: "h-10 w-10",
54
+ md: "h-16 w-16",
55
+ lg: "h-32 w-32",
56
+ };
57
+
58
+ const INDICATOR_CLASSES: Record<"sm" | "md" | "lg", string> = {
59
+ sm: "h-2 w-2",
60
+ md: "h-4 w-4",
61
+ lg: "h-8 w-8",
62
+ };
63
+
64
+ function UploadProgressBar({
65
+ progress,
66
+ isLoading,
67
+ }: {
68
+ progress: number;
69
+ isLoading?: boolean;
70
+ }) {
71
+ const [visible, setVisible] = useState(false);
72
+
73
+ useEffect(() => {
74
+ if (isLoading) {
75
+ setVisible(true);
76
+ return;
77
+ }
78
+ // isLoading just turned false
79
+ if (progress >= 100) {
80
+ setVisible(true);
81
+ const t = setTimeout(() => setVisible(false), 1000);
82
+ return () => clearTimeout(t);
83
+ }
84
+ // Legacy path (no isLoading prop): driven purely by progress
85
+ if (progress > 0 && progress < 100) {
86
+ setVisible(true);
87
+ return;
88
+ }
89
+ setVisible(false);
90
+ }, [progress, isLoading]);
91
+
92
+ if (!visible) return null;
93
+
94
+ const indeterminate = isLoading && progress === 0;
95
+
96
+ return (
97
+ <div className="mt-2 h-1.5 w-32 rounded-full bg-gray-200">
98
+ <div
99
+ className={cn(
100
+ "h-full rounded-full bg-ews-primary transition-all duration-300",
101
+ indeterminate && "animate-pulse"
102
+ )}
103
+ style={{ width: indeterminate ? "35%" : `${progress}%` }}
104
+ />
105
+ </div>
106
+ );
107
+ }
108
+
109
+ export const ProfileImageUpload: React.FC<ProfileImageUploadProps> = ({
110
+ imageUrl,
111
+ altText,
112
+ readOnly = false,
113
+ size = "lg",
114
+ uploadProgress = 0,
115
+ isLoading = false,
116
+ showDeleteButton = true,
117
+ accept = "image/*",
118
+ maxFileSizeMB = 3,
119
+ onFileSelect,
120
+ onFileSizeExceeded,
121
+ onDeleteConfirm,
122
+ deleteConfirmTitle,
123
+ deleteConfirmMessage,
124
+ deleteConfirmLabel,
125
+ cancelLabel,
126
+ }) => {
127
+ const fileInputRef = useRef<HTMLInputElement>(null);
128
+ const [isHovered, setIsHovered] = useState(false);
129
+ const [showConfirm, setShowConfirm] = useState(false);
130
+
131
+ const handleEditClick = () => fileInputRef.current?.click();
132
+
133
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
134
+ const file = e.target.files?.[0];
135
+ if (!file) return;
136
+
137
+ if (file.size > maxFileSizeMB * 1024 * 1024) {
138
+ onFileSizeExceeded?.();
139
+ e.target.value = "";
140
+ return;
141
+ }
142
+
143
+ onFileSelect(file);
144
+ e.target.value = ""; // allow re-selecting the same file
145
+ };
146
+
147
+ const handleConfirmDelete = () => {
148
+ setShowConfirm(false);
149
+ onDeleteConfirm();
150
+ };
151
+
152
+ return (
153
+ <>
154
+ <div className="inline-flex flex-col items-center">
155
+ <div
156
+ className="relative"
157
+ onMouseEnter={() => setIsHovered(true)}
158
+ onMouseLeave={() => setIsHovered(false)}
159
+ >
160
+ <img
161
+ src={imageUrl}
162
+ alt={altText}
163
+ className={cn(
164
+ "rounded-full border-4 border-white object-cover",
165
+ SIZE_CLASSES[size]
166
+ )}
167
+ />
168
+
169
+ {/* Online status dot */}
170
+ <div
171
+ className={cn(
172
+ "absolute bottom-0 right-0 rounded-full border-4 border-white bg-green-400",
173
+ INDICATOR_CLASSES[size]
174
+ )}
175
+ />
176
+
177
+ <input
178
+ type="file"
179
+ ref={fileInputRef}
180
+ accept={accept}
181
+ className="hidden"
182
+ onChange={handleFileChange}
183
+ />
184
+
185
+ {isHovered && !readOnly && !isLoading && (
186
+ <>
187
+ {/* Edit / upload button */}
188
+ <button
189
+ type="button"
190
+ onClick={handleEditClick}
191
+ className="absolute left-0 top-0 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-ews-primary transition-colors hover:bg-ews-secondary"
192
+ aria-label="Edit profile image"
193
+ >
194
+ <Pencil className="h-4 w-4 text-white" />
195
+ </button>
196
+
197
+ {/* Delete button — only when not a fallback/default image */}
198
+ {showDeleteButton && (
199
+ <button
200
+ type="button"
201
+ onClick={() => setShowConfirm(true)}
202
+ className="absolute left-0 top-10 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-red-600 transition-colors hover:bg-red-700"
203
+ aria-label="Delete profile image"
204
+ >
205
+ <Trash className="h-4 w-4 text-white" />
206
+ </button>
207
+ )}
208
+ </>
209
+ )}
210
+ </div>
211
+
212
+ <UploadProgressBar progress={uploadProgress} isLoading={isLoading} />
213
+ </div>
214
+
215
+ {/* Delete confirmation */}
216
+ <Modal
217
+ isOpen={showConfirm}
218
+ onClose={() => setShowConfirm(false)}
219
+ title={deleteConfirmTitle}
220
+ variant="error"
221
+ size="sm"
222
+ primaryAction={deleteConfirmLabel}
223
+ secondaryAction={cancelLabel}
224
+ onPrimaryAction={handleConfirmDelete}
225
+ onSecondaryAction={() => setShowConfirm(false)}
226
+ >
227
+ <p>{deleteConfirmMessage}</p>
228
+ </Modal>
229
+ </>
230
+ );
231
+ };
@@ -0,0 +1,2 @@
1
+ export { ProfileImageUpload } from "./ProfileImageUpload";
2
+ export type { ProfileImageUploadProps } from "./ProfileImageUpload";
package/src/index.ts CHANGED
@@ -16,6 +16,9 @@ export { MultiSearchAutocomplete } from "./components/MultiSearchAutocomplete";
16
16
  export { Modal } from "./components/Modal";
17
17
  export type { ErrorField, ErrorObject, ModalProps } from "./components/Modal";
18
18
 
19
+ export { ProfileImageUpload } from "./components/ProfileImageUpload";
20
+ export type { ProfileImageUploadProps } from "./components/ProfileImageUpload";
21
+
19
22
  export { DropdownMultiSelect } from "./components/DropdownMultiSelect";
20
23
  export type { DropdownMultiSelectProps } from "./components/DropdownMultiSelect";
21
24
 
@@ -46,7 +49,10 @@ export type { IconProps, SimpleIconProps } from "./icons";
46
49
 
47
50
  // Utils
48
51
  export {
52
+ BLOOD_TYPES,
53
+ BloodType,
49
54
  cn,
55
+ createEnvConfig,
50
56
  debounce,
51
57
  formatCurrency,
52
58
  formatDate,
@@ -54,6 +60,7 @@ export {
54
60
  generateId,
55
61
  isValidPhoneNumber,
56
62
  } from "./utils";
63
+ export type { EnvConfig, EnvConfigOverrides, Environment } from "./utils";
57
64
 
58
65
  // Hooks
59
66
  export { useDebounce, useDebouncedCallback, useSelectField } from "./hooks";
@@ -0,0 +1,82 @@
1
+ export type Environment = "local" | "dev" | "qa" | "prod";
2
+
3
+ export interface EnvConfigOverrides {
4
+ bffUrl?: string;
5
+ loginBffUrl?: string;
6
+ appUrl?: string;
7
+ loginUrl?: string;
8
+ dashboardUrl?: string;
9
+ adminUrl?: string;
10
+ }
11
+
12
+ export interface EnvConfig {
13
+ env: Environment;
14
+ bffUrl: string;
15
+ loginBffUrl: string;
16
+ appUrl: string;
17
+ loginUrl: string;
18
+ dashboardUrl: string;
19
+ adminUrl: string;
20
+ }
21
+
22
+ const LOCAL_PORTS = {
23
+ bff: 8082,
24
+ loginBff: 8080,
25
+ app: 3000,
26
+ login: 3001,
27
+ dashboard: 3002,
28
+ admin: 3008,
29
+ } as const;
30
+
31
+ function buildUrls(environment: Environment): Omit<EnvConfig, "env"> {
32
+ if (environment === "local") {
33
+ const base = "http://local";
34
+ const domain = "medecine360local.com";
35
+ return {
36
+ bffUrl: `${base}.api.${domain}:${LOCAL_PORTS.bff}/bff/api/v1`,
37
+ loginBffUrl: `${base}.api.${domain}:${LOCAL_PORTS.loginBff}/login-bff/api/v1`,
38
+ appUrl: `${base}.app.${domain}:${LOCAL_PORTS.app}`,
39
+ loginUrl: `${base}.login.${domain}:${LOCAL_PORTS.login}`,
40
+ dashboardUrl: `${base}.dashboard.${domain}:${LOCAL_PORTS.dashboard}`,
41
+ adminUrl: `${base}.admin.${domain}:${LOCAL_PORTS.admin}`,
42
+ };
43
+ }
44
+
45
+ const prefix = environment === "prod" ? "" : `${environment}.`;
46
+ const domain = "medecine360.com";
47
+
48
+ return {
49
+ bffUrl: `https://${prefix}api.${domain}/bff/api/v1`,
50
+ loginBffUrl: `https://${prefix}api.${domain}/login-bff/api/v1`,
51
+ appUrl: `https://${prefix}app.${domain}`,
52
+ loginUrl: `https://${prefix}login.${domain}`,
53
+ dashboardUrl: `https://${prefix}dashboard.${domain}`,
54
+ adminUrl: `https://${prefix}admin.${domain}`,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Creates an environment configuration with all derived URLs.
60
+ *
61
+ * For deployed environments (dev, qa, prod), only the environment name is needed
62
+ * — all URLs follow a predictable convention.
63
+ *
64
+ * For local development, sensible defaults are provided but can be overridden
65
+ * (e.g. to point BFF URL at a mock server).
66
+ *
67
+ * @param env - The environment name (VITE_ENV value)
68
+ * @param overrides - Optional URL overrides (e.g. for local mock servers)
69
+ */
70
+ export function createEnvConfig(
71
+ env: string,
72
+ overrides?: EnvConfigOverrides
73
+ ): EnvConfig {
74
+ const environment = (env || "local") as Environment;
75
+ const derived = buildUrls(environment);
76
+
77
+ return {
78
+ env: environment,
79
+ ...derived,
80
+ ...overrides,
81
+ };
82
+ }
@@ -1,5 +1,12 @@
1
1
  import { clsx, type ClassValue } from "clsx";
2
2
 
3
+ export { createEnvConfig } from "./env-config";
4
+ export type {
5
+ EnvConfig,
6
+ EnvConfigOverrides,
7
+ Environment,
8
+ } from "./env-config";
9
+
3
10
  /**
4
11
  * Default currency for price formatting
5
12
  */
@@ -94,3 +101,34 @@ export function isValidPhoneNumber(value: string): boolean {
94
101
  const phoneRegex = /^(\+\d{1,17}|\d{1,17})$/;
95
102
  return phoneRegex.test(trimmedValue);
96
103
  }
104
+
105
+ /**
106
+ * Blood type enum matching backend BloodTypeEnum values
107
+ */
108
+ export enum BloodType {
109
+ A_POSITIVE = "A+",
110
+ A_NEGATIVE = "A-",
111
+ B_POSITIVE = "B+",
112
+ B_NEGATIVE = "B-",
113
+ AB_POSITIVE = "AB+",
114
+ AB_NEGATIVE = "AB-",
115
+ O_POSITIVE = "O+",
116
+ O_NEGATIVE = "O-",
117
+ UNKNOWN = "UNKNOWN",
118
+ }
119
+
120
+ /**
121
+ * Ordered list of all blood type values.
122
+ * Use this to build select/dropdown options.
123
+ */
124
+ export const BLOOD_TYPES: BloodType[] = [
125
+ BloodType.A_POSITIVE,
126
+ BloodType.A_NEGATIVE,
127
+ BloodType.B_POSITIVE,
128
+ BloodType.B_NEGATIVE,
129
+ BloodType.AB_POSITIVE,
130
+ BloodType.AB_NEGATIVE,
131
+ BloodType.O_POSITIVE,
132
+ BloodType.O_NEGATIVE,
133
+ BloodType.UNKNOWN,
134
+ ];