@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.
- package/bin/create-dynamics-app.js +1 -1
- package/dist/index.js +140 -15
- package/dist/index.js.map +1 -1
- package/dist/utils/consultingHelpers.d.ts +13 -0
- package/dist/utils/consultingHelpers.d.ts.map +1 -0
- package/dist/utils/consultingHelpers.js +569 -0
- package/dist/utils/consultingHelpers.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js.map +1 -1
- package/dist/utils/initGit.d.ts.map +1 -1
- package/dist/utils/initGit.js.map +1 -1
- package/dist/utils/installDependencies.d.ts.map +1 -1
- package/dist/utils/installDependencies.js +3 -2
- package/dist/utils/installDependencies.js.map +1 -1
- package/dist/utils/updatePackageJson.d.ts +1 -1
- package/dist/utils/updatePackageJson.d.ts.map +1 -1
- package/dist/utils/updatePackageJson.js +11 -1
- package/dist/utils/updatePackageJson.js.map +1 -1
- package/package.json +1 -1
- package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
- package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
- package/templates/dynamics-365-starter/README.md +566 -137
- package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
- package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
- package/templates/dynamics-365-starter/deployment/README.md +484 -0
- package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
- package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
- package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
- package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
- package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
- package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
- package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
- package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
- package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
- package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
- package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
- package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
- package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
- package/templates/dynamics-365-starter/package.json +22 -1
- package/templates/dynamics-365-starter/public/index.html +8 -11
- package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
- package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
- package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
- package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
- package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
- package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
- package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
- package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
- package/templates/dynamics-365-starter/src/examples/README.md +52 -0
- package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
- package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
- package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
- package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
- package/templates/dynamics-365-starter/src/index.tsx +107 -19
- package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
- package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
- package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
- package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
- package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
- package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
- package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
- package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
- package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
- package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
- package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
- package/templates/dynamics-365-starter/src/styles/index.css +74 -7
- package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
- package/templates/dynamics-365-starter/tsconfig.json +11 -8
- package/templates/dynamics-365-starter/webpack.config.js +8 -9
- package/templates/power-pages-starter/README.md +7 -1
- package/templates/power-pages-starter/public/index.html +8 -11
- package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
- package/templates/power-pages-starter/src/index.tsx +3 -3
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
- package/templates/power-pages-starter/tsconfig.json +3 -9
- package/templates/power-pages-starter/webpack.config.js +8 -3
package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Creating a custom management component
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to create a management component
|
|
5
|
+
* for a custom entity following the established patterns from
|
|
6
|
+
* AccountManagement and ContactManagement.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Usage in your application
|
|
11
|
+
* import { OpportunityManagement } from './examples/component-examples/opportunity-management';
|
|
12
|
+
*
|
|
13
|
+
* function App() {
|
|
14
|
+
* return (
|
|
15
|
+
* <DynamicsProvider>
|
|
16
|
+
* <OpportunityManagement />
|
|
17
|
+
* </DynamicsProvider>
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
24
|
+
import {
|
|
25
|
+
Button,
|
|
26
|
+
TextField,
|
|
27
|
+
DetailsList,
|
|
28
|
+
Dialog,
|
|
29
|
+
Panel,
|
|
30
|
+
Dropdown,
|
|
31
|
+
DynamicsColumn,
|
|
32
|
+
} from '@khester/dynamics-ui-components';
|
|
33
|
+
import {
|
|
34
|
+
SelectionMode,
|
|
35
|
+
DetailsListLayoutMode,
|
|
36
|
+
CheckboxVisibility,
|
|
37
|
+
} from '@fluentui/react';
|
|
38
|
+
import { useDynamicsApi } from '../../providers/DynamicsProvider';
|
|
39
|
+
import { Logger } from '../../components/Logging/logger';
|
|
40
|
+
import {
|
|
41
|
+
Opportunity,
|
|
42
|
+
OpportunityConstants,
|
|
43
|
+
SalesStageCode_OptionSet,
|
|
44
|
+
} from '../entity-examples/opportunity-model';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Opportunity Management Component Props
|
|
48
|
+
*/
|
|
49
|
+
interface OpportunityManagementProps {
|
|
50
|
+
/** Optional account ID to filter opportunities */
|
|
51
|
+
accountId?: string;
|
|
52
|
+
/** Whether to show the create button */
|
|
53
|
+
showCreateButton?: boolean;
|
|
54
|
+
/** Custom title for the component */
|
|
55
|
+
title?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Column configuration for the opportunities list
|
|
60
|
+
* Following the same pattern as AccountManagement and ContactManagement
|
|
61
|
+
*/
|
|
62
|
+
const columns: DynamicsColumn[] = [
|
|
63
|
+
{
|
|
64
|
+
key: OpportunityConstants.PrimaryName,
|
|
65
|
+
name: 'Opportunity Name',
|
|
66
|
+
fieldName: OpportunityConstants.PrimaryName,
|
|
67
|
+
minWidth: 200,
|
|
68
|
+
maxWidth: 300,
|
|
69
|
+
isResizable: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: OpportunityConstants.EstimatedValue,
|
|
73
|
+
name: 'Est. Value',
|
|
74
|
+
fieldName: OpportunityConstants.EstimatedValue,
|
|
75
|
+
minWidth: 120,
|
|
76
|
+
maxWidth: 150,
|
|
77
|
+
isResizable: true,
|
|
78
|
+
onRender: (item: Opportunity) => {
|
|
79
|
+
return item.estimatedvalue
|
|
80
|
+
? `$${item.estimatedvalue.toLocaleString()}`
|
|
81
|
+
: '';
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: OpportunityConstants.CloseProbability,
|
|
86
|
+
name: 'Close %',
|
|
87
|
+
fieldName: OpportunityConstants.CloseProbability,
|
|
88
|
+
minWidth: 80,
|
|
89
|
+
maxWidth: 100,
|
|
90
|
+
isResizable: true,
|
|
91
|
+
onRender: (item: Opportunity) => {
|
|
92
|
+
return item.closeprobability ? `${item.closeprobability}%` : '';
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: OpportunityConstants.EstimatedCloseDate,
|
|
97
|
+
name: 'Close Date',
|
|
98
|
+
fieldName: OpportunityConstants.EstimatedCloseDate,
|
|
99
|
+
minWidth: 120,
|
|
100
|
+
maxWidth: 150,
|
|
101
|
+
isResizable: true,
|
|
102
|
+
onRender: (item: Opportunity) => {
|
|
103
|
+
return item.estimatedclosedate
|
|
104
|
+
? new Date(item.estimatedclosedate).toLocaleDateString()
|
|
105
|
+
: '';
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: OpportunityConstants.SalesStageCode,
|
|
110
|
+
name: 'Stage',
|
|
111
|
+
fieldName: OpportunityConstants.SalesStageCode,
|
|
112
|
+
minWidth: 100,
|
|
113
|
+
maxWidth: 120,
|
|
114
|
+
isResizable: true,
|
|
115
|
+
onRender: (item: Opportunity) => {
|
|
116
|
+
const stageMap: Record<number, string> = {
|
|
117
|
+
[SalesStageCode_OptionSet.Qualify]: 'Qualify',
|
|
118
|
+
[SalesStageCode_OptionSet.Develop]: 'Develop',
|
|
119
|
+
[SalesStageCode_OptionSet.Propose]: 'Propose',
|
|
120
|
+
[SalesStageCode_OptionSet.Close]: 'Close',
|
|
121
|
+
};
|
|
122
|
+
return item.salesstagecode !== undefined
|
|
123
|
+
? stageMap[item.salesstagecode] || 'Unknown'
|
|
124
|
+
: '';
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: OpportunityConstants.CreatedOn,
|
|
129
|
+
name: 'Created',
|
|
130
|
+
fieldName: OpportunityConstants.CreatedOn,
|
|
131
|
+
minWidth: 120,
|
|
132
|
+
maxWidth: 150,
|
|
133
|
+
isResizable: true,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Sales stage filter options
|
|
139
|
+
*/
|
|
140
|
+
const salesStageOptions = [
|
|
141
|
+
{ key: 'all', text: 'All Stages' },
|
|
142
|
+
{ key: SalesStageCode_OptionSet.Qualify, text: 'Qualify' },
|
|
143
|
+
{ key: SalesStageCode_OptionSet.Develop, text: 'Develop' },
|
|
144
|
+
{ key: SalesStageCode_OptionSet.Propose, text: 'Propose' },
|
|
145
|
+
{ key: SalesStageCode_OptionSet.Close, text: 'Close' },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* OpportunityManagement Component
|
|
150
|
+
*
|
|
151
|
+
* Provides complete CRUD interface for opportunities with:
|
|
152
|
+
* - List view with sorting and filtering
|
|
153
|
+
* - Search functionality
|
|
154
|
+
* - Create, edit, and delete operations
|
|
155
|
+
* - Sales stage filtering
|
|
156
|
+
* - Integration with logging system
|
|
157
|
+
*
|
|
158
|
+
* @param props - Component properties
|
|
159
|
+
* @returns JSX.Element
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* // Basic usage
|
|
164
|
+
* <OpportunityManagement />
|
|
165
|
+
*
|
|
166
|
+
* // With account filter
|
|
167
|
+
* <OpportunityManagement
|
|
168
|
+
* accountId="account-guid-here"
|
|
169
|
+
* title="Account Opportunities"
|
|
170
|
+
* />
|
|
171
|
+
*
|
|
172
|
+
* // Read-only view
|
|
173
|
+
* <OpportunityManagement
|
|
174
|
+
* showCreateButton={false}
|
|
175
|
+
* />
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
export const OpportunityManagement: React.FC<OpportunityManagementProps> = ({
|
|
179
|
+
accountId,
|
|
180
|
+
showCreateButton = true,
|
|
181
|
+
title = 'Opportunity Management',
|
|
182
|
+
}) => {
|
|
183
|
+
const { apiService, isEnvironmentMock, environmentType } = useDynamicsApi();
|
|
184
|
+
|
|
185
|
+
// State management
|
|
186
|
+
const [opportunities, setOpportunities] = useState<Opportunity[]>([]);
|
|
187
|
+
const [filteredOpportunities, setFilteredOpportunities] = useState<
|
|
188
|
+
Opportunity[]
|
|
189
|
+
>([]);
|
|
190
|
+
const [loading, setLoading] = useState(false);
|
|
191
|
+
const [searchText, setSearchText] = useState('');
|
|
192
|
+
const [selectedOpportunity, setSelectedOpportunity] =
|
|
193
|
+
useState<Opportunity | null>(null);
|
|
194
|
+
const [salesStageFilter, setSalesStageFilter] = useState<string>('all');
|
|
195
|
+
|
|
196
|
+
// Panel and dialog state
|
|
197
|
+
const [showNewOpportunityPanel, setShowNewOpportunityPanel] = useState(false);
|
|
198
|
+
const [showEditOpportunityPanel, setShowEditOpportunityPanel] =
|
|
199
|
+
useState(false);
|
|
200
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
201
|
+
const [opportunityToDelete, setOpportunityToDelete] =
|
|
202
|
+
useState<Opportunity | null>(null);
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Log component initialization
|
|
206
|
+
*/
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
Logger.userAction(
|
|
209
|
+
'OpportunityManagement loaded',
|
|
210
|
+
{ environmentType, isEnvironmentMock, accountId },
|
|
211
|
+
'OpportunityManagement'
|
|
212
|
+
);
|
|
213
|
+
}, [environmentType, isEnvironmentMock, accountId]);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Load opportunities from the API
|
|
217
|
+
* Handles both general and account-specific loading
|
|
218
|
+
*/
|
|
219
|
+
const loadOpportunities = useCallback(async () => {
|
|
220
|
+
if (!apiService) {
|
|
221
|
+
Logger.error(
|
|
222
|
+
'API service not available',
|
|
223
|
+
'OpportunityManagement.loadOpportunities'
|
|
224
|
+
);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setLoading(true);
|
|
229
|
+
Logger.log(
|
|
230
|
+
'Loading opportunities',
|
|
231
|
+
'OpportunityManagement.loadOpportunities'
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
let opportunitiesData: Opportunity[];
|
|
236
|
+
|
|
237
|
+
if (accountId) {
|
|
238
|
+
// Load opportunities for specific account
|
|
239
|
+
opportunitiesData = await Opportunity.retrieveByAccount(
|
|
240
|
+
apiService,
|
|
241
|
+
accountId
|
|
242
|
+
);
|
|
243
|
+
} else {
|
|
244
|
+
// Load all active opportunities
|
|
245
|
+
opportunitiesData =
|
|
246
|
+
await Opportunity.retrieveActiveOpportunities(apiService);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Process opportunities for display
|
|
250
|
+
const opportunitiesWithDisplayData = opportunitiesData.map(
|
|
251
|
+
(opportunity) => {
|
|
252
|
+
const opportunityInstance = new Opportunity(opportunity);
|
|
253
|
+
return {
|
|
254
|
+
...opportunityInstance,
|
|
255
|
+
createdon: opportunity.createdon
|
|
256
|
+
? new Date(opportunity.createdon).toLocaleDateString()
|
|
257
|
+
: '',
|
|
258
|
+
} as Opportunity;
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
setOpportunities(opportunitiesWithDisplayData);
|
|
263
|
+
setFilteredOpportunities(opportunitiesWithDisplayData);
|
|
264
|
+
|
|
265
|
+
Logger.log(
|
|
266
|
+
`Successfully loaded ${opportunitiesWithDisplayData.length} opportunities`,
|
|
267
|
+
'OpportunityManagement.loadOpportunities'
|
|
268
|
+
);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
Logger.error(
|
|
271
|
+
'Failed to load opportunities',
|
|
272
|
+
'OpportunityManagement.loadOpportunities',
|
|
273
|
+
error
|
|
274
|
+
);
|
|
275
|
+
} finally {
|
|
276
|
+
setLoading(false);
|
|
277
|
+
}
|
|
278
|
+
}, [apiService, accountId]);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Initial data load
|
|
282
|
+
*/
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
loadOpportunities();
|
|
285
|
+
}, [loadOpportunities]);
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Filter opportunities based on search text and sales stage
|
|
289
|
+
*/
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
let filtered = opportunities;
|
|
292
|
+
|
|
293
|
+
// Apply search filter
|
|
294
|
+
if (searchText) {
|
|
295
|
+
filtered = filtered.filter(
|
|
296
|
+
(opportunity) =>
|
|
297
|
+
opportunity.name?.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
298
|
+
opportunity.description
|
|
299
|
+
?.toLowerCase()
|
|
300
|
+
.includes(searchText.toLowerCase())
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Apply sales stage filter
|
|
305
|
+
if (salesStageFilter !== 'all') {
|
|
306
|
+
const stageValue = parseInt(salesStageFilter);
|
|
307
|
+
filtered = filtered.filter(
|
|
308
|
+
(opportunity) => opportunity.salesstagecode === stageValue
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
setFilteredOpportunities(filtered);
|
|
313
|
+
}, [opportunities, searchText, salesStageFilter]);
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle opportunity item selection/invocation
|
|
317
|
+
*/
|
|
318
|
+
const handleItemInvoked = useCallback(
|
|
319
|
+
(item: Opportunity) => {
|
|
320
|
+
Logger.userAction(
|
|
321
|
+
'Opportunity item invoked',
|
|
322
|
+
{ opportunityId: item.opportunityid, opportunityName: item.name },
|
|
323
|
+
'OpportunityManagement.handleItemInvoked'
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (selectedOpportunity?.opportunityid === item.opportunityid) {
|
|
327
|
+
setShowEditOpportunityPanel(true);
|
|
328
|
+
} else {
|
|
329
|
+
setSelectedOpportunity(item);
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
[selectedOpportunity]
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Handle new opportunity creation
|
|
337
|
+
*/
|
|
338
|
+
const handleNewOpportunity = useCallback(() => {
|
|
339
|
+
Logger.userAction(
|
|
340
|
+
'New opportunity panel opened',
|
|
341
|
+
{},
|
|
342
|
+
'OpportunityManagement.handleNewOpportunity'
|
|
343
|
+
);
|
|
344
|
+
setShowNewOpportunityPanel(true);
|
|
345
|
+
}, []);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Handle opportunity editing
|
|
349
|
+
*/
|
|
350
|
+
const handleEditOpportunity = useCallback(() => {
|
|
351
|
+
if (selectedOpportunity) {
|
|
352
|
+
Logger.userAction(
|
|
353
|
+
'Edit opportunity panel opened',
|
|
354
|
+
{
|
|
355
|
+
opportunityId: selectedOpportunity.opportunityid,
|
|
356
|
+
opportunityName: selectedOpportunity.name,
|
|
357
|
+
},
|
|
358
|
+
'OpportunityManagement.handleEditOpportunity'
|
|
359
|
+
);
|
|
360
|
+
setShowEditOpportunityPanel(true);
|
|
361
|
+
}
|
|
362
|
+
}, [selectedOpportunity]);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Handle opportunity deletion
|
|
366
|
+
*/
|
|
367
|
+
const handleDeleteOpportunity = useCallback(() => {
|
|
368
|
+
if (selectedOpportunity) {
|
|
369
|
+
Logger.userAction(
|
|
370
|
+
'Delete opportunity dialog opened',
|
|
371
|
+
{
|
|
372
|
+
opportunityId: selectedOpportunity.opportunityid,
|
|
373
|
+
opportunityName: selectedOpportunity.name,
|
|
374
|
+
},
|
|
375
|
+
'OpportunityManagement.handleDeleteOpportunity'
|
|
376
|
+
);
|
|
377
|
+
setOpportunityToDelete(selectedOpportunity);
|
|
378
|
+
setShowDeleteDialog(true);
|
|
379
|
+
}
|
|
380
|
+
}, [selectedOpportunity]);
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Confirm opportunity deletion
|
|
384
|
+
*/
|
|
385
|
+
const confirmDelete = useCallback(async () => {
|
|
386
|
+
if (opportunityToDelete && apiService) {
|
|
387
|
+
try {
|
|
388
|
+
Logger.userAction(
|
|
389
|
+
'Opportunity deletion confirmed',
|
|
390
|
+
{
|
|
391
|
+
opportunityId: opportunityToDelete.opportunityid,
|
|
392
|
+
opportunityName: opportunityToDelete.name,
|
|
393
|
+
},
|
|
394
|
+
'OpportunityManagement.confirmDelete'
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
await Opportunity.delete(
|
|
398
|
+
apiService,
|
|
399
|
+
opportunityToDelete.opportunityid!
|
|
400
|
+
);
|
|
401
|
+
await loadOpportunities();
|
|
402
|
+
|
|
403
|
+
setShowDeleteDialog(false);
|
|
404
|
+
setOpportunityToDelete(null);
|
|
405
|
+
setSelectedOpportunity(null);
|
|
406
|
+
|
|
407
|
+
Logger.log(
|
|
408
|
+
`Successfully deleted opportunity: ${opportunityToDelete.name}`,
|
|
409
|
+
'OpportunityManagement.confirmDelete'
|
|
410
|
+
);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
Logger.error(
|
|
413
|
+
`Failed to delete opportunity: ${opportunityToDelete.name}`,
|
|
414
|
+
'OpportunityManagement.confirmDelete',
|
|
415
|
+
error
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}, [opportunityToDelete, apiService, loadOpportunities]);
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Handle opportunity save (create or update)
|
|
423
|
+
*/
|
|
424
|
+
const handleOpportunitySaved = useCallback(() => {
|
|
425
|
+
Logger.userAction(
|
|
426
|
+
'Opportunity saved, refreshing list',
|
|
427
|
+
{},
|
|
428
|
+
'OpportunityManagement.handleOpportunitySaved'
|
|
429
|
+
);
|
|
430
|
+
loadOpportunities();
|
|
431
|
+
setShowNewOpportunityPanel(false);
|
|
432
|
+
setShowEditOpportunityPanel(false);
|
|
433
|
+
setSelectedOpportunity(null);
|
|
434
|
+
}, [loadOpportunities]);
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Handle sales stage filter change
|
|
438
|
+
*/
|
|
439
|
+
const handleSalesStageFilterChange = useCallback(
|
|
440
|
+
(
|
|
441
|
+
_event: React.FormEvent<HTMLDivElement>,
|
|
442
|
+
option?: { key: string | number | undefined; text: string }
|
|
443
|
+
) => {
|
|
444
|
+
if (option) {
|
|
445
|
+
const newStage = option.key?.toString() || 'all';
|
|
446
|
+
Logger.userAction(
|
|
447
|
+
'Sales stage filter changed',
|
|
448
|
+
{ from: salesStageFilter, to: newStage },
|
|
449
|
+
'OpportunityManagement.handleSalesStageFilterChange'
|
|
450
|
+
);
|
|
451
|
+
setSalesStageFilter(newStage);
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
[salesStageFilter]
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<div className="opportunity-management">
|
|
459
|
+
<div className="opportunity-management__header">
|
|
460
|
+
<h2>{title}</h2>
|
|
461
|
+
<div className="opportunity-management__actions">
|
|
462
|
+
<TextField
|
|
463
|
+
placeholder="Search opportunities..."
|
|
464
|
+
value={searchText}
|
|
465
|
+
onChange={(_, value) => setSearchText(value || '')}
|
|
466
|
+
iconProps={{ iconName: 'Search' }}
|
|
467
|
+
/>
|
|
468
|
+
<Dropdown
|
|
469
|
+
placeholder="Filter by stage"
|
|
470
|
+
options={salesStageOptions}
|
|
471
|
+
selectedKey={salesStageFilter}
|
|
472
|
+
onChange={handleSalesStageFilterChange}
|
|
473
|
+
/>
|
|
474
|
+
{showCreateButton && (
|
|
475
|
+
<Button
|
|
476
|
+
text="New Opportunity"
|
|
477
|
+
variant="primary"
|
|
478
|
+
onClick={handleNewOpportunity}
|
|
479
|
+
iconProps={{ iconName: 'Add' }}
|
|
480
|
+
/>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<div className="opportunity-management__toolbar">
|
|
486
|
+
<Button
|
|
487
|
+
text="Edit"
|
|
488
|
+
onClick={handleEditOpportunity}
|
|
489
|
+
disabled={!selectedOpportunity}
|
|
490
|
+
iconProps={{ iconName: 'Edit' }}
|
|
491
|
+
/>
|
|
492
|
+
<Button
|
|
493
|
+
text="Delete"
|
|
494
|
+
onClick={handleDeleteOpportunity}
|
|
495
|
+
disabled={!selectedOpportunity}
|
|
496
|
+
iconProps={{ iconName: 'Delete' }}
|
|
497
|
+
/>
|
|
498
|
+
<Button
|
|
499
|
+
text="Refresh"
|
|
500
|
+
onClick={loadOpportunities}
|
|
501
|
+
iconProps={{ iconName: 'Refresh' }}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<div className="opportunity-management__list">
|
|
506
|
+
<DetailsList
|
|
507
|
+
items={filteredOpportunities}
|
|
508
|
+
columns={columns}
|
|
509
|
+
onItemInvoked={handleItemInvoked}
|
|
510
|
+
selectionMode={SelectionMode.single}
|
|
511
|
+
layoutMode={DetailsListLayoutMode.justified}
|
|
512
|
+
checkboxVisibility={CheckboxVisibility.onHover}
|
|
513
|
+
/>
|
|
514
|
+
|
|
515
|
+
{loading && (
|
|
516
|
+
<div className="opportunity-management__loading">
|
|
517
|
+
Loading opportunities...
|
|
518
|
+
</div>
|
|
519
|
+
)}
|
|
520
|
+
|
|
521
|
+
{!loading && filteredOpportunities.length === 0 && (
|
|
522
|
+
<div className="opportunity-management__empty">
|
|
523
|
+
{searchText || salesStageFilter !== 'all'
|
|
524
|
+
? 'No opportunities found matching your filters.'
|
|
525
|
+
: accountId
|
|
526
|
+
? 'No opportunities found for this account. Create your first opportunity!'
|
|
527
|
+
: 'No opportunities found. Create your first opportunity!'}
|
|
528
|
+
</div>
|
|
529
|
+
)}
|
|
530
|
+
</div>
|
|
531
|
+
|
|
532
|
+
{/* Delete Confirmation Dialog */}
|
|
533
|
+
<Dialog
|
|
534
|
+
hidden={!showDeleteDialog}
|
|
535
|
+
onDismiss={() => setShowDeleteDialog(false)}
|
|
536
|
+
title="Confirm Delete"
|
|
537
|
+
content={`Are you sure you want to delete "${opportunityToDelete?.name}"? This action cannot be undone.`}
|
|
538
|
+
actions={[
|
|
539
|
+
{ text: 'Delete', onClick: confirmDelete, primary: true },
|
|
540
|
+
{ text: 'Cancel', onClick: () => setShowDeleteDialog(false) },
|
|
541
|
+
]}
|
|
542
|
+
/>
|
|
543
|
+
|
|
544
|
+
{/* Note: OpportunityForm would need to be created following the AccountForm/ContactForm pattern */}
|
|
545
|
+
{/* This example focuses on the management component structure */}
|
|
546
|
+
</div>
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* CSS classes for styling (would be in a separate .css file)
|
|
552
|
+
*/
|
|
553
|
+
export const opportunityManagementStyles = `
|
|
554
|
+
.opportunity-management {
|
|
555
|
+
padding: 16px;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.opportunity-management__header {
|
|
559
|
+
display: flex;
|
|
560
|
+
justify-content: space-between;
|
|
561
|
+
align-items: center;
|
|
562
|
+
margin-bottom: 16px;
|
|
563
|
+
flex-wrap: wrap;
|
|
564
|
+
gap: 16px;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.opportunity-management__header h2 {
|
|
568
|
+
margin: 0;
|
|
569
|
+
font-size: 20px;
|
|
570
|
+
font-weight: 600;
|
|
571
|
+
color: #323130;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.opportunity-management__actions {
|
|
575
|
+
display: flex;
|
|
576
|
+
gap: 12px;
|
|
577
|
+
align-items: center;
|
|
578
|
+
flex-wrap: wrap;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.opportunity-management__toolbar {
|
|
582
|
+
display: flex;
|
|
583
|
+
gap: 8px;
|
|
584
|
+
margin-bottom: 16px;
|
|
585
|
+
padding: 8px 0;
|
|
586
|
+
border-bottom: 1px solid #edebe9;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.opportunity-management__list {
|
|
590
|
+
min-height: 400px;
|
|
591
|
+
border: 1px solid #edebe9;
|
|
592
|
+
border-radius: 4px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.opportunity-management__loading,
|
|
596
|
+
.opportunity-management__empty {
|
|
597
|
+
display: flex;
|
|
598
|
+
justify-content: center;
|
|
599
|
+
align-items: center;
|
|
600
|
+
height: 200px;
|
|
601
|
+
color: #605e5c;
|
|
602
|
+
font-style: italic;
|
|
603
|
+
text-align: center;
|
|
604
|
+
padding: 16px;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
@media (max-width: 768px) {
|
|
608
|
+
.opportunity-management {
|
|
609
|
+
padding: 8px;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.opportunity-management__header {
|
|
613
|
+
flex-direction: column;
|
|
614
|
+
align-items: stretch;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.opportunity-management__actions {
|
|
618
|
+
justify-content: stretch;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.opportunity-management__toolbar {
|
|
622
|
+
flex-wrap: wrap;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
`;
|