@khester/create-dynamics-app 1.0.8 → 1.1.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 (107) hide show
  1. package/bin/create-dynamics-app.js +1 -1
  2. package/dist/index.js +140 -15
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/consultingHelpers.d.ts +13 -0
  5. package/dist/utils/consultingHelpers.d.ts.map +1 -0
  6. package/dist/utils/consultingHelpers.js +569 -0
  7. package/dist/utils/consultingHelpers.js.map +1 -0
  8. package/dist/utils/copyTemplate.d.ts.map +1 -1
  9. package/dist/utils/copyTemplate.js.map +1 -1
  10. package/dist/utils/initGit.d.ts.map +1 -1
  11. package/dist/utils/initGit.js.map +1 -1
  12. package/dist/utils/installDependencies.d.ts.map +1 -1
  13. package/dist/utils/installDependencies.js +3 -2
  14. package/dist/utils/installDependencies.js.map +1 -1
  15. package/dist/utils/updatePackageJson.d.ts +1 -1
  16. package/dist/utils/updatePackageJson.d.ts.map +1 -1
  17. package/dist/utils/updatePackageJson.js +11 -1
  18. package/dist/utils/updatePackageJson.js.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
  21. package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
  22. package/templates/dynamics-365-starter/README.md +566 -137
  23. package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
  24. package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
  25. package/templates/dynamics-365-starter/deployment/README.md +484 -0
  26. package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
  27. package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
  28. package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
  29. package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
  30. package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
  31. package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
  32. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
  33. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
  34. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
  35. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
  36. package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
  37. package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
  38. package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
  39. package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
  40. package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
  41. package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
  42. package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
  43. package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
  44. package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
  45. package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
  46. package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
  47. package/templates/dynamics-365-starter/package.json +22 -1
  48. package/templates/dynamics-365-starter/public/index.html +8 -11
  49. package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
  50. package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
  51. package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
  52. package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
  53. package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
  54. package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
  55. package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
  56. package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
  57. package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
  58. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
  59. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
  60. package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
  61. package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
  62. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
  63. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
  64. package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
  65. package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
  66. package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
  67. package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
  68. package/templates/dynamics-365-starter/src/examples/README.md +52 -0
  69. package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
  70. package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
  71. package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
  72. package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
  73. package/templates/dynamics-365-starter/src/index.tsx +107 -19
  74. package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
  75. package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
  76. package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
  77. package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
  78. package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
  79. package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
  80. package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
  81. package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
  82. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
  83. package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
  84. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
  85. package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
  86. package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
  87. package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
  88. package/templates/dynamics-365-starter/src/styles/index.css +74 -7
  89. package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
  90. package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
  91. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
  92. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
  93. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
  94. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
  95. package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
  96. package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
  97. package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
  98. package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
  99. package/templates/dynamics-365-starter/tsconfig.json +11 -8
  100. package/templates/dynamics-365-starter/webpack.config.js +8 -9
  101. package/templates/power-pages-starter/README.md +7 -1
  102. package/templates/power-pages-starter/public/index.html +8 -11
  103. package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
  104. package/templates/power-pages-starter/src/index.tsx +3 -3
  105. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
  106. package/templates/power-pages-starter/tsconfig.json +3 -9
  107. package/templates/power-pages-starter/webpack.config.js +8 -3
@@ -0,0 +1,370 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import {
3
+ Button,
4
+ TextField,
5
+ DetailsList,
6
+ Dialog,
7
+ Panel,
8
+ } from '@khester/dynamics-ui-components';
9
+ import {
10
+ SelectionMode,
11
+ DetailsListLayoutMode,
12
+ CheckboxVisibility,
13
+ } from '@fluentui/react';
14
+ import { useDynamicsApi } from '../providers/DynamicsProvider';
15
+ import { Account } from '../models/Account';
16
+ import { AccountConstants } from '../constants/account';
17
+ import { Logger } from './Logging/logger';
18
+ import { AccountForm } from './AccountForm';
19
+ import './AccountManagement.css';
20
+
21
+ const columns = [
22
+ {
23
+ key: AccountConstants.PrimaryName,
24
+ name: 'Account Name',
25
+ fieldName: AccountConstants.PrimaryName,
26
+ minWidth: 200,
27
+ maxWidth: 300,
28
+ isResizable: true,
29
+ },
30
+ {
31
+ key: AccountConstants.AccountNumber,
32
+ name: 'Account Number',
33
+ fieldName: AccountConstants.AccountNumber,
34
+ minWidth: 120,
35
+ maxWidth: 160,
36
+ isResizable: true,
37
+ },
38
+ {
39
+ key: AccountConstants.EMailAddress1,
40
+ name: 'Email',
41
+ fieldName: AccountConstants.EMailAddress1,
42
+ minWidth: 200,
43
+ maxWidth: 300,
44
+ isResizable: true,
45
+ },
46
+ {
47
+ key: AccountConstants.Telephone1,
48
+ name: 'Phone',
49
+ fieldName: AccountConstants.Telephone1,
50
+ minWidth: 150,
51
+ maxWidth: 200,
52
+ isResizable: true,
53
+ },
54
+ {
55
+ key: AccountConstants.WebSiteURL,
56
+ name: 'Website',
57
+ fieldName: AccountConstants.WebSiteURL,
58
+ minWidth: 150,
59
+ maxWidth: 200,
60
+ isResizable: true,
61
+ },
62
+ {
63
+ key: AccountConstants.CreatedOn,
64
+ name: 'Created',
65
+ fieldName: AccountConstants.CreatedOn,
66
+ minWidth: 120,
67
+ maxWidth: 160,
68
+ isResizable: true,
69
+ },
70
+ ];
71
+
72
+ export const AccountManagement: React.FC = () => {
73
+ const { apiService, isEnvironmentMock, environmentType } = useDynamicsApi();
74
+
75
+ const [accounts, setAccounts] = useState<Account[]>([]);
76
+ const [filteredAccounts, setFilteredAccounts] = useState<Account[]>([]);
77
+ const [loading, setLoading] = useState(false);
78
+ const [searchText, setSearchText] = useState('');
79
+ const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
80
+ const [showNewAccountPanel, setShowNewAccountPanel] = useState(false);
81
+ const [showEditAccountPanel, setShowEditAccountPanel] = useState(false);
82
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
83
+ const [accountToDelete, setAccountToDelete] = useState<Account | null>(null);
84
+
85
+ useEffect(() => {
86
+ Logger.userAction(
87
+ 'AccountManagement loaded',
88
+ { environmentType, isEnvironmentMock },
89
+ 'AccountManagement'
90
+ );
91
+ }, [environmentType, isEnvironmentMock]);
92
+
93
+ const loadAccounts = useCallback(async () => {
94
+ if (!apiService) {
95
+ Logger.error(
96
+ 'API service not available',
97
+ 'AccountManagement.loadAccounts'
98
+ );
99
+ return;
100
+ }
101
+
102
+ setLoading(true);
103
+ Logger.log('Loading accounts', 'AccountManagement.loadAccounts');
104
+
105
+ try {
106
+ const accountsData = await Account.retrieveActiveAccounts(apiService);
107
+
108
+ const accountsWithDisplayData = accountsData.map((account) => {
109
+ const accountInstance = new Account(account);
110
+ return {
111
+ ...accountInstance,
112
+ createdon: account.createdon
113
+ ? new Date(account.createdon).toLocaleDateString()
114
+ : '',
115
+ } as Account;
116
+ });
117
+
118
+ setAccounts(accountsWithDisplayData);
119
+ setFilteredAccounts(accountsWithDisplayData);
120
+
121
+ Logger.log(
122
+ `Successfully loaded ${accountsWithDisplayData.length} accounts`,
123
+ 'AccountManagement.loadAccounts'
124
+ );
125
+ } catch (error) {
126
+ Logger.error(
127
+ 'Failed to load accounts',
128
+ 'AccountManagement.loadAccounts',
129
+ error
130
+ );
131
+ } finally {
132
+ setLoading(false);
133
+ }
134
+ }, [apiService]);
135
+
136
+ useEffect(() => {
137
+ loadAccounts();
138
+ }, [loadAccounts]);
139
+
140
+ useEffect(() => {
141
+ if (!searchText) {
142
+ setFilteredAccounts(accounts);
143
+ } else {
144
+ const filtered = accounts.filter(
145
+ (account) =>
146
+ account.name?.toLowerCase().includes(searchText.toLowerCase()) ||
147
+ account.accountnumber
148
+ ?.toLowerCase()
149
+ .includes(searchText.toLowerCase()) ||
150
+ account.emailaddress1
151
+ ?.toLowerCase()
152
+ .includes(searchText.toLowerCase()) ||
153
+ account.telephone1?.includes(searchText) ||
154
+ account.websiteurl?.toLowerCase().includes(searchText.toLowerCase())
155
+ );
156
+ setFilteredAccounts(filtered);
157
+ }
158
+ }, [accounts, searchText]);
159
+
160
+ const handleItemInvoked = useCallback(
161
+ (item: Account) => {
162
+ Logger.userAction(
163
+ 'Account item invoked',
164
+ { accountId: item.accountid, accountName: item.name },
165
+ 'AccountManagement.handleItemInvoked'
166
+ );
167
+
168
+ if (selectedAccount?.accountid === item.accountid) {
169
+ setShowEditAccountPanel(true);
170
+ } else {
171
+ setSelectedAccount(item);
172
+ }
173
+ },
174
+ [selectedAccount]
175
+ );
176
+
177
+ const handleNewAccount = useCallback(() => {
178
+ Logger.userAction(
179
+ 'New account panel opened',
180
+ {},
181
+ 'AccountManagement.handleNewAccount'
182
+ );
183
+ setShowNewAccountPanel(true);
184
+ }, []);
185
+
186
+ const handleEditAccount = useCallback(() => {
187
+ if (selectedAccount) {
188
+ Logger.userAction(
189
+ 'Edit account panel opened',
190
+ {
191
+ accountId: selectedAccount.accountid,
192
+ accountName: selectedAccount.name,
193
+ },
194
+ 'AccountManagement.handleEditAccount'
195
+ );
196
+ setShowEditAccountPanel(true);
197
+ }
198
+ }, [selectedAccount]);
199
+
200
+ const handleDeleteAccount = useCallback(() => {
201
+ if (selectedAccount) {
202
+ Logger.userAction(
203
+ 'Delete account dialog opened',
204
+ {
205
+ accountId: selectedAccount.accountid,
206
+ accountName: selectedAccount.name,
207
+ },
208
+ 'AccountManagement.handleDeleteAccount'
209
+ );
210
+ setAccountToDelete(selectedAccount);
211
+ setShowDeleteDialog(true);
212
+ }
213
+ }, [selectedAccount]);
214
+
215
+ const confirmDelete = useCallback(async () => {
216
+ if (accountToDelete && apiService) {
217
+ try {
218
+ Logger.userAction(
219
+ 'Account deletion confirmed',
220
+ {
221
+ accountId: accountToDelete.accountid,
222
+ accountName: accountToDelete.name,
223
+ },
224
+ 'AccountManagement.confirmDelete'
225
+ );
226
+
227
+ await Account.delete(apiService, accountToDelete.accountid!);
228
+ await loadAccounts();
229
+
230
+ setShowDeleteDialog(false);
231
+ setAccountToDelete(null);
232
+ setSelectedAccount(null);
233
+
234
+ Logger.log(
235
+ `Successfully deleted account: ${accountToDelete.name}`,
236
+ 'AccountManagement.confirmDelete'
237
+ );
238
+ } catch (error) {
239
+ Logger.error(
240
+ `Failed to delete account: ${accountToDelete.name}`,
241
+ 'AccountManagement.confirmDelete',
242
+ error
243
+ );
244
+ }
245
+ }
246
+ }, [accountToDelete, apiService, loadAccounts]);
247
+
248
+ const handleAccountSaved = useCallback(() => {
249
+ Logger.userAction(
250
+ 'Account saved, refreshing list',
251
+ {},
252
+ 'AccountManagement.handleAccountSaved'
253
+ );
254
+ loadAccounts();
255
+ setShowNewAccountPanel(false);
256
+ setShowEditAccountPanel(false);
257
+ setSelectedAccount(null);
258
+ }, [loadAccounts]);
259
+
260
+ return (
261
+ <div className="account-management">
262
+ <div className="account-management__header">
263
+ <h2>Account Management</h2>
264
+ <div className="account-management__actions">
265
+ <TextField
266
+ placeholder="Search accounts..."
267
+ value={searchText}
268
+ onChange={(_, value) => setSearchText(value || '')}
269
+ iconProps={{ iconName: 'Search' }}
270
+ />
271
+ <Button
272
+ text="New Account"
273
+ variant="primary"
274
+ onClick={handleNewAccount}
275
+ iconProps={{ iconName: 'Add' }}
276
+ />
277
+ </div>
278
+ </div>
279
+
280
+ <div className="account-management__toolbar">
281
+ <Button
282
+ text="Edit"
283
+ onClick={handleEditAccount}
284
+ disabled={!selectedAccount}
285
+ iconProps={{ iconName: 'Edit' }}
286
+ />
287
+ <Button
288
+ text="Delete"
289
+ onClick={handleDeleteAccount}
290
+ disabled={!selectedAccount}
291
+ iconProps={{ iconName: 'Delete' }}
292
+ />
293
+ <Button
294
+ text="Refresh"
295
+ onClick={loadAccounts}
296
+ iconProps={{ iconName: 'Refresh' }}
297
+ />
298
+ </div>
299
+
300
+ <div className="account-management__list">
301
+ <DetailsList
302
+ items={filteredAccounts}
303
+ columns={columns}
304
+ onItemInvoked={handleItemInvoked}
305
+ selectionMode={SelectionMode.single}
306
+ layoutMode={DetailsListLayoutMode.justified}
307
+ checkboxVisibility={CheckboxVisibility.onHover}
308
+ />
309
+
310
+ {loading && (
311
+ <div className="account-management__loading">Loading accounts...</div>
312
+ )}
313
+
314
+ {!loading && filteredAccounts.length === 0 && (
315
+ <div className="account-management__empty">
316
+ {searchText
317
+ ? 'No accounts found matching your search.'
318
+ : 'No accounts found. Create your first account!'}
319
+ </div>
320
+ )}
321
+ </div>
322
+
323
+ {/* New Account Panel */}
324
+ <Panel
325
+ isOpen={showNewAccountPanel}
326
+ onDismiss={() => setShowNewAccountPanel(false)}
327
+ headerText="New Account"
328
+ >
329
+ <AccountForm
330
+ onSave={handleAccountSaved}
331
+ onCancel={() => setShowNewAccountPanel(false)}
332
+ />
333
+ </Panel>
334
+
335
+ {/* Edit Account Panel */}
336
+ <Panel
337
+ isOpen={showEditAccountPanel}
338
+ onDismiss={() => {
339
+ setShowEditAccountPanel(false);
340
+ setSelectedAccount(null);
341
+ }}
342
+ headerText="Edit Account"
343
+ >
344
+ {selectedAccount && (
345
+ <AccountForm
346
+ accountId={selectedAccount.accountid}
347
+ initialData={selectedAccount}
348
+ onSave={handleAccountSaved}
349
+ onCancel={() => {
350
+ setShowEditAccountPanel(false);
351
+ setSelectedAccount(null);
352
+ }}
353
+ />
354
+ )}
355
+ </Panel>
356
+
357
+ {/* Delete Confirmation Dialog */}
358
+ <Dialog
359
+ hidden={!showDeleteDialog}
360
+ onDismiss={() => setShowDeleteDialog(false)}
361
+ title="Confirm Delete"
362
+ content={`Are you sure you want to delete ${accountToDelete?.name}? This action cannot be undone.`}
363
+ actions={[
364
+ { text: 'Delete', onClick: confirmDelete, primary: true },
365
+ { text: 'Cancel', onClick: () => setShowDeleteDialog(false) },
366
+ ]}
367
+ />
368
+ </div>
369
+ );
370
+ };
@@ -1,11 +1,17 @@
1
1
  import React, { useState, useCallback, useEffect } from 'react';
2
- import {
3
- Button,
4
- TextField,
5
- Dropdown,
6
- DatePicker
2
+ import {
3
+ Button,
4
+ TextField,
5
+ Dropdown,
6
+ DatePicker,
7
7
  } from '@khester/dynamics-ui-components';
8
8
  import { useDynamicsApi } from '../providers/DynamicsProvider';
9
+ import { Contact, IContact } from '../models/Contact';
10
+ import {
11
+ ContactConstants,
12
+ PreferredContactMethodCode_OptionSet,
13
+ } from '../constants/contact';
14
+ import { Logger } from './Logging/logger';
9
15
  import './ContactForm.css';
10
16
 
11
17
  interface ContactFormData {
@@ -25,120 +31,194 @@ interface ContactFormProps {
25
31
  }
26
32
 
27
33
  const preferredContactOptions = [
28
- { key: 1, text: 'Email' },
29
- { key: 2, text: 'Phone' },
30
- { key: 3, text: 'Mail' }
34
+ { key: PreferredContactMethodCode_OptionSet.Any, text: 'Any' },
35
+ { key: PreferredContactMethodCode_OptionSet.Email, text: 'Email' },
36
+ { key: PreferredContactMethodCode_OptionSet.Phone, text: 'Phone' },
37
+ { key: PreferredContactMethodCode_OptionSet.Fax, text: 'Fax' },
38
+ { key: PreferredContactMethodCode_OptionSet.Mail, text: 'Mail' },
31
39
  ];
32
40
 
33
41
  export const ContactForm: React.FC<ContactFormProps> = ({
34
42
  contactId,
35
43
  initialData,
36
44
  onSave,
37
- onCancel
45
+ onCancel,
38
46
  }) => {
39
- const { createRecord, updateRecord } = useDynamicsApi();
40
-
47
+ const { apiService } = useDynamicsApi();
48
+
41
49
  const [formData, setFormData] = useState<ContactFormData>({
42
50
  firstname: '',
43
51
  lastname: '',
44
52
  emailaddress1: '',
45
53
  telephone1: '',
46
- preferredcontactmethodcode: 1,
47
- birthdate: null
54
+ preferredcontactmethodcode: PreferredContactMethodCode_OptionSet.Any,
55
+ birthdate: null,
48
56
  });
49
57
 
58
+ useEffect(() => {
59
+ Logger.userAction(
60
+ contactId
61
+ ? 'Contact form opened for edit'
62
+ : 'Contact form opened for new contact',
63
+ { contactId },
64
+ 'ContactForm'
65
+ );
66
+ }, [contactId]);
67
+
50
68
  const [errors, setErrors] = useState<Partial<ContactFormData>>({});
51
69
  const [isSubmitting, setIsSubmitting] = useState(false);
52
70
  const [submitError, setSubmitError] = useState<string>('');
53
71
 
54
72
  useEffect(() => {
55
73
  if (initialData) {
74
+ Logger.debug(
75
+ 'Populating form with initial data',
76
+ 'ContactForm.useEffect',
77
+ initialData
78
+ );
79
+
56
80
  setFormData({
57
81
  firstname: initialData.firstname || '',
58
82
  lastname: initialData.lastname || '',
59
83
  emailaddress1: initialData.emailaddress1 || '',
60
84
  telephone1: initialData.telephone1 || '',
61
- preferredcontactmethodcode: initialData.preferredcontactmethodcode || 1,
62
- birthdate: initialData.birthdate || null
85
+ preferredcontactmethodcode:
86
+ initialData.preferredcontactmethodcode ||
87
+ PreferredContactMethodCode_OptionSet.Any,
88
+ birthdate: initialData.birthdate || null,
63
89
  });
64
90
  }
65
91
  }, [initialData]);
66
92
 
67
93
  const validateForm = useCallback((): boolean => {
68
94
  const newErrors: Partial<ContactFormData> = {};
95
+ const validationErrors: string[] = [];
69
96
 
70
- if (!formData.firstname.trim()) {
71
- newErrors.firstname = 'First name is required';
97
+ // At least one name is required (following Contact model validation)
98
+ if (!formData.firstname.trim() && !formData.lastname.trim()) {
99
+ const error = 'Either first name or last name is required';
100
+ newErrors.firstname = error;
101
+ newErrors.lastname = error;
102
+ validationErrors.push(error);
72
103
  }
73
104
 
74
- if (!formData.lastname.trim()) {
75
- newErrors.lastname = 'Last name is required';
105
+ if (
106
+ formData.emailaddress1 &&
107
+ !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.emailaddress1)
108
+ ) {
109
+ const error = 'Please enter a valid email address';
110
+ newErrors.emailaddress1 = error;
111
+ validationErrors.push(error);
76
112
  }
77
113
 
78
- if (!formData.emailaddress1.trim()) {
79
- newErrors.emailaddress1 = 'Email address is required';
80
- } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.emailaddress1)) {
81
- newErrors.emailaddress1 = 'Please enter a valid email address';
114
+ if (formData.telephone1 && !/^\+?[\d\s\-()]+$/.test(formData.telephone1)) {
115
+ const error = 'Please enter a valid phone number';
116
+ newErrors.telephone1 = error;
117
+ validationErrors.push(error);
82
118
  }
83
119
 
84
- if (formData.telephone1 && !/^\+?[\d\s\-\(\)]+$/.test(formData.telephone1)) {
85
- newErrors.telephone1 = 'Please enter a valid phone number';
120
+ if (validationErrors.length > 0) {
121
+ Logger.validation(
122
+ 'Contact',
123
+ validationErrors,
124
+ 'ContactForm.validateForm'
125
+ );
86
126
  }
87
127
 
88
128
  setErrors(newErrors);
89
129
  return Object.keys(newErrors).length === 0;
90
130
  }, [formData]);
91
131
 
92
- const handleInputChange = useCallback((field: keyof ContactFormData, value: any) => {
93
- setFormData(prev => ({ ...prev, [field]: value }));
94
-
95
- // Clear error for this field when user starts typing
96
- if (errors[field]) {
97
- setErrors(prev => ({ ...prev, [field]: undefined }));
98
- }
99
- setSubmitError('');
100
- }, [errors]);
132
+ const handleInputChange = useCallback(
133
+ (field: keyof ContactFormData, value: any) => {
134
+ setFormData((prev) => ({ ...prev, [field]: value }));
135
+
136
+ // Clear error for this field when user starts typing
137
+ if (errors[field]) {
138
+ setErrors((prev) => ({ ...prev, [field]: undefined }));
139
+ }
140
+ setSubmitError('');
141
+ },
142
+ [errors]
143
+ );
101
144
 
102
145
  const handleSubmit = useCallback(async () => {
103
146
  if (!validateForm()) {
104
147
  return;
105
148
  }
106
149
 
150
+ if (!apiService) {
151
+ setSubmitError('API service not available');
152
+ Logger.error('API service not available', 'ContactForm.handleSubmit');
153
+ return;
154
+ }
155
+
107
156
  setIsSubmitting(true);
108
157
  setSubmitError('');
109
158
 
110
159
  try {
111
- const contactData = {
112
- firstname: formData.firstname.trim(),
113
- lastname: formData.lastname.trim(),
114
- emailaddress1: formData.emailaddress1.trim(),
115
- telephone1: formData.telephone1.trim() || null,
116
- preferredcontactmethodcode: formData.preferredcontactmethodcode,
117
- birthdate: formData.birthdate ? formData.birthdate.toISOString() : null
160
+ Logger.userAction(
161
+ contactId ? 'Contact update submitted' : 'Contact creation submitted',
162
+ { contactId, formData },
163
+ 'ContactForm.handleSubmit'
164
+ );
165
+
166
+ const contactData: IContact = {
167
+ [ContactConstants.FirstName]: formData.firstname.trim() || undefined,
168
+ [ContactConstants.LastName]: formData.lastname.trim() || undefined,
169
+ [ContactConstants.EMailAddress1]:
170
+ formData.emailaddress1.trim() || undefined,
171
+ [ContactConstants.BusinessPhone]:
172
+ formData.telephone1.trim() || undefined,
173
+ [ContactConstants.PreferredContactMethodCode]:
174
+ formData.preferredcontactmethodcode,
175
+ [ContactConstants.BirthDate]: formData.birthdate
176
+ ? formData.birthdate.toISOString().split('T')[0]
177
+ : undefined,
118
178
  };
119
179
 
180
+ // Remove undefined values
181
+ Object.keys(contactData).forEach((key) => {
182
+ if (contactData[key as keyof IContact] === undefined) {
183
+ delete contactData[key as keyof IContact];
184
+ }
185
+ });
186
+
187
+ const contact = new Contact(contactData);
188
+
120
189
  if (contactId) {
121
- await updateRecord('contacts', contactId, contactData);
190
+ contact.contactid = contactId;
191
+ await Contact.update(apiService, contact);
192
+ Logger.log(
193
+ `Successfully updated contact: ${contact.firstname} ${contact.lastname}`,
194
+ 'ContactForm.handleSubmit'
195
+ );
122
196
  } else {
123
- await createRecord('contacts', contactData);
197
+ await Contact.create(apiService, contact);
198
+ Logger.log(
199
+ `Successfully created contact: ${contact.firstname} ${contact.lastname}`,
200
+ 'ContactForm.handleSubmit'
201
+ );
124
202
  }
125
203
 
126
204
  if (onSave) {
127
205
  onSave();
128
206
  }
129
207
  } catch (error) {
130
- setSubmitError(error instanceof Error ? error.message : 'An error occurred while saving');
208
+ const errorMessage =
209
+ error instanceof Error
210
+ ? error.message
211
+ : 'An error occurred while saving';
212
+ setSubmitError(errorMessage);
213
+ Logger.error(
214
+ `Failed to ${contactId ? 'update' : 'create'} contact`,
215
+ 'ContactForm.handleSubmit',
216
+ error
217
+ );
131
218
  } finally {
132
219
  setIsSubmitting(false);
133
220
  }
134
- }, [
135
- formData,
136
- contactId,
137
- validateForm,
138
- createRecord,
139
- updateRecord,
140
- onSave
141
- ]);
221
+ }, [formData, contactId, validateForm, apiService, onSave]);
142
222
 
143
223
  const handleCancel = useCallback(() => {
144
224
  if (onCancel) {
@@ -161,7 +241,9 @@ export const ContactForm: React.FC<ContactFormProps> = ({
161
241
  label="First Name"
162
242
  required
163
243
  value={formData.firstname}
164
- onChange={(_, value) => handleInputChange('firstname', value || '')}
244
+ onChange={(_, value) =>
245
+ handleInputChange('firstname', value || '')
246
+ }
165
247
  errorMessage={errors.firstname}
166
248
  disabled={isSubmitting}
167
249
  />
@@ -171,7 +253,9 @@ export const ContactForm: React.FC<ContactFormProps> = ({
171
253
  label="Last Name"
172
254
  required
173
255
  value={formData.lastname}
174
- onChange={(_, value) => handleInputChange('lastname', value || '')}
256
+ onChange={(_, value) =>
257
+ handleInputChange('lastname', value || '')
258
+ }
175
259
  errorMessage={errors.lastname}
176
260
  disabled={isSubmitting}
177
261
  />
@@ -185,7 +269,9 @@ export const ContactForm: React.FC<ContactFormProps> = ({
185
269
  type="email"
186
270
  required
187
271
  value={formData.emailaddress1}
188
- onChange={(_, value) => handleInputChange('emailaddress1', value || '')}
272
+ onChange={(_, value) =>
273
+ handleInputChange('emailaddress1', value || '')
274
+ }
189
275
  errorMessage={errors.emailaddress1}
190
276
  disabled={isSubmitting}
191
277
  />
@@ -195,7 +281,9 @@ export const ContactForm: React.FC<ContactFormProps> = ({
195
281
  label="Phone Number"
196
282
  type="tel"
197
283
  value={formData.telephone1}
198
- onChange={(_, value) => handleInputChange('telephone1', value || '')}
284
+ onChange={(_, value) =>
285
+ handleInputChange('telephone1', value || '')
286
+ }
199
287
  errorMessage={errors.telephone1}
200
288
  disabled={isSubmitting}
201
289
  />
@@ -208,7 +296,9 @@ export const ContactForm: React.FC<ContactFormProps> = ({
208
296
  label="Preferred Contact Method"
209
297
  options={preferredContactOptions}
210
298
  selectedKey={formData.preferredcontactmethodcode}
211
- onChange={(_, option) => handleInputChange('preferredcontactmethodcode', option?.key)}
299
+ onChange={(_, option) =>
300
+ handleInputChange('preferredcontactmethodcode', option?.key)
301
+ }
212
302
  disabled={isSubmitting}
213
303
  />
214
304
  </div>
@@ -230,12 +320,8 @@ export const ContactForm: React.FC<ContactFormProps> = ({
230
320
  onClick={handleSubmit}
231
321
  disabled={isSubmitting}
232
322
  />
233
- <Button
234
- text="Cancel"
235
- onClick={handleCancel}
236
- disabled={isSubmitting}
237
- />
323
+ <Button text="Cancel" onClick={handleCancel} disabled={isSubmitting} />
238
324
  </div>
239
325
  </div>
240
326
  );
241
- };
327
+ };