diff --git a/src/classes/OpenOpportunitiesBatch.cls b/src/classes/OpenOpportunitiesBatch.cls new file mode 100644 index 00000000..c1507241 --- /dev/null +++ b/src/classes/OpenOpportunitiesBatch.cls @@ -0,0 +1,40 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +global class OpenOpportunitiesBatch implements Database.Batchable { + + public OpenOpportunitiesBatch(){} + + global Iterable start(Database.BatchableContext bc) { + + Opportunity [] opportunities = new Opportunity [] {}; + + opportunities = [SELECT + Id, + OwnerId + FROM Opportunity + WHERE Isclosed = false]; + + Set usersId = new Set(); + + for(Opportunity opp :opportunities) { + usersId.add(opp.OwnerId); + } + + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + return users; + } + + global void execute(Database.BatchableContext bc, User[] scope) { + + User user = scope[0]; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(user.Id); + + OpenOpportunityMailer.sendOpenOpportunitiesBatchReport(user, new String[]{user.Email}, stagedOpportunities); + } + + global void finish(Database.BatchableContext bc){} +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesBatch.cls-meta.xml b/src/classes/OpenOpportunitiesBatch.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesBatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls new file mode 100644 index 00000000..073cf441 --- /dev/null +++ b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls @@ -0,0 +1,16 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/22/2012 + * + */ +global class OpenOpportunitiesNeedUpdateScheduler implements Schedulable { + + public OpenOpportunitiesNeedUpdateScheduler() {} + + global void execute(SchedulableContext sc) { + + Database.executeBatch(new OpenOpportunityNeedUpdateBatch(), 1); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesNeedUpdateScheduler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunitiesScheduler.cls b/src/classes/OpenOpportunitiesScheduler.cls new file mode 100644 index 00000000..28f3101e --- /dev/null +++ b/src/classes/OpenOpportunitiesScheduler.cls @@ -0,0 +1,16 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +global class OpenOpportunitiesScheduler implements Schedulable { + + public OpenOpportunitiesScheduler() {} + + global void execute(SchedulableContext sc) { + + Database.executeBatch(new OpenOpportunitiesBatch(), 1); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunitiesScheduler.cls-meta.xml b/src/classes/OpenOpportunitiesScheduler.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunitiesScheduler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityEmailUtils.cls b/src/classes/OpenOpportunityEmailUtils.cls new file mode 100644 index 00000000..e8995939 --- /dev/null +++ b/src/classes/OpenOpportunityEmailUtils.cls @@ -0,0 +1,274 @@ +/************************************************** +Class Name: OpenOpportunityEmailUtils +Class Description: Utiliy class which creates the HTML content to be displayed on the email / VF page. +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando +Update Date: 2013-03-04 +Additional Comments: This class has comments on the code in order to help future changes +**************************************************/ +public with sharing class OpenOpportunityEmailUtils { + + + private static String[] earlyStages = new String[] {'Stage 1 - Connect','Stage 2 - Talking','Stage 5 - Submitted'}; + + + /************************************************** + Comments: Static variables that represents the diferent containers for the email / VF page HTML components + **************************************************/ + private static String ENVELOPE = '
[TITLE][CONTAINER]
'; + private static String TITLE = '

Open Opportunities

'; + private static String SUB_TITLE = ''; + private static String CONTAINER = '
[SUB_CONTAINER]
'; + + private static final String FOGBUGZ_LINK = 'http://manage.dimagi.com/default.asp?'; + + /************************************************** + Method Name: buildEmailContent + Method Comments: Method call from the Weekly / Daily Schedule flow to build the HTML Content + **************************************************/ + public static String buildEmailContent(Map stagedOpportunities, Boolean hasComments, Map stageComments) { + + String result = ENVELOPE; + String content = ''; + + List sortedStages = new List(stagedOpportunities.keySet()); + sortedStages.sort(); + + for(String stageName :sortedStages) { + + String stageTable = '' + SUB_TITLE.replace('[SUB_TITLE]', stageName); + + /************************************************** + Comments: for each stage we call buildEmailStageTable + **************************************************/ + stageTable += ''; + + /************************************************** + Comments: for each stage we call getEmailStageComments (if there are no comments, the input text is generated anyway) + **************************************************/ + stageTable += ''; + + stageTable += '
' + buildEmailStageTable(stageName, stagedOpportunities.get(stageName)) + '
' + getEmailStageComments(stageComments.get(stageName)) + '
'; + stageTable = stageTable.replace('null', ''); + content += stageTable; + } + + result = result.replace('[TITLE]', TITLE); + return result.replace('[CONTAINER]', CONTAINER.replace('[SUB_CONTAINER]', content)); + } + + /************************************************** + Method Name: buildEmailStageTable + Method Comments: Method which returns the content of a HTML table for a Stage + **************************************************/ + public static String buildEmailStageTable(String stageName, Opportunity[] opportunities) { + + String result = '[THEADER][TBODY]
'; + result = result.replace('[THEADER]', getEmailStageTableHeader()); + + String tbody = ''; + + Integer daysNotUpdatedLimit = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c.intValue() + : 30; + + Integer daysNotUpdatedLimitEarlyStages = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c.intValue() + : 10; + + Set earlyStagesSet = new Set(earlyStages); + + for(Opportunity opp :opportunities) { + tbody += getEmailStageTableRow(opp, daysNotUpdatedLimit, daysNotUpdatedLimitEarlyStages, earlyStagesSet); + } + result = result.replace('[TBODY]', tbody).replace('null', ''); + return result; + } + + /************************************************** + Method Name: getEmailStageTableHeader + Method Comments: Returns the header of the stage tables, depending on which columns where selected + **************************************************/ + private static String getEmailStageTableHeader() { + + final String LEFT_STYLE = 'style="background:#f2f3f3;text-align:left"'; + final String RIGHT_STYLE = 'style="background:#f2f3f3;text-align:right"'; + + String result = ''; + + /************************************************** + Comments: Fetches the columns from the Custom Settings + **************************************************/ + Open_Opportunity_Fields__c[] selectedFields = OpenOpportunityReportController.getOpportunityFields(); + + if (!selectedFields.isEmpty()) { + + result += 'Opportunity Name'; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + result += '' + selectedField.Label__c + ''; + } + } + else { + + result += 'Opportunity Name' + + 'Stage Duration' + + 'Fogbugz Ticket Number' + + 'Fogbugz Assigned To' + + 'Probability (%)' + + 'Amount' + + 'Account Name' + + 'Business Unit Owner' + + 'Days not Updated'; + } + result += ''; + + + return result.replace('[LEFT_STYLE]', LEFT_STYLE).replace('[RIGHT_STYLE]', RIGHT_STYLE); + } + + /************************************************** + Method Name: getEmailStageTableRow + Method Comments: method called from buildEmailStageTable. For each opportunity this method is called. Renders the status of the opportunity and its data. + **************************************************/ + private static String getEmailStageTableRow(Opportunity opp, Integer daysNotUpdatedLimit, Integer daysNotUpdatedLimitEarlyStages, Set earlyStagesSet) { + + final String LEFT_STYLE = 'style="border-width:0 0 1px 0;vertical-align:middle;padding:4px 2px 4px 5px;border-bottom:1px solid #e3deb8;"'; + final String STYLE = 'style="border-width:0 0 1px 0;vertical-align:middle;padding:4px 2px 4px 5px;border-bottom:1px solid #e3deb8;text-align:right"'; + String rowStart = ''; + + /************************************************** + Comments: Filtering process in order to define the style of the Opportunity Row (red, yellow, etc) + **************************************************/ + if (opp.AccountId == null) { + rowStart = ''; + } + else { + rowStart = ''; + } + + // added by Nick - checking if it is a long wait after submission + + if (opp.StageName.contains('Submitted') && opp.long_wait__c) { + rowStart = rowStart; + } + else { + // end added by Nick + + if (earlyStagesSet.contains(opp.StageName)) { + rowStart = opp.Total_Days_Not_Updated__c > daysNotUpdatedLimitEarlyStages + ? '' + : rowStart; + } + else { + rowStart = opp.Total_Days_Not_Updated__c > daysNotUpdatedLimit + ? '' + : rowStart; + } + } + + String href = URL.getSalesforceBaseUrl().toExternalForm() + '/' + String.valueOf(opp.Id); + + Open_Opportunity_Fields__c[] selectedFields = OpenOpportunityReportController.getOpportunityFields(); + String result = rowStart; + + if (!selectedFields.isEmpty()) { + + /************************************************** + Comments: This loop goes over the Opportunity fields (based on the selected columns) and + formats the different fields (datetime, float, link, etc) + **************************************************/ + result += '' + opp.Name + ''; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + try { + + String fieldType = selectedField.Type__c; + + String fieldValue = ''; + if (selectedField.Name.equals('Owner.Name')) { + fieldValue = opp.Owner.Name; + } + else { + fieldValue = String.valueOf(opp.get(selectedField.Name)); + } + + if (fieldType.equalsIgnoreCase('Date')) { + fieldValue = opp.get(selectedField.Name) != null + ? Date.valueOf(opp.get(selectedField.Name)).format() + : ''; + } + else if (fieldType.equalsIgnoreCase('DateTime')) { + fieldValue = opp.get(selectedField.Name) != null + ? Datetime.valueOf(opp.get(selectedField.Name)).format('MM/dd/yyyy HH:mm a') + : ''; + } + else if (fieldType.equalsIgnoreCase('Currency')) { + + fieldValue = ''; + if (opp.get(selectedField.Name) != null) { + + List args = new String[]{'0','number','###,###,##0.00'}; + Decimal currencyValue = (Decimal) opp.get(selectedField.Name); + fieldValue = '$' + String.format(currencyValue.format(), args); + } + } + + if (selectedField.Name.equals('Fogbugz_Ticket_Number__c')) { + result += '' + fieldValue + ''; + } + else { + result += '' + fieldValue + ''; + } + } + catch (Exception e) {} + } + } + else { + /************************************************** + Comments: When the selectedFields list is empty, that means we need to display + the default columns, which are already defined below. + We don;t need to make a custom format treatment as above, as we already know which columns are selected. + **************************************************/ + String amountValue = ''; + if (opp.Amount != null) { + List args = new String[]{'0','number','###,###,##0.00'}; + amountValue = '$' + String.format(opp.Amount.format(), args); + } + + result += '' + opp.Name + '' + + '' + opp.Stage_Duration__c + '' + + '' + opp.Fogbugz_Ticket_Number__c + '' + + '' + opp.Fogbugz_Assigned_To__c + '' + + '' + opp.Fogbugz_Probability__c + '' + + '' + amountValue + '' + + '' + opp.Account.Name + '' + + '' + opp.Business_Unit_Owner__r.Name + '' + + '' + opp.Total_Days_Not_Updated__c + ''; + } + + result += ''; + + return result.replace('[STYLE]', STYLE).replace('[LEFT_STYLE]', LEFT_STYLE); + } + + /************************************************** + Method Name: getEmailStageComments + Method Comments: This method is called from buildEmailContent method, in order to add to the email comments written on the UI. + **************************************************/ + private static String getEmailStageComments(String comments) { + + String headerRow = ''; + + String dataComment = (comments != null && comments.trim().length() > 0) ? comments : ' '; + + String dataRow = '' + + dataComment + + ''; + + return '' + headerRow + dataRow + '
'; + } + + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityEmailUtils.cls-meta.xml b/src/classes/OpenOpportunityEmailUtils.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityEmailUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityFieldSelectionController.cls b/src/classes/OpenOpportunityFieldSelectionController.cls new file mode 100644 index 00000000..4ef6b61b --- /dev/null +++ b/src/classes/OpenOpportunityFieldSelectionController.cls @@ -0,0 +1,142 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ +public with sharing class OpenOpportunityFieldSelectionController { + + public Selectoption[] availableFields {get; set;} + public String selectedValue {get; set;} + public Integer orderCount {get; set;} + + public OpenOpportunityFieldSelectionController() { + + init(); + } + + public void addColumn() { + + if (selectedValue != null && selectedValue != '') { + insertNewColumn(); + removeSelectedValue(); + } + else { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Please select a field')); + } + } + + public void clearCurrentSelection() { + + Map result = Open_Opportunity_Fields__c.getAll(); + delete result.values(); + + init(); + } + + public Open_Opportunity_Fields__c[] getCurrentSelection() { + + Open_Opportunity_Fields__c[] result = new Open_Opportunity_Fields__c[] {}; + + result = [SELECT + Id, + Name, + Label__c, + Order__c + FROM Open_Opportunity_Fields__c + Order By Order__c]; + + return result; + } + + private void init() { + + availableFields = new Selectoption[] {}; + orderCount = 0; + Set alreadySelectedColumns = getCurrentSelectionNames(); + Map fieldList = getOpportunitySchemaFields(); + + String[] fieldIterator = new String[] {}; + fieldIterator.addAll(fieldList.keySet()); + fieldIterator.add('Opportunity Owner'); + fieldIterator.sort(); + + for (String fieldName :fieldIterator) { + + if (!alreadySelectedColumns.contains(fieldName)) { + + if (fieldName.equals('Opportunity Owner')) { + availableFields.add(new Selectoption('Owner.Name', 'Opportunity Owner')); + } + else { + Schema.Describefieldresult fieldResult = fieldList.get(fieldName); + availableFields.add(new Selectoption(fieldResult.getName(), fieldResult.getLabel())); + } + } + } + } + + private void insertNewColumn() { + + Open_Opportunity_Fields__c newColumn = new Open_Opportunity_Fields__c(); + + if (selectedValue.equals('Owner.Name')) { + newColumn.Name = 'Owner.Name'; + newColumn.Label__c = 'Opportunity Owner'; + newColumn.Type__c = 'String'; + } + else { + Map opportunitySchemaFields = getOpportunitySchemaFields(); + Schema.Describefieldresult fieldDescribe = opportunitySchemaFields.get(selectedValue); + + newColumn.Name = fieldDescribe.getName(); + newColumn.Label__c = fieldDescribe.getLabel(); + newColumn.Type__c = fieldDescribe.getType().name(); + } + orderCount ++; + newColumn.Order__c = orderCount; + + try { + insert newColumn; + } + catch(Exception e) {} + } + + private void removeSelectedValue() { + + for (Integer i = 0; i < availableFields.size(); i++) { + + if (availableFields[i].getValue().equals(selectedValue)) { + + availableFields.remove(i); + break; + } + } + } + + private Set getCurrentSelectionNames() { + + Map result = Open_Opportunity_Fields__c.getAll(); + orderCount = result.size(); + return result != null ? result.keySet() : new Set (); + } + + private Map getOpportunitySchemaFields() { + + Map fieldsSchema = Schema.SObjectType.Opportunity.fields.getMap(); + Map result = new Map(); + + for (String fieldName :fieldsSchema.keySet()) { + + Schema.SObjectField fieldSchema = fieldsSchema.get(fieldName); + Schema.Describefieldresult fieldDescribe = fieldSchema.getDescribe(); + + if (fieldDescribe.getName() != 'Name') { + result.put(fieldDescribe.getName(), fieldDescribe); + } + } + + return result; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml b/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityFieldSelectionController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityFieldsUIController.cls b/src/classes/OpenOpportunityFieldsUIController.cls new file mode 100644 index 00000000..1879135d --- /dev/null +++ b/src/classes/OpenOpportunityFieldsUIController.cls @@ -0,0 +1,27 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ +public with sharing class OpenOpportunityFieldsUIController { + + public OpenOpportunityListData stageContainer; + public String stageName {get; set;} + public String htmlTable {get; set;} + + + public OpenOpportunityFieldsUIController() {} + + + public void setStageContainer(OpenOpportunityListData value) { + + stageContainer = value; + htmlTable = OpenOpportunityEmailUtils.buildEmailStageTable(stageName, stageContainer.opportunities); + } + + public OpenOpportunityListData getStageContainer() { + + return stageContainer; + } +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml b/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityFieldsUIController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityListData.cls b/src/classes/OpenOpportunityListData.cls new file mode 100644 index 00000000..a7eb8fc9 --- /dev/null +++ b/src/classes/OpenOpportunityListData.cls @@ -0,0 +1,18 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/16/2012 + * + */ + public with sharing class OpenOpportunityListData { + + public String stageName {get; set;} + public Opportunity[] opportunities {get; set;} + + public OpenOpportunityListData(String stageName, Opportunity[] opportunities) { + + this.opportunities = opportunities; + this.stageName = stageName; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityListData.cls-meta.xml b/src/classes/OpenOpportunityListData.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityListData.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityMailer.cls b/src/classes/OpenOpportunityMailer.cls new file mode 100644 index 00000000..85447cc9 --- /dev/null +++ b/src/classes/OpenOpportunityMailer.cls @@ -0,0 +1,113 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +public class OpenOpportunityMailer { + + private static final String EMAIL_SUBJECT = ' | | Biz Dev Report Out'; + private static final String RED_EMAIL_SUBJECT = 'Your Overdue Opportunities'; + private static final String[] earlyStages = new String[] {'Stage 1 - Connect','Stage 2 - Talking', 'Stage 5 - Submitted'}; + + public static void sendOpenOpportunitiesBatchReport(User user, String[] recipients, Map stagedOpportunities) { + + Map stageComments = new Map(); + for (String stageComment :stagedOpportunities.keySet()) { + stageComments.put(stageComment, ''); + } + + String content = OpenOpportunityEmailUtils.buildEmailContent(stagedOpportunities, false, stageComments); + + sendEmail(content, recipients, EMAIL_SUBJECT, user.Name); + } + + public static void sendOpenOpportunitiesSingleReport(User[] users, String[] recipients, Map stageComments) { + + Id[] usersId = new Id[] {}; + String usersNameSubject = ' - '; + for (User user :users) { + usersId.add(user.Id); + usersNameSubject += user.Name + ' - '; + } + + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(usersId); + + String content = OpenOpportunityEmailUtils.buildEmailContent(stagedOpportunities, true, stageComments); + + sendEmail(content, recipients, EMAIL_SUBJECT, usersNameSubject); + } + + public static void sendRedOpenOpportunitiesBatchReport(User user, String[] recipients, Map stagedOpportunities) { + + Map redStagedOpportunities = new Map(); + + Integer daysNotUpdatedLimit = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit__c.intValue() + : 30; + + Integer daysNotUpdatedLimitEarlyStages = Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Days_Not_Updated_Limit_Early_Stages__c.intValue() + : 10; + + Set earlyStagesSet = new Set(earlyStages); + + // Remove fresh opportunities + for (String stageName :stagedOpportunities.keySet()) { + + Opportunity[] opportunities = new Opportunity[] {}; + + for(Opportunity opportunity :stagedOpportunities.get(stageName)) { + + if (earlyStagesSet.contains(opportunity.StageName)) { + + if (opportunity.Total_Days_Not_Updated__c > daysNotUpdatedLimitEarlyStages) { + opportunities.add(opportunity); + } + } + else { + + if (opportunity.Total_Days_Not_Updated__c > daysNotUpdatedLimit) { + opportunities.add(opportunity); + } + } + } + + if (!opportunities.isEmpty()) { + redStagedOpportunities.put(stageName, opportunities); + } + } + + // Only send Mail if there are opportunities + if (!redStagedOpportunities.isEmpty()) { + + Map stageComments = new Map(); + for (String stageComment :redStagedOpportunities.keySet()) { + stageComments.put(stageComment, ''); + } + + String content = OpenOpportunityEmailUtils.buildEmailContent(redStagedOpportunities, false, stageComments); + + sendEmail(content, recipients, RED_EMAIL_SUBJECT, user.Name); + } + } + + private static void sendEmail(String content, String[] recipients, String subjectTemplate, String userName) { + + String subject = subjectTemplate.replace('', userName).replace('', Date.today().format()); + + OrgWideEmailAddress wideAddress = OpenOpportunityReportController.getOrganizationWideAddressMail(); + + Messaging.Singleemailmessage mail = new Messaging.Singleemailmessage(); + + if (wideAddress != null) { + mail.setOrgWideEmailAddressId(wideAddress.Id); + } + + mail.setHtmlBody(content); + mail.setSubject(subject); + mail.setToAddresses(recipients); + Messaging.sendEmail(new Messaging.Email[] {mail}); + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityMailer.cls-meta.xml b/src/classes/OpenOpportunityMailer.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityMailer.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityNeedUpdateBatch.cls b/src/classes/OpenOpportunityNeedUpdateBatch.cls new file mode 100644 index 00000000..0fcdfa16 --- /dev/null +++ b/src/classes/OpenOpportunityNeedUpdateBatch.cls @@ -0,0 +1,41 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/22/2012 + * + */ +global class OpenOpportunityNeedUpdateBatch implements Database.Batchable { + + public OpenOpportunityNeedUpdateBatch(){} + + global Iterable start(Database.BatchableContext bc) { + + Opportunity [] opportunities = new Opportunity [] {}; + + opportunities = [SELECT + Id, + OwnerId + FROM Opportunity + WHERE Isclosed = false]; + + Set usersId = new Set(); + + for(Opportunity opp :opportunities) { + usersId.add(opp.OwnerId); + } + + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + return users; + } + + global void execute(Database.BatchableContext bc, User[] scope) { + + User user = scope[0]; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(user.Id); + + OpenOpportunityMailer.sendRedOpenOpportunitiesBatchReport(user, new String[]{user.Email}, stagedOpportunities); + } + + global void finish(Database.BatchableContext bc){} + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml b/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityNeedUpdateBatch.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityReportController.cls b/src/classes/OpenOpportunityReportController.cls new file mode 100644 index 00000000..d05654df --- /dev/null +++ b/src/classes/OpenOpportunityReportController.cls @@ -0,0 +1,202 @@ +/************************************************** +Class Name: OpenOpportunityReportController +Class Description: Opportunity Expert Controller +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando +Update Date: 2013-03-04 +Additional Comments: This controller performs all Opportunity Related queries for all components on the OpenOpportunites Module. + It also fetches the org wide email as well as the opportunity column fields from the Custom Settings. +**************************************************/ +public class OpenOpportunityReportController { + + private static OpenOpportunityReportController instance = null; + private static Open_Opportunity_Fields__c[] selectedColumnFields = null; + + private OpenOpportunityReportController() {} + + public static OpenOpportunityReportController getInstance() { + + if (instance == null) { + + instance = new OpenOpportunityReportController(); + } + return instance; + } + + public Opportunity[] getOpenOpportunitiesByUser(Id[] usersId) { + + String[] stages = new String[] {'Stage 1 - Connect','Stage 2 - Talking','Stage 3 - Prospect','Stage 4 - Proposal Development','Stage 5 - Submitted','Stage 6 - In Negotiations'}; + + Opportunity[] result = new Opportunity[] {}; + + /************************************************** + Comments: Filled in with default values + **************************************************/ + String queryValues = 'Id,Name,StageName,CreatedDate,Amount,AccountId,Total_Days_Not_Updated__c,Stage_Name_Updated_Date__c,Stage_Duration__c,Business_Unit_Owner__c,' + + 'Business_Unit_Owner__r.Name,Account.Name,OwnerId,Owner.Name,Fogbugz_Link__c,Fogbugz_Probability__c,Fogbugz_Days_Not_Updated__c,' + + 'Fogbugz_Ticket_Number__c,Fogbugz_Assigned_To__c,Fogbugz_Last_Updated_Date__c,long_wait__c'; + // THIS STRING MUST NOT END WITH A COMMA. + + Open_Opportunity_Fields__c[] selectedFields = getOpportunityFields(); + + + /************************************************** + Comments: WHEN CUSTOM COLUMNS ARE SELECTED, I ADD THOSE COLUMNS TO THE QUERY STRING + **************************************************/ + if (!selectedFields.isEmpty()) { + queryValues = 'Name,'; + for(Open_Opportunity_Fields__c selectedField :selectedFields) { + + if (selectedField.Name != 'Name') { + queryValues += selectedField.Name + ','; + } + } + + // THERE ARE SOME VALUES THAT NEED TO BE ADDED TO THE QUERY EVEN IF THEY WERE NOT SELECTED. + queryValues+='Business_Unit_Owner__r.Name,Account.Name'; + + if (!queryValues.contains('AccountId')) { + queryValues += ',AccountId'; + } + if (!queryValues.contains('Owner.Name')) { + queryValues += ',Owner.Name'; + } + if (!queryValues.contains('Total_Days_Not_Updated__c')) { + queryValues += ',Total_Days_Not_Updated__c'; + } + if (!queryValues.contains('StageName')) { + queryValues += ',StageName'; + } + if(!queryValues.contains('long_wait__c')) { + queryValues += ',long_wait__c'; + } + + /************************************************** + Comments: IF WE WANT TO ADD AN EXTRA FIELD THAT MUST BE USED AS A CONDITION PUT IT HERE. + + Expected Format: + + if (!queryValues.contains('FIELD_NAME__c')) { + queryValues += ',FIELD_NAME__c'; + } + + **************************************************/ + } + + /************************************************** + Comments: Perform the query on Opportunities based on the fields selected. + **************************************************/ + String sql = 'SELECT ' + queryValues + ' FROM Opportunity WHERE IsClosed = false AND OwnerId IN :usersId AND StageName IN :stages'; + + result = Database.query(sql); + return result; + } + + + public Map getOpenOpportunitiesOrderByStage(Id userId) { + + return getOpenOpportunitiesOrderByStage(new Id[] {userId}); + } + + public Map getOpenOpportunitiesOrderByStage(Id[] usersId) { + + Map result = new Map(); + Opportunity[] opportunities = getOpenOpportunitiesByUser(usersId); + + for(Opportunity opportunity :opportunities) { + + String stageName = opportunity.StageName; + + if (result.containsKey(stageName)) { + + result.get(stageName).add(opportunity); + } + else { + + result.put(stageName, new Opportunity[] {opportunity}); + } + } + + return result; + } + + public void updateStageDate(Id[] usersId) { + + Opportunity[] result = new Opportunity[] {}; + + result = [SELECT + StageName, + CreatedDate, + Stage_Name_Updated_Date__c, + + (SELECT StageName, CreatedDate FROM OpportunityHistories) + + FROM Opportunity + WHERE IsClosed = false + AND Stage_Name_Updated_Date__c = null + AND OwnerId IN :usersId]; + + Opportunity[] opportunities = new Opportunity[] {}; + + for (Opportunity opp :result) { + + if (opp.Stage_Name_Updated_Date__c == null) { + + opp.Stage_Name_Updated_Date__c = Date.valueOf(opp.CreatedDate); + for (OpportunityHistory oh :opp.OpportunityHistories) { + + String ohStageName = oh.StageName; + Date createdDate = Date.valueOf(oh.CreatedDate); + + if (ohStageName != opp.StageName) { + + opp.Stage_Name_Updated_Date__c = createdDate; + break; + } + } + opportunities.add(opp); + } + } + + if (!opportunities.isEmpty()) { + update opportunities; + } + + } + + public static Open_Opportunity_Fields__c[] getOpportunityFields() { + + if (selectedColumnFields == null) { + + selectedColumnFields = new Open_Opportunity_Fields__c[] {}; + selectedColumnFields = [SELECT + Id, + Name, + Label__c, + Type__c, + Order__c + FROM Open_Opportunity_Fields__c + Order By Order__c]; + } + + return selectedColumnFields; + } + + public static OrgWideEmailAddress getOrganizationWideAddressMail() { + + OrgWideEmailAddress[] addresses = new OrgWideEmailAddress[] {}; + final String DIMAGI_WIDE_ADDRESS_NAME = 'Dimagi Salesforce'; + + addresses = [SELECT Id, + Address, + DisplayName + FROM OrgWideEmailAddress + WHERE DisplayName = :DIMAGI_WIDE_ADDRESS_NAME]; + + if (!addresses.isEmpty()) { + return addresses[0]; + } + return null; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityReportController.cls-meta.xml b/src/classes/OpenOpportunityReportController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityReportController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityReportUIController.cls b/src/classes/OpenOpportunityReportUIController.cls new file mode 100644 index 00000000..9b38eca8 --- /dev/null +++ b/src/classes/OpenOpportunityReportUIController.cls @@ -0,0 +1,118 @@ +/** + * + * @author Fernando Rodriguez (frodriguez@adooxen.com) + * @date 01/11/2012 + * + */ +public class OpenOpportunityReportUIController { + + public String[] selectedUsers {get;set;} + public OpenOpportunityListData[] stagedListData {get; set;} + public Map stagedComments {get;set;} + public String recipients {get;set;} + public Id userId {get;set;} + + private static final String DEFAULT_EMAIL = Open_Opportunity_Settings__c.getOrgDefaults() != null + ? Open_Opportunity_Settings__c.getOrgDefaults().Default_Email_Recipient__c + : 'bizdev@dimagi.com'; + + public OpenOpportunityReportUIController() { + + userId = (Apexpages.currentPage().getParameters().containsKey('uid')) + ? Apexpages.currentPage().getParameters().get('uid') + : Userinfo.getUserId(); + + selectedUsers = new String[] {userId}; + stagedComments = new Map(); + stagedListData = new OpenOpportunityListData[] {}; + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(userId); + recipients = DEFAULT_EMAIL; + + if (!stagedOpportunities.isEmpty()) { + + String[] stageList = new String[] {}; + stageList.addAll(stagedOpportunities.keySet()); + stageList.sort(); + + for (String stageName :stageList) { + stagedComments.put(stageName, ''); + stagedListData.add(new OpenOpportunityListData(stageName, stagedOpportunities.get(stageName))); + } + } + } + + public void reload() { + + try { + + Id[] usersId = selectedUsers; + + if (usersId != null && !usersId.isEmpty()) { + + stagedComments = new Map(); + Map stagedOpportunities = OpenOpportunityReportController.getInstance().getOpenOpportunitiesOrderByStage(usersId); + stagedListData = new OpenOpportunityListData[] {}; + + OpenOpportunityReportController.getInstance().updateStageDate(usersId); + + if (!stagedOpportunities.isEmpty()) { + stagedListData = new OpenOpportunityListData[] {}; + + String[] stageList = new String[] {}; + stageList.addAll(stagedOpportunities.keySet()); + stageList.sort(); + + for (String stageName :stageList) { + stagedComments.put(stageName, ''); + stagedListData.add(new OpenOpportunityListData(stageName, stagedOpportunities.get(stageName))); + } + } + } + } + catch (Exception e) { + + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'An error ocurred. Please refresh the Report')); + } + } + + public void initAction() { + + Id userId = (Apexpages.currentPage().getParameters().containsKey('uid')) + ? Apexpages.currentPage().getParameters().get('uid') + : Userinfo.getUserId(); + + OpenOpportunityReportController.getInstance().updateStageDate(new Id[] {userId}); + } + + public void sendEmail() { + + try { + Id[] usersId = selectedUsers; + User[] users = [SELECT Id, Email, Name FROM User WHERE Id IN :usersId]; + + if (!users.isEmpty()) { + String[] mails = (recipients != null && recipients.trim().length() > 0) ? recipients.split(',') : new String[] {}; + OpenOpportunityMailer.sendOpenOpportunitiesSingleReport(users, mails, stagedComments); + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.CONFIRM, 'Mail Sent Success')); + } + else { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Cannot Send Mail. Please select one or more users')); + } + } + catch (Exception e) { + Apexpages.addMessage(new Apexpages.Message(ApexPages.Severity.FATAL, 'Cannot Send Mail. Please check message fields')); + } + } + + public Selectoption[] getUsers() { + + User[] users = [SELECT Id, Name FROM User ORDER BY Name]; + Selectoption[] result = new Selectoption[] {}; + + for (User user :users) { + result.add(new Selectoption(user.Id, user.Name)); + } + + return result; + } +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityReportUIController.cls-meta.xml b/src/classes/OpenOpportunityReportUIController.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityReportUIController.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityTest.cls b/src/classes/OpenOpportunityTest.cls new file mode 100644 index 00000000..b8a7b61d --- /dev/null +++ b/src/classes/OpenOpportunityTest.cls @@ -0,0 +1,135 @@ +/************************************************** +Class Name: OpenOpportunityTest +Class Description: Class for Open Opportunities Testing and Coverage +Author: Fernando Rodriguez (frodriguez@adooxen.com) +Modified By: Fernando Rodriguez +Update Date: 2013-03-04 +Additional Comments: +**************************************************/ +@isTest +public class OpenOpportunityTest { + + public static final String TEST_EMAIL = 'frodriguez@adooxen.com'; + + static testMethod void testEmailReportSuccess() { + + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + controller.getUsers(); + controller.reload(); + for (String stageComment :controller.stagedComments.keySet()) { + controller.stagedComments.put(stageComment, 'Test Comment'); + } + controller.initAction(); + controller.sendEmail(); + Test.stopTest(); + } + + /* + static testMethod void testEmailReportSuccessWithFields() { + + addCustomColumns(); + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + + for (String stageComment :controller.stagedComments.keySet()) { + controller.stagedComments.put(stageComment, 'Test Comment'); + } + controller.initAction(); + controller.sendEmail(); + Test.stopTest(); + } + */ + + static testMethod void testEmailReportFailure() { + + createOpportunity(); + Test.startTest(); + OpenOpportunityReportUIController controller = new OpenOpportunityReportUIController(); + controller.recipients += 'this is not an address'; + controller.sendEmail(); + Test.stopTest(); + } + + static testMethod void testBatchEmailReport() { + + createOpportunity(); + Test.startTest(); + Database.executeBatch(new OpenOpportunitiesBatch()); + Test.stopTest(); + } + + static testMethod void testBatchRedEmailReport() { + + createOpportunity(); + Test.startTest(); + Database.executeBatch(new OpenOpportunityNeedUpdateBatch()); + Test.stopTest(); + } + + static testMethod void testOpportunityStageDuration() { + + Test.startTest(); + Id opportunityId = createOpportunity(); + Opportunity opp = [SELECT StageName FROM Opportunity WHERE Id = :opportunityId]; + opp.StageName = 'Stage 2 - Talking'; + update opp; + Test.stopTest(); + } + + private static void addCustomColumns() { + + Open_Opportunity_Fields__c column = new Open_Opportunity_Fields__c(); + column.Name = 'CreatedDate'; + column.Label__c = 'Created Date'; + column.Type__c = 'DateTime'; + column.Order__c = 1; + insert column; + + column = new Open_Opportunity_Fields__c(); + column.Name = 'CloseDate'; + column.Label__c = 'Close Date'; + column.Type__c = 'Date'; + column.Order__c = 2; + insert column; + } + + + private static Id createOpportunity() { + + Country__c country = new Country__c(); + country.Name = 'Test'; + insert country; + + Account account = new Account(); + account.Name = 'Test Account'; + account.Office_Type__c = 'Country Office'; + account.Country__c = country.Id; + insert account; + + // Create 2 Opportunities for this user and account; + Id opportunityId = createOpportunity(account.Id); + createOpportunity(account.Id); + + return opportunityId; + } + + private static Id createOpportunity(Id accountId) { + + Opportunity opportunity = new Opportunity(); + opportunity.Name = 'Test Opportunity'; + opportunity.Amount = 5000; + opportunity.Fogbugz_Assigned_To__c = 'Test Assignee'; + opportunity.Fogbugz_Ticket_Number__c = '12345'; + opportunity.Fogbugz_Last_Updated_Date__c = Date.today(); + opportunity.StageName = 'Stage 1 - Connect'; + opportunity.CloseDate = Date.today(); + opportunity.AccountId = accountId; + insert opportunity; + + return opportunity.Id; + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityTest.cls-meta.xml b/src/classes/OpenOpportunityTest.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/classes/OpenOpportunityUtils.cls b/src/classes/OpenOpportunityUtils.cls new file mode 100644 index 00000000..fef68294 --- /dev/null +++ b/src/classes/OpenOpportunityUtils.cls @@ -0,0 +1,23 @@ +public with sharing class OpenOpportunityUtils { + + public static void updateStageName(Opportunity[] opportunities) { + + for (Opportunity opportunity :opportunities) { + + opportunity.Stage_Name_Updated_Date__c = Date.today(); + } + } + + public static void updateStageName(Opportunity[] opportunities, Map oldOpportunities) { + + for (Opportunity opportunity :opportunities) { + + Opportunity oldOpportunity = oldOpportunities.get(opportunity.Id); + + if (oldOpportunity.StageName != opportunity.StageName) { + opportunity.Stage_Name_Updated_Date__c = Date.today(); + } + } + } + +} \ No newline at end of file diff --git a/src/classes/OpenOpportunityUtils.cls-meta.xml b/src/classes/OpenOpportunityUtils.cls-meta.xml new file mode 100644 index 00000000..5a25bcec --- /dev/null +++ b/src/classes/OpenOpportunityUtils.cls-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active + diff --git a/src/components/OpenOpportunityReportTable.component b/src/components/OpenOpportunityReportTable.component new file mode 100644 index 00000000..c7130069 --- /dev/null +++ b/src/components/OpenOpportunityReportTable.component @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/objects/Open_Opportunity_Fields__c.object b/src/objects/Open_Opportunity_Fields__c.object new file mode 100644 index 00000000..bbe97224 --- /dev/null +++ b/src/objects/Open_Opportunity_Fields__c.object @@ -0,0 +1,36 @@ + + + List + Public + false + false + + Label__c + false + + 100 + false + Text + false + + + Order__c + false + + 3 + false + 0 + Number + true + + + Type__c + false + + 50 + false + Text + false + + + diff --git a/src/objects/Open_Opportunity_Settings__c.object b/src/objects/Open_Opportunity_Settings__c.object new file mode 100644 index 00000000..c692b514 --- /dev/null +++ b/src/objects/Open_Opportunity_Settings__c.object @@ -0,0 +1,46 @@ + + + Hierarchy + Public + Open Opportunity Settings + false + false + + Days_Not_Updated_Limit_Early_Stages__c + 10 + If Days not Updated field is greater than this value, the row will be colored red. + false + If Days not Updated field is greater than this value, the row will be colored red. +Applies for Stages 1& 2 + + 3 + false + 0 + Number + false + + + Days_Not_Updated_Limit__c + 30 + If Days not Updated field is greater than this value, the row will be colored red. + false + If Days not Updated field is greater than this value, the row will be colored red. +Applies for Stages 3 + + + 3 + false + 0 + Number + false + + + Default_Email_Recipient__c + Default Email Recipient + false + + true + Email + false + + + diff --git a/src/objects/Opportunity.object b/src/objects/Opportunity.object new file mode 100644 index 00000000..476f7e95 --- /dev/null +++ b/src/objects/Opportunity.object @@ -0,0 +1,1048 @@ + + + true + + Account_Name__c + false + Account.Name + + false + Text + false + + + Area__c + false + + + + Health + true + + + Poverty and Economic Development + false + + + Natural Resource Management + false + + + Training & Education Services + false + + + Logistics + false + + + Other + false + + false + + false + MultiselectPicklist + 6 + + + Business_Unit_Owner_Name__c + false + Business_Unit_Owner__r.FirstName + ' ' + Business_Unit_Owner__r.LastName + BlankAsZero + + false + Text + false + + + Business_Unit_Owner__c + SetNull + false + Who owns the Biz Dev process for this business unit. + + User + Opportunities + false + false + Lookup + + + Country_Name__c + false + Country__r.Name + + false + Text + false + + + Country__c + Restrict + false + What country is this opportunity taking place. + + Country__c + Opportunities + Opportunities + false + false + Lookup + + + Deadline_for_Submitting_Proposal__c + false + Internal deadline for review ready. + + 32768 + false + LongTextArea + 3 + + + Fogbugz_Assigned_To__c + false + Who is the Fogbugz ticket assigned to currently. + + 50 + false + false + Text + false + + + Fogbugz_Client__c + false + + 50 + false + false + Text + false + + + Fogbugz_Days_Not_Updated__c + Today - Fogbugz Last Updated Date + false + TODAY() - DATEVALUE(Fogbugz_Last_Updated_Date__c) + BlankAsZero + + 18 + false + 0 + Number + false + + + Fogbugz_Last_Updated_Date__c + false + The last day the Fogbugz ticket was updated. + + false + false + DateTime + + + Fogbugz_Link__c + false + "http://manage.dimagi.com/default.asp?" & Fogbugz_Ticket_Number__c + + false + Text + false + + + Fogbugz_Most_Recent_Note__c + false + The most recent update to Fogbugz ticket. + + 32768 + false + LongTextArea + 5 + + + Fogbugz_Opened_By__c + false + + 50 + false + false + Text + false + + + Fogbugz_Owner_Mapping__c + false + CASE( Fogbugz_Assigned_To__c , +"Gillian Javetski", "Neal Lesh", +"Benjamin Lightburn", "Neal Lesh", +"Andrea Fletcher", "Kieran Sharpey-Schafer", +"Sheel Shah", "Devika Sarin", +"Mohini Bhavsar", "Devika Sarin", +"Krishna Swamy", "Devika Sarin", +"Vikram Kumar","Neal Lesh", +"Nick Nestle") + + false + Text + false + + + Fogbugz_Probability__c + false + + 18 + false + 0 + false + Percent + + + Fogbugz_Ticket_Number__c + true + The ticket number of the corresponding Fogbugz ticket. + + 50 + false + false + Text + false + + + Funder_Account__c + Restrict + false + The account that is supplying the money for the opportunity. i.e. USAID funds CARE who then pays us. USAID is the "Funder Account", CARE is the "Account". + + Account + Opportunities Funded + Opportunities_Funded + false + false + Lookup + + + Funding_Type__c + false + Please flag if NIH or gov't potentially + + 32768 + false + LongTextArea + 3 + + + Implementing_Business_Unit__c + SetNull + false + The Dimagi BU that will manage the execution of the project. + + Business_Unit__c + Opportunities (Implementing Business Unit) + Opportunities1 + false + false + Lookup + + + Industry__c + false + + + + Agriculture + false + + + Consulting + false + + + Education + false + + + Finance + false + + + Health + false + + + Mobile Money + false + + + Nutrition + false + + + Other + false + + + Telecom + false + + + Water & Sanitation + false + + false + + false + Picklist + + + LeadSource + + + Contact Us + false + + + Referred to Us + false + + + Other + false + + + Conference + false + + + Pulled from Website + false + + + Publication - First Author + false + + + Publication - Last Author + false + + + Web Sign Up + false + + + Workshop - Maputo + false + + + Workshop - Dakar + false + + + Workshop - Other + false + + false + + Picklist + + + Opp_Stage__c + false + CASE( StageName , +'Stage 1 - Connect', 'Stage 1 - Connect', +'Stage 2 - Talking', 'Stage 2 - Talking', +'Stage 3 - Prospect', 'Stage 3 - Prospect', +'Stage 4 - Proposal Development', 'Stage 4 - Proposal Development', +'Stage 5 - Submitted', 'Stage 5 - Submitted', +'Stage 6 - In Negotiations', 'Stage 6 - In Negotiations', +'Closed - Lost','Closed - Lost', +'Closed - Won', 'Closed - Won', +'Pending other action', 'Pending other action', +'Closed','Closed', +'Stage 1 - Connect') + BlankAsZero + + false + Text + false + + + Opportunity_Owning_Entity__c + Restrict + false + Which busniness unit owns this opportunity. + + Business_Unit__c + Opportunities + Opportunities + false + false + Lookup + + + Product_Text__c + Populated via a trigger + false + Stores the products on the opp record so you can filter on them. + + false + false + TextArea + + + Project_Dates__c + false + Start Date, when features need to be ready, etc. + + 32768 + false + LongTextArea + 4 + + + Proposal_Dropbox_Location__c + false + This should be in Dimagi - Proposals + + false + false + TextArea + + + Report_Out_Summary__c + false + Used in the Biz Dev Report Outs to add comments. + + false + false + TextArea + + + Salesforce_Opportunity_ID__c + false + Id + BlankAsZero + Salesforce's internal reference ID. Put this in the Fogbugz External ID field. + + false + Text + false + + + Short_Description__c + false + + false + false + TextArea + + + StageName + + + Stage 1 - Connect + false + false + Pipeline + 0 + false + + + Stage 2 - Talking + false + false + Talking with an org but there is no concrete opportunity yet. + Pipeline + 0 + false + + + Stage 3 - Prospect + false + false + There is a real discreet opportunity now. A Fogbugz ticket is created. + Pipeline + 0 + false + + + Stage 4 - Proposal Development + false + false + Developing a proposal. + Pipeline + 0 + false + + + Stage 5 - Submitted + false + false + You have submitted the proposal + Pipeline + 0 + false + + + Stage 6 - In Negotiations + false + false + Won the award and negotiating price. + Pipeline + 0 + false + + + Closed + false + false + Pipeline + 0 + false + + false + + Picklist + + + Stage_Duration__c + false + TODAY() - Stage_Name_Updated_Date__c + BlankAsZero + + 18 + false + 0 + Number + false + + + Stage_Name_Updated_Date__c + false + + false + false + Date + + + Sub_Area__c + false + + + + *** Health *** + false + + + Maternal, Newborn, & Child Health + false + + + Family Planning + false + + + HIV/AIDS + false + + + Malaria + false + + + Respiratory Diseases + false + + + Tuberculosis + false + + + Polio + false + + + Vaccinations + false + + + Diarrhea + false + + + Primary Care + false + + + Non-Communicable Diseases + false + + + Mental Health + false + + + Nutrition + false + + + *** Poverty and Economic Development *** + false + + + Gender Services + false + + + Water, Sanitation, & Hygiene + false + + + Financial Services to the Poor + false + + + Urban Development + false + + + *** Natural Resource Management *** + false + + + Agriculture + false + + + Food Security + false + + + Environment + false + + + *** Training & Education Services *** + false + + + Adult Training + false + + + Child Education + false + + + Early Childhood Development + false + + + *** Logistics *** + false + + + Human Resources + false + + + Commodity Tracking/Procurement + false + + + *** Other *** + false + + + Emergency Response + false + + + Orphans and Vulnerable Children + false + + + Telecommunications + false + + false + + false + MultiselectPicklist + 8 + + + Tech_Capabilities_Features__c + false + Known technical dependencies / architecture. + + 32768 + false + LongTextArea + 5 + + + Total_Days_Not_Updated__c + false + MIN(Fogbugz_Days_Not_Updated__c, TODAY() - DATEVALUE(LastModifiedDate)) + BlankAsZero + + 18 + false + 0 + Number + false + + + Type + + + Existing Business + false + + + New Business + false + + false + + Picklist + + + X10_Major_component_risks__c + false + + 32768 + false + LongTextArea + 3 + + + X11_Worked_with_org_before__c + false + Any special context to know about? + + 32768 + false + LongTextArea + 3 + + + X4_Budget_Size__c + false + Size, split: dev/field/server, are we willing to lose money on this? + + 32768 + false + LongTextArea + 3 + + + X5_Which_Entity__c + false + Inc, DSA, DSI + + 32768 + false + LongTextArea + 3 + + + X7_Long_term_partnership_or_one_off__c + false + Partnership potential or just is this a one-off project? + + 32768 + false + LongTextArea + 3 + + + X8_Other_Direct_Costs_ODC_covered_by__c + false + Are ODC covered by Dimagi or the partner. e.g. SMS gateway. + + 32768 + false + LongTextArea + 3 + + + X9_Room_for_innovation__c + false + Is there room for innovation or are we specifically neglecting parts of the RFP? + + 32768 + false + LongTextArea + 5 + + + long_wait__c + false + false + For submitted opportunities with a long waiting period to hear back. These won't be marked red on the Biz Dev reports. + + false + Checkbox + + + Active_Opps + OPPORTUNITY.NAME + ACCOUNT.NAME + Funder_Account__c + Area__c + Sub_Area__c + OPPORTUNITY.AMOUNT + OPPORTUNITY.STAGE_NAME + Fogbugz_Probability__c + CORE.USERS.ALIAS + Everything + + OPPORTUNITY.STAGE_NAME + notEqual + Closed + + + + + AllOpportunities + OPPORTUNITY.CREATED_DATE + OPPORTUNITY.NAME + ACCOUNT.NAME + CORE.USERS.FULL_NAME + Fogbugz_Assigned_To__c + Fogbugz_Ticket_Number__c + Fogbugz_Link__c + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Opportunity_Owning_Entity__c + Country__c + Country_Name__c + Everything + + + + ClosingNextMonth + Everything + + OPPORTUNITY.CLOSED + equals + 0 + + + OPPORTUNITY.CLOSE_DATE + equals + NEXT_MONTH + + + + + ClosingThisMonth + Everything + + OPPORTUNITY.CLOSED + equals + 0 + + + OPPORTUNITY.CLOSE_DATE + equals + THIS_MONTH + + + + + Moz_Opportunities + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Everything + + + + Moz_Opportunities1 + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Mine + + + + MyOpportunities + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.CREATED_DATE + OPPORTUNITY.STAGE_NAME + Fogbugz_Assigned_To__c + Fogbugz_Ticket_Number__c + Salesforce_Opportunity_ID__c + CORE.USERS.ALIAS + Fogbugz_Client__c + Fogbugz_Last_Updated_Date__c + Fogbugz_Link__c + Fogbugz_Most_Recent_Note__c + Fogbugz_Opened_By__c + Fogbugz_Probability__c + Mine + + OPPORTUNITY.STAGE_NAME + notEqual + Closed Won,Closed Lost + + + + + My_South_Africa_Opps + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.STAGE_NAME + OPPORTUNITY.AMOUNT + OPPORTUNITY.CLOSE_DATE + CORE.USERS.ALIAS + Country__c + Opportunity_Owning_Entity__c + Fogbugz_Link__c + Everything + + Country__c + equals + South Africa + + + + + NewThisWeek + Everything + + OPPORTUNITY.CREATED_DATE + equals + THIS_WEEK + + + + + Non_Integrated_Opportunities + OPPORTUNITY.NAME + Fogbugz_Assigned_To__c + ACCOUNT.NAME + OPPORTUNITY.STAGE_NAME + CORE.USERS.ALIAS + Fogbugz_Client__c + Fogbugz_Last_Updated_Date__c + Fogbugz_Link__c + Fogbugz_Opened_By__c + Fogbugz_Ticket_Number__c + Everything + + CORE.USERS.ALIAS + equals + fogbugz + + + + + Opportunities_without_Accounts + OPPORTUNITY.NAME + ACCOUNT.NAME + Salesforce_Opportunity_ID__c + CORE.USERS.ALIAS + OPPORTUNITY.STAGE_NAME + Fogbugz_Assigned_To__c + Fogbugz_Link__c + Everything + + Fogbugz_Assigned_To__c + equals + closed + + + + + Opportunities_without_an_Owner + OPPORTUNITY.NAME + ACCOUNT.NAME + Fogbugz_Assigned_To__c + OPPORTUNITY.AMOUNT + OPPORTUNITY.STAGE_NAME + Fogbugz_Link__c + Fogbugz_Ticket_Number__c + Everything + + Fogbugz_Assigned_To__c + equals + CLOSED + + + + + Won + Everything + + OPPORTUNITY.WON + equals + 1 + + + OPPORTUNITY.CLOSED + equals + 1 + + + + + OPPORTUNITY.NAME + ACCOUNT.NAME + OPPORTUNITY.CLOSE_DATE + Sync_with_FB_multi + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.NAME + ACCOUNT.NAME + ACCOUNT.SITE + OPPORTUNITY.STAGE_NAME + OPPORTUNITY.CLOSE_DATE + CORE.USERS.ALIAS + + + Change_Stage_Name + false + ISCHANGED( StageName ) && $User.ProfileId = '00eb0000000gmdW' + StageName + You may not make changes to this field in Salesforce. Please edit the Fogbugz ticket. + + + Sync_with_FB_multi + online + massActionButton + 600 + page + Sync all with FogBugz + sidebar + RunFBSync + false + true + + + Sync_with_FB_single + online + button + 600 + page + Sync with FogBugz + sidebar + RunFBSyncSingle + false + + diff --git a/src/package.xml b/src/package.xml index 3340efa1..17eb7117 100644 --- a/src/package.xml +++ b/src/package.xml @@ -4,5 +4,55 @@ * ApexClass - 25.0 + + * + ApexComponent + + + * + ApexPage + + + * + ApexTrigger + + + * + Account + AccountContactRole + Activity + Asset + Campaign + CampaignMember + Case + CaseContactRole + Contact + ContentVersion + Contract + ContractContactRole + Event + Idea + Lead + Opportunity + OpportunityCompetitor + OpportunityContactRole + OpportunityLineItem + PartnerRole + Product2 + Site + SocialPersona + Solution + Task + User + CustomObject + + + * + CustomTab + + + * + StaticResource + + 27.0 diff --git a/src/pages/OpenOpportunityFieldSelection.page b/src/pages/OpenOpportunityFieldSelection.page new file mode 100644 index 00000000..4afa38b1 --- /dev/null +++ b/src/pages/OpenOpportunityFieldSelection.page @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/OpenOpportunityReportLayout.page b/src/pages/OpenOpportunityReportLayout.page new file mode 100644 index 00000000..6f50aa5c --- /dev/null +++ b/src/pages/OpenOpportunityReportLayout.page @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/staticresources/SiteSamples.resource b/src/staticresources/SiteSamples.resource new file mode 100644 index 00000000..014adbd8 Binary files /dev/null and b/src/staticresources/SiteSamples.resource differ diff --git a/src/staticresources/SiteSamples.resource-meta.xml b/src/staticresources/SiteSamples.resource-meta.xml new file mode 100644 index 00000000..1ed30e1a --- /dev/null +++ b/src/staticresources/SiteSamples.resource-meta.xml @@ -0,0 +1,6 @@ + + + Public + application/zip + Static resource for sites sample pages + diff --git a/src/tabs/Open_Opportunities.tab b/src/tabs/Open_Opportunities.tab new file mode 100644 index 00000000..4f120dd2 --- /dev/null +++ b/src/tabs/Open_Opportunities.tab @@ -0,0 +1,7 @@ + + + + false + Custom56: Bottle + OpenOpportunityReportLayout + diff --git a/src/tabs/Report_Settings.tab b/src/tabs/Report_Settings.tab new file mode 100644 index 00000000..2113f012 --- /dev/null +++ b/src/tabs/Report_Settings.tab @@ -0,0 +1,7 @@ + + + + false + Custom26: Flag + OpenOpportunityFieldSelection + diff --git a/src/triggers/OpenOpportunityUpdateTrigger.trigger b/src/triggers/OpenOpportunityUpdateTrigger.trigger new file mode 100644 index 00000000..d5f49324 --- /dev/null +++ b/src/triggers/OpenOpportunityUpdateTrigger.trigger @@ -0,0 +1,9 @@ +trigger OpenOpportunityUpdateTrigger on Opportunity (before insert, before update) { + + if (trigger.isInsert) { + OpenOpportunityUtils.updateStageName(trigger.new); + } + else if (trigger.isUpdate) { + OpenOpportunityUtils.updateStageName(trigger.new, trigger.oldMap); + } +} \ No newline at end of file diff --git a/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml b/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml new file mode 100644 index 00000000..6e684be3 --- /dev/null +++ b/src/triggers/OpenOpportunityUpdateTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 26.0 + Active +