Adding react-teams-tab-field-visit-mashup sample (#848)
This commit is contained in:
parent
b0204bd14b
commit
a8f4e4f0c2
|
@ -0,0 +1,25 @@
|
||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
|
||||||
|
[*]
|
||||||
|
|
||||||
|
# change these settings to your own preference
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# we recommend you to keep these unchanged
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[{package,bower}.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
|
@ -0,0 +1,34 @@
|
||||||
|
constants.ts
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Build generated files
|
||||||
|
dist
|
||||||
|
lib
|
||||||
|
solution
|
||||||
|
temp
|
||||||
|
*.sppkg
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Visual Studio files
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
.vs
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
|
||||||
|
# Resx Generated Code
|
||||||
|
*.resx.ts
|
||||||
|
|
||||||
|
# Styles Generated Code
|
||||||
|
*.scss.ts
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"@microsoft/generator-sharepoint": {
|
||||||
|
"plusBeta": true,
|
||||||
|
"isCreatingSolution": false,
|
||||||
|
"environment": "spo",
|
||||||
|
"version": "1.7.0",
|
||||||
|
"libraryName": "field-visit-demo-tab",
|
||||||
|
"libraryId": "67407b5b-e3eb-480a-97a3-195810f83702",
|
||||||
|
"packageManager": "npm",
|
||||||
|
"isDomainIsolated": false,
|
||||||
|
"componentType": "webpart"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Bob German
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Teams Tab - Field Visit Demo (Mashup)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
A web part for use in Microsoft Teams that displays a mashup of information partaining to customer visits. Visits are obtained from the Team's shared calendar and displayed by user. When a visit is selected, the solution displays:
|
||||||
|
* customer information (from the Northwind database)
|
||||||
|
* documents (from SharePoint)
|
||||||
|
* recent transactions (mock)
|
||||||
|
* a map (Bing maps)
|
||||||
|
* current weather (Open Weather Map)
|
||||||
|
* photos (from SharePoint)
|
||||||
|
* a text box for sending messages to the Teams channel with deep links back to the selected visit
|
||||||
|
|
||||||
|
The solution demonstrates:
|
||||||
|
|
||||||
|
* A Teams tab using SharePoint Framework
|
||||||
|
* Accessing the hosting Team using the teams context and the Graph API
|
||||||
|
* Deep linking to a SharePoint Framework tab
|
||||||
|
* A mashup using React components
|
||||||
|
|
||||||
|
![Field Visit Demo](./documentation/FieldVisitDemo.png)
|
||||||
|
|
||||||
|
## Used SharePoint Framework Version
|
||||||
|
|
||||||
|
![drop](https://img.shields.io/badge/drop-1.8.0-green.svg)
|
||||||
|
|
||||||
|
## Applies to
|
||||||
|
|
||||||
|
* [SharePoint Framework Developer](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
|
||||||
|
* [Building Microsoft Teams Tabs using SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/integrate-with-teams-introduction)
|
||||||
|
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
|
||||||
|
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Solution|Author(s)
|
||||||
|
--------|---------
|
||||||
|
field-visit-demo-tab | Bob German ([@Bob1German](http://www.twitter.com/Bob1German))
|
||||||
|
|
||||||
|
Many thanks to [Arbindo Chattopadhyay](https://www.linkedin.com/in/arbindoc/) for writing the [detailed installation instructions ](./documentation/setup.md) and compiling [links to resources](./documentation/resources.md).
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
Version|Date|Comments
|
||||||
|
-------|----|--------
|
||||||
|
1.1|April 20, 2019|Updated for SPFx 1.8, moved to sp-dev-fx-webparts
|
||||||
|
1.0|April 5, 2019|Initial release
|
||||||
|
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Minimal Path to Awesome
|
||||||
|
|
||||||
|
The quickest path to building the web part and running it with mock data:
|
||||||
|
|
||||||
|
* Obtain API keys for [Bing Maps](https://docs.microsoft.com/en-us/bingmaps/getting-started/bing-maps-dev-center-help/getting-a-bing-maps-key) and [Open Weather Maps](https://openweathermap.org/api). In src/webparts/fieldVisitTab/, copy constants.sample.ts to constants.ts and add the API keys.
|
||||||
|
* npm install
|
||||||
|
* gulp serve
|
||||||
|
|
||||||
|
To work with real data, the web part requires content to be in place including:
|
||||||
|
|
||||||
|
* A Microsoft Team
|
||||||
|
* Calendar items in the Team's shared calendar, encoded with Northwind database customer IDs
|
||||||
|
* Documents and photos in SharePoint
|
||||||
|
|
||||||
|
[Detailed setup instructions are here](./documentation/setup.md).
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||||
|
"version": "2.0",
|
||||||
|
"bundles": {
|
||||||
|
"field-visit-tab-web-part": {
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"entrypoint": "./lib/webparts/fieldVisitTab/FieldVisitTabWebPart.js",
|
||||||
|
"manifest": "./src/webparts/fieldVisitTab/FieldVisitTabWebPart.manifest.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"externals": {},
|
||||||
|
"localizedResources": {
|
||||||
|
"FieldVisitTabWebPartStrings": "lib/webparts/fieldVisitTab/loc/{locale}.js",
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||||
|
"deployCdnPath": "temp/deploy"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||||
|
"workingDir": "./temp/deploy/",
|
||||||
|
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||||
|
"container": "field-visit-demo-tab",
|
||||||
|
"accessKey": "<!-- ACCESS KEY -->"
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||||
|
"solution": {
|
||||||
|
"name": "field-visit-demo-tab-client-side-solution",
|
||||||
|
"id": "67407b5b-e3eb-480a-97a3-195810f83702",
|
||||||
|
"version": "1.0.1.0",
|
||||||
|
"includeClientSideAssets": true,
|
||||||
|
"skipFeatureDeployment": true,
|
||||||
|
"webApiPermissionRequests": [
|
||||||
|
{
|
||||||
|
"resource": "Microsoft Graph",
|
||||||
|
"scope": "Calendars.Read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resource": "Microsoft Graph",
|
||||||
|
"scope": "Group.ReadWrite.All"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isDomainIsolated": false
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"zippedPackage": "solution/field-visit-demo-tab.sppkg"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||||
|
"port": 4321,
|
||||||
|
"https": true,
|
||||||
|
"initialPage": "https://localhost:5432/workbench",
|
||||||
|
"api": {
|
||||||
|
"port": 5432,
|
||||||
|
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||||
|
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 658 KiB |
|
@ -0,0 +1,28 @@
|
||||||
|
# Resources
|
||||||
|
|
||||||
|
Here are some resources to help you understand the tooling used in this solution.
|
||||||
|
|
||||||
|
TypeScript
|
||||||
|
* [TypeScript Site](http://bit.ly/SPF-TypeScript)
|
||||||
|
* [TypeScript Playground](http://bit.ly/TSPlayground)
|
||||||
|
* [Bob’s TS “Cheat Sheets”](http://bit.ly/LearnTypeScript)
|
||||||
|
|
||||||
|
Tools
|
||||||
|
* [WebPack](http://bit.ly/SPF-WebPack)
|
||||||
|
* [Gulp task runner](http://bit.ly/SPF-Gulp)
|
||||||
|
* [Visual Studio Code](http://bit.ly/SPF-VSCode)
|
||||||
|
|
||||||
|
|
||||||
|
Tutorials
|
||||||
|
* [SPFx Dev Setup](http://bit.ly/SPFx-DevSetup)
|
||||||
|
* [Build your first Web Part](http://bit.ly/SPFx-FirstWP)
|
||||||
|
* [SPFx Labs](http://bit.ly/SPFx-Labs)
|
||||||
|
|
||||||
|
Teams
|
||||||
|
* [Building Microsoft Teams Tabs using SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/integrate-with-teams-introduction)
|
||||||
|
* [Teams Labs](http://bit.ly/TeamsDevLabs)
|
||||||
|
|
||||||
|
APIs
|
||||||
|
* [Graph API](http://bit.ly/MSGraphAPI)
|
||||||
|
* [SharePoint REST API](http://bit.ly/MSSharePointAPI)
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Configuration - Field Visit Demo
|
||||||
|
|
||||||
|
## 1. Setting up your development environment
|
||||||
|
|
||||||
|
The FieldVisit demo is a SharePoint Framework (SPFx) implementation. It is intended to demonstrate key SPFx capabilities hosted in SharePoint as well as Microsoft Teams.
|
||||||
|
Follow instructions from articles below to setup your Office 365 tenant and development environment
|
||||||
|
|
||||||
|
* [Set up Office 365 Tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||||
|
* [Set up development environment](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-development-environment)
|
||||||
|
|
||||||
|
## 2. SharePoint and Teams Configuration
|
||||||
|
The sample uses content from Teams and SharePoint to build the demo scenario. In this section we will walk through configuring Teams and SharePoint with demo content.
|
||||||
|
|
||||||
|
### Calendar Configuration
|
||||||
|
Select a Team in Microsoft Teams where you are owner and follow steps below
|
||||||
|
|
||||||
|
1. Browse to the Team where you’d like this sample to be configured and navigate to General channel. **NOTE:** You can choose to configure the demo on any channel of your choice. For sake of simplicity, we will be referring to General channel throughout this document.
|
||||||
|
2. Create a scheduled meeting and name it “Visit The Big Cheese (THEBI)”.
|
||||||
|
* Ensure the meeting is scheduled in the General channel
|
||||||
|
* The demo shows the upcoming 1 week's appointments, so to keep the demo working, ensure the meeting is repeated once a week for however long you need the demo
|
||||||
|
* Set the meeting duration as you wish, such as 1 hour from 10 AM to 11 AM
|
||||||
|
* You may choose to add participants, but not mandatory. The meeting will be displayed under a tab for your username as well as any other users you add as participants.
|
||||||
|
Since the meeting is scheduled in the channel, all team members will be able to join this meeting
|
||||||
|
3. Log out and Log in using a different user login which has access to the same team. Repeat step 2 with a different name and time, for example "Building inspection (ANTON)”
|
||||||
|
|
||||||
|
**NOTE**: The meeting names have a 5 characters code in parenthesis, such as THEBI and ANTON. These are sample customer codes from Northwind database. If you’d like to use any other customer code, you can select one from link [here](https://services.odata.org/V3/Northwind/Northwind.svc/Customers). Note that the mock transaction data is not provided for every customer; you can view or add the mock data in the [Activity Service Mock](../src/webparts/fieldVisitTab/services/ActivityService/ActivityServiceMock.ts) class.
|
||||||
|
|
||||||
|
## SharePoint Configuration
|
||||||
|
In this section we will walk through configuration of SharePoint site, which will hold documents and photos.
|
||||||
|
|
||||||
|
1. Browse to the SharePoint site collection of the Team configured above
|
||||||
|
* Navigate to Files tab within your Teams’ General channel
|
||||||
|
* Click on ‘Open in SharePoint’
|
||||||
|
* Navigate to root of the document library ‘Documents’
|
||||||
|
2. Add a new Column
|
||||||
|
* Click on ‘Add column’ -> Single line of Text
|
||||||
|
* **Name:** Customer
|
||||||
|
* Click Save
|
||||||
|
3. Upload 3 sample documents / spreadsheets / PDF to Documents
|
||||||
|
4. Update properties of these 3 files with **Customer** attribute as THEBI
|
||||||
|
5. Upload additional 3 documents / spreadsheets / PDF to Documents
|
||||||
|
6. Update properties of these 3 files to ANTON
|
||||||
|
7. Create a new ‘Picture Library’
|
||||||
|
* Click on **Add an app** from the **Settings** menu
|
||||||
|
* Search for **Picture Library**
|
||||||
|
* Name the library **Photos**
|
||||||
|
8. Navigate to newly created **Photos** picture library
|
||||||
|
9. Create two new folders with names **THEBI** and **ANTON**
|
||||||
|
10. Upload 2 sample pictures (.jpg or .png) in each folder
|
||||||
|
|
||||||
|
## 3. Sample Project Updates
|
||||||
|
In this section we will make changes to the sample FieldVisit project
|
||||||
|
1. Download the FieldVisit project from GitHub (link here)
|
||||||
|
2. If you do not already have installed, download and install Visual Studio Code
|
||||||
|
3. Open the folder where you've placed the demo code
|
||||||
|
4. This sample uses Weather and Bing Map API to display Weather and Map within the web part. In order for the weather and map APIs to work, you will need to generate API keys using the links below:
|
||||||
|
* **[Open Weather Maps](https://openweathermap.org/api)**
|
||||||
|
* **[Bing Maps](https://docs.microsoft.com/en-us/bingmaps/getting-started/bing-maps-dev-center-help/getting-a-bing-maps-key)**
|
||||||
|
5. Copy file **constants.sample.ts** in src\webparts\fieldvisittab to **constants.ts**
|
||||||
|
6. Open constants.ts and update keys with your own keys. **NOTE:** These keys are used to retrieve map and weather from Bing. If you do not provide a valid key, maps and weather will not be displayed
|
||||||
|
7. In Visual Studio Code, click on menu **Terminal -> New Terminal**
|
||||||
|
8. Issue command **gulp bundle -–ship**
|
||||||
|
9. Issue command **gulp trust-dev-cert** (**NOTE:** This step needs to be performed only once per development machine)
|
||||||
|
10. Issue command **gulp package-solution -–ship**
|
||||||
|
11. Ensure there are no errors for any of these commands.
|
||||||
|
|
||||||
|
## 4. Deploying Solution in SharePoint
|
||||||
|
|
||||||
|
In this section we will deploy the SPFx solution package into SharePoint App Catalog.
|
||||||
|
|
||||||
|
1. Login as tenant admin in Office 365 and navigate to SharePoint Admin center at https://<tenant>-admin.sharepoint.com/_layouts/15/online/SiteCollections.aspx
|
||||||
|
2. On the left menu bar, click on **apps**
|
||||||
|
3. Click on **App Catalog**
|
||||||
|
4. On the left menu bar, click on **Apps for SharePoint**
|
||||||
|
5. Click on Upload and browse to folder where you have deployed the same code
|
||||||
|
6. Navigate to **FieldVisitDemo\SPFx\sharepoint\solution** and select the **field-visit-demo-tab.spppkg** file
|
||||||
|
7. Click on OK
|
||||||
|
8. In the popup window **check** on **Make this solution available to all sites in organization** and click on **Deploy**
|
||||||
|
9. Navigate to new SharePoint Admin Center at the following link: https://<tenant>-admin.sharepoint.com/_layouts/15/online/AdminHome.aspx
|
||||||
|
10. From the left menu bar, click on **API Management**
|
||||||
|
11. Select the permission for the field visit solution click on the **Approve or Reject** button
|
||||||
|
12. Click **Approve** to approve the permissions
|
||||||
|
|
||||||
|
## 5. Using the Solution in SharePoint
|
||||||
|
In this section we will configure the web part on a SharePoint page.
|
||||||
|
|
||||||
|
1. Navigate to SharePoint site collection configured in section SharePoint Configuration
|
||||||
|
2. Add a new page
|
||||||
|
3. Provide title of the page **Field Visit**
|
||||||
|
4. Add a new web part and select **FieldVisitTab** web part
|
||||||
|
5. Edit the properties of the web part and fill in as following
|
||||||
|
* **Group Name:** Name of the Team where the sample is installed. Refer to Teams Configuration section
|
||||||
|
* **Group/Team ID:** Here you need ID of the Team. Use the [Microsoft Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer) to find the ID of the Team
|
||||||
|
* **Channel ID:** ID of the General channel. Use the [Microsoft Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer) to find the channel ID as well
|
||||||
|
6. Save the web part configuration
|
||||||
|
7. Publish this page
|
||||||
|
8. Navigate to the site collection home page and then navigate back to the page
|
||||||
|
9. You should see 2 appointments. Click on each appointment to see the documents, pictures, map and weather
|
||||||
|
|
||||||
|
## 6. Deploying Solution in Teams
|
||||||
|
|
||||||
|
In this section we will deploy the SPFx FieldVisit TAB web part in Microsoft Teams as a tab. For simplicity sake, we will sideload the solution to a particular Team. Ensure Teams is [configured to allow side loading](https://docs.microsoft.com/en-us/microsoftteams/enable-features-office-365).
|
||||||
|
|
||||||
|
1. Open File Explorer and navigate to **FieldVisitDemo\SPFx\teams** folder
|
||||||
|
2. You will find 3 files – manifest.json, tab20x20.png and tab96x96.png. Select all 3 files and add them to a new compressed zip file
|
||||||
|
3. Name the zip file **FieldVisitTeamsManifest.zip**
|
||||||
|
4. Navigate to Team in which the solution is configured. Refer to Teams Configuration section
|
||||||
|
5. Navigate to **Manage Team -> Apps**
|
||||||
|
6. Click on **Upload a custom app** in the bottom right corner
|
||||||
|
7. Select the Zip file you created, **FieldVisitTeamsManifest.zip**
|
||||||
|
8. Navigate to the **General** channel and click on + to add a new TAB
|
||||||
|
9. Select **FieldVisitTab**
|
||||||
|
10. Click on **Save** in the TAB configuration screen. **NOTE:** We do not need to provide Team name, ID and Channel ID when configuring TAB in Team because the web part retrieves this information from the Teams context.
|
||||||
|
11. Your new FIeldVisit Tab should now be configured showing 2 appointments. Click on each appointment to view further details such as documents, pictures, map and weather
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Many thanks to [Arbindo Chattopadhyay](https://www.linkedin.com/in/arbindoc/) for writing these instructions.
|
|
@ -0,0 +1,7 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const build = require('@microsoft/sp-build-web');
|
||||||
|
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||||
|
|
||||||
|
build.initialize(gulp);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "field-visit-demo-tab",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "gulp bundle",
|
||||||
|
"clean": "gulp clean",
|
||||||
|
"test": "gulp test"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@microsoft/sp-core-library": "1.8.0",
|
||||||
|
"@microsoft/sp-lodash-subset": "1.8.0",
|
||||||
|
"@microsoft/sp-office-ui-fabric-core": "1.8.0",
|
||||||
|
"@microsoft/sp-property-pane": "1.8.0",
|
||||||
|
"@microsoft/sp-webpart-base": "1.8.0",
|
||||||
|
"@microsoft/sp-http": "1.8.0",
|
||||||
|
"@microsoft/teams-js": "^1.4.1",
|
||||||
|
"@types/es6-promise": "0.0.33",
|
||||||
|
"@types/react": "16.4.2",
|
||||||
|
"@types/react-dom": "16.0.5",
|
||||||
|
"@types/webpack-env": "1.13.1",
|
||||||
|
"react": "16.7.0",
|
||||||
|
"react-dom": "16.7.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@microsoft/microsoft-graph-types": "^1.5.0",
|
||||||
|
"@microsoft/sp-build-web": "1.8.0",
|
||||||
|
"@microsoft/sp-module-interfaces": "1.8.0",
|
||||||
|
"@microsoft/sp-tslint-rules": "1.8.0",
|
||||||
|
"@microsoft/sp-webpart-workbench": "1.8.0",
|
||||||
|
"@microsoft/rush-stack-compiler-3.3": "0.1.7",
|
||||||
|
"@types/chai": "3.4.34",
|
||||||
|
"@types/mocha": "2.2.38",
|
||||||
|
"ajv": "~5.2.2",
|
||||||
|
"gulp": "~3.9.1",
|
||||||
|
"@microsoft/rush-stack-compiler-2.7": "0.4.0"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "16.4.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||||
|
"id": "ed686d10-9382-4c3b-be6b-1957d4ec9692",
|
||||||
|
"alias": "FieldVisitTabWebPart",
|
||||||
|
"componentType": "WebPart",
|
||||||
|
|
||||||
|
// The "*" signifies that the version should be taken from the package.json
|
||||||
|
"version": "*",
|
||||||
|
"manifestVersion": 2,
|
||||||
|
|
||||||
|
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||||
|
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||||
|
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||||
|
"requiresCustomScript": false,
|
||||||
|
|
||||||
|
"preconfiguredEntries": [{
|
||||||
|
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||||
|
"group": { "default": "Other" },
|
||||||
|
"title": { "default": "FieldVisitTab" },
|
||||||
|
"description": { "default": "Displays details of upcoming field visits in a Microsoft Teams tab" },
|
||||||
|
"officeFabricIconFontName": "Page",
|
||||||
|
"properties": {
|
||||||
|
"description": "FieldVisitTab"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ReactDom from 'react-dom';
|
||||||
|
import { Version, Environment } from '@microsoft/sp-core-library';
|
||||||
|
import {
|
||||||
|
BaseClientSideWebPart,
|
||||||
|
IPropertyPaneConfiguration,
|
||||||
|
PropertyPaneTextField
|
||||||
|
} from '@microsoft/sp-webpart-base';
|
||||||
|
|
||||||
|
import * as microsoftTeams from '@microsoft/teams-js';
|
||||||
|
import * as strings from 'FieldVisitTabWebPartStrings';
|
||||||
|
import { IFieldVisitsProps, FieldVisits }
|
||||||
|
from './components/FieldVisits';
|
||||||
|
|
||||||
|
import ServiceFactory from './services/ServiceFactory';
|
||||||
|
|
||||||
|
export interface IFieldVisitTabWebPartProps {
|
||||||
|
groupName: string;
|
||||||
|
groupId: string;
|
||||||
|
channelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class FieldVisitTabWebPart extends BaseClientSideWebPart<IFieldVisitTabWebPartProps> {
|
||||||
|
|
||||||
|
private teamsContext?: microsoftTeams.Context;
|
||||||
|
private groupName?: string;
|
||||||
|
private groupId?: string;
|
||||||
|
private channelId?: string;
|
||||||
|
|
||||||
|
protected onInit(): Promise<any> {
|
||||||
|
|
||||||
|
let p: Promise<any> = Promise.resolve();
|
||||||
|
|
||||||
|
if (this.context.microsoftTeams &&
|
||||||
|
this.context.microsoftTeams.getContext) {
|
||||||
|
|
||||||
|
// Get configuration from the Teams SDK
|
||||||
|
p = new Promise((resolve, reject) => {
|
||||||
|
if (this.context.microsoftTeams &&
|
||||||
|
this.context.microsoftTeams.getContext) {
|
||||||
|
this.context.microsoftTeams.getContext(context => {
|
||||||
|
this.teamsContext = context;
|
||||||
|
this.groupName = context.teamName;
|
||||||
|
this.groupId = context.groupId;
|
||||||
|
this.channelId = context.channelId;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Get configuration from web part settings
|
||||||
|
this.groupName = this.properties.groupName;
|
||||||
|
this.groupId = this.properties.groupId;
|
||||||
|
this.channelId = this.properties.channelId;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
|
||||||
|
// Get services needed for the mashup
|
||||||
|
const visitService = ServiceFactory.getVisitService(
|
||||||
|
Environment.type, this.context, this.context.serviceScope
|
||||||
|
);
|
||||||
|
const weatherService = ServiceFactory.getWeatherService(
|
||||||
|
Environment.type, this.context, this.context.serviceScope);
|
||||||
|
|
||||||
|
const mapService = ServiceFactory.getMapService(
|
||||||
|
Environment.type, this.context, this.context.serviceScope);
|
||||||
|
|
||||||
|
const documentService = ServiceFactory.getDocumentService(
|
||||||
|
Environment.type, this.context, this.context.serviceScope
|
||||||
|
);
|
||||||
|
const activityService = ServiceFactory.getActivityService(
|
||||||
|
Environment.type, this.context, this.context.serviceScope
|
||||||
|
);
|
||||||
|
const conversationService = ServiceFactory.getConversationService(
|
||||||
|
Environment.type, this.context, this.context.serviceScope,
|
||||||
|
this.groupId, this.channelId
|
||||||
|
);
|
||||||
|
const photoService = ServiceFactory.getPhotoService(
|
||||||
|
Environment.type, this.context, this.context.serviceScope
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render the mashup
|
||||||
|
const element: React.ReactElement<IFieldVisitsProps> = React.createElement(
|
||||||
|
FieldVisits,
|
||||||
|
{
|
||||||
|
visitService: visitService,
|
||||||
|
weatherService: weatherService,
|
||||||
|
mapService: mapService,
|
||||||
|
documentService: documentService,
|
||||||
|
activityService: activityService,
|
||||||
|
conversationService: conversationService,
|
||||||
|
photoService: photoService,
|
||||||
|
groupName: this.groupName,
|
||||||
|
groupId: this.groupId,
|
||||||
|
channelId: this.channelId,
|
||||||
|
entityId: this.teamsContext ? this.teamsContext.entityId : "",
|
||||||
|
subEntityId: this.teamsContext ? this.teamsContext.subEntityId : "",
|
||||||
|
teamsApplicationId: 'ed686d10-9382-4c3b-be6b-1957d4ec9692',
|
||||||
|
currentUserEmail: this.context.pageContext.user.email
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDom.render(element, this.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDispose(): void {
|
||||||
|
ReactDom.unmountComponentAtNode(this.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get dataVersion(): Version {
|
||||||
|
return Version.parse('1.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
|
return {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
header: {
|
||||||
|
description: strings.PropertyPaneDescription
|
||||||
|
},
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
groupName: strings.BasicGroupName,
|
||||||
|
groupFields: [
|
||||||
|
PropertyPaneTextField('groupName', {
|
||||||
|
label: strings.GroupNameLabel
|
||||||
|
}),
|
||||||
|
PropertyPaneTextField('groupId', {
|
||||||
|
label: strings.GroupIdLabel
|
||||||
|
}),
|
||||||
|
PropertyPaneTextField('channelId', {
|
||||||
|
label: strings.ChannelIdLabel
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { IActivityService } from '../services/ActivityService/IActivityService';
|
||||||
|
import { IActivity } from '../model/IActivity';
|
||||||
|
|
||||||
|
export interface IActivityProps {
|
||||||
|
service: IActivityService;
|
||||||
|
customerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IActivityState {
|
||||||
|
activities?: IActivity[];
|
||||||
|
currentCustomerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Activities extends React.Component<IActivityProps, IActivityState> {
|
||||||
|
|
||||||
|
constructor(props: IActivityProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
activities: undefined,
|
||||||
|
currentCustomerId: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IActivityProps> {
|
||||||
|
|
||||||
|
if (this.props.customerId) {
|
||||||
|
|
||||||
|
if (this.state.currentCustomerId == this.props.customerId) {
|
||||||
|
|
||||||
|
if (this.state.activities && this.state.activities.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className={styles.activities}>
|
||||||
|
|
||||||
|
<div className={styles.activitiesHeadingRow}>
|
||||||
|
<div className={styles.activitiesDateColumn}>
|
||||||
|
Date
|
||||||
|
</div>
|
||||||
|
<div className={styles.activitiesNameColumn}>
|
||||||
|
Activity
|
||||||
|
</div>
|
||||||
|
<div className={styles.activitiesAmountColumn}>
|
||||||
|
Amount
|
||||||
|
</div>
|
||||||
|
<div className={styles.activitiesDescriptionColumn}>
|
||||||
|
Description
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.activities.map(a => (
|
||||||
|
<div className={styles.activitiesRow}>
|
||||||
|
<div className={styles.activitiesDateColumn}>
|
||||||
|
{a.date.toDateString()}
|
||||||
|
</div>
|
||||||
|
<div className={styles.activitiesNameColumn}>
|
||||||
|
{a.name}
|
||||||
|
</div>
|
||||||
|
<div className={styles.activitiesAmountColumn}>
|
||||||
|
{a.amount}
|
||||||
|
</div>
|
||||||
|
<div className={styles.activitiesDescriptionColumn}>
|
||||||
|
{a.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
))}
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return (<div>No documents found</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.props.service.getDocuments(this.props.customerId)
|
||||||
|
.then((activities: IActivity[]) => {
|
||||||
|
this.setState({
|
||||||
|
activities: activities,
|
||||||
|
currentCustomerId: this.props.customerId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<div>Loading</div>);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>No visit selected</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { IVisit } from '../model/IVisit';
|
||||||
|
|
||||||
|
export interface ICompanyInfoProps {
|
||||||
|
visit?: IVisit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CompanyInfo extends React.Component<ICompanyInfoProps, {}> {
|
||||||
|
|
||||||
|
public render(): React.ReactElement<ICompanyInfoProps> {
|
||||||
|
|
||||||
|
if (this.props.visit) {
|
||||||
|
return (
|
||||||
|
<div className={styles.documents}>
|
||||||
|
<div className={styles.documentsHeadingRow}>{ this.props.visit.customer.CompanyName }</div>
|
||||||
|
<div className={styles.documentsRow}>
|
||||||
|
<div>{ this.props.visit.customer.Address }</div>
|
||||||
|
<div>{ this.props.visit.customer.City },
|
||||||
|
{ this.props.visit.customer.Region }
|
||||||
|
{ this.props.visit.customer.PostalCode }</div>
|
||||||
|
<div>{ this.props.visit.customer.Phone }</div>
|
||||||
|
<div>{ this.props.visit.customer.ContactName },
|
||||||
|
{ this.props.visit.customer.ContactTitle }</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { IDocumentService } from '../services/DocumentService/IDocumentService';
|
||||||
|
import { IDocument } from '../model/IDocument';
|
||||||
|
|
||||||
|
export interface IDocumentProps {
|
||||||
|
service: IDocumentService;
|
||||||
|
customerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDocumentState {
|
||||||
|
documents: IDocument[] | null;
|
||||||
|
currentCustomerId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Documents extends React.Component<IDocumentProps, IDocumentState> {
|
||||||
|
|
||||||
|
constructor(props: IDocumentProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
documents: null,
|
||||||
|
currentCustomerId: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IDocumentProps> {
|
||||||
|
|
||||||
|
if (this.props.customerId) {
|
||||||
|
|
||||||
|
if (this.state.currentCustomerId == this.props.customerId) {
|
||||||
|
|
||||||
|
if (this.state.documents && this.state.documents.length > 0) {
|
||||||
|
return (
|
||||||
|
<div className={styles.documents}>
|
||||||
|
|
||||||
|
<div className={styles.documentsHeadingRow}>
|
||||||
|
<div className={styles.documentsNameColumn}>
|
||||||
|
Document
|
||||||
|
</div>
|
||||||
|
<div className={styles.documentsAuthorColumn}>
|
||||||
|
Author
|
||||||
|
</div>
|
||||||
|
<div className={styles.documentsDateColumn}>
|
||||||
|
Modified date
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.documents.map(doc => (
|
||||||
|
<div className={styles.documentsRow}>
|
||||||
|
<div className={styles.documentsNameColumn}>
|
||||||
|
<a href={`${doc.url}?web=1`} target='blank'>{doc.name}</a>
|
||||||
|
</div>
|
||||||
|
<div className={styles.documentsAuthorColumn}>
|
||||||
|
{doc.author}
|
||||||
|
</div>
|
||||||
|
<div className={styles.documentsDateColumn}>
|
||||||
|
{doc.date.toDateString()}
|
||||||
|
{doc.date.getHours() % 12}:
|
||||||
|
{doc.date.getMinutes()<10 ? "0" : ""}
|
||||||
|
{doc.date.getMinutes()}
|
||||||
|
{doc.date.getHours() < 12 ? 'am' : 'pm'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
))}
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return (<div>No documents found</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.props.service.getDocuments(this.props.customerId)
|
||||||
|
.then((docs: IDocument[]) => {
|
||||||
|
this.setState({
|
||||||
|
documents: docs,
|
||||||
|
currentCustomerId: this.props.customerId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<div>Loading</div>);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,281 @@
|
||||||
|
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||||
|
|
||||||
|
.fieldVisits {
|
||||||
|
|
||||||
|
.fieldVisitsRow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldVisitsLeftColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-lg8;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fieldVisitsRightColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-lg4;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.userTabs {
|
||||||
|
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.userTab {
|
||||||
|
display: inline;
|
||||||
|
border: solid;
|
||||||
|
border-width: 1px 1px 0 1px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: x-large;
|
||||||
|
margin: 0 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userTabSelected {
|
||||||
|
background-color: $ms-color-themeLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitList {
|
||||||
|
|
||||||
|
border-top-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
|
||||||
|
.visitListRow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
border-color: black;
|
||||||
|
border-top-style: hidden;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListRow:hover {
|
||||||
|
background-color: $ms-color-neutralLight
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListRowSelected,
|
||||||
|
.visitListRowSelected:hover {
|
||||||
|
background-color: $ms-color-themeLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListDateColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm4;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg2;
|
||||||
|
};
|
||||||
|
|
||||||
|
.visitListDetailColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm8;
|
||||||
|
@include ms-md9;
|
||||||
|
@include ms-lg10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListTime {
|
||||||
|
@include ms-font-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListDate {
|
||||||
|
@include ms-font-m;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListTitle {
|
||||||
|
@include ms-font-l;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListLocation {
|
||||||
|
@include ms-font-s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visitListContact {
|
||||||
|
@include ms-font-s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather {
|
||||||
|
.weatherContainer {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weatherrow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
@include ms-fontColor-white;
|
||||||
|
background-color: $ms-color-themeDark;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weathercolumn1 {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-xl2;
|
||||||
|
@include ms-lg3;
|
||||||
|
@include ms-md5;
|
||||||
|
@include ms-sm12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weathercolumn2 {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-xl10;
|
||||||
|
@include ms-lg9;
|
||||||
|
@include ms-md7;
|
||||||
|
@include ms-sm12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weatherTemp {
|
||||||
|
@include ms-font-xxl;
|
||||||
|
@include ms-fontColor-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.map {
|
||||||
|
.mapImage {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.documents {
|
||||||
|
|
||||||
|
border-top-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.documentsRow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
border-color: $ms-color-neutralLight;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentsHeadingRow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
color: $ms-color-white;
|
||||||
|
background-color: $ms-color-themeDark;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentsRow:hover {
|
||||||
|
background-color: $ms-color-neutralLight
|
||||||
|
}
|
||||||
|
|
||||||
|
.documentsNameColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm8;
|
||||||
|
@include ms-md6;
|
||||||
|
@include ms-lg5;
|
||||||
|
};
|
||||||
|
|
||||||
|
.documentsAuthorColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm1;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg2;
|
||||||
|
};
|
||||||
|
|
||||||
|
.documentsDateColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm4;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg5;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.activities {
|
||||||
|
|
||||||
|
border-top-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.activitiesRow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
border-color: $ms-color-neutralLight;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activitiesHeadingRow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
color: $ms-color-white;
|
||||||
|
background-color: $ms-color-themeDark;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activitiesRow:hover {
|
||||||
|
background-color: $ms-color-neutralLight
|
||||||
|
}
|
||||||
|
|
||||||
|
.activitiesDateColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm4;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg2;
|
||||||
|
};
|
||||||
|
|
||||||
|
.activitiesNameColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm4;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg3;
|
||||||
|
};
|
||||||
|
|
||||||
|
.activitiesAmountColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm4;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg2;
|
||||||
|
};
|
||||||
|
|
||||||
|
.activitiesDescriptionColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm12;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg5;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.postToChannel {
|
||||||
|
|
||||||
|
margin: 5px;
|
||||||
|
@include ms-Grid;
|
||||||
|
|
||||||
|
.postToChannelRow {
|
||||||
|
@include ms-Grid-row;
|
||||||
|
border-color: $ms-color-neutralLight;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postToChannelTextColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm9;
|
||||||
|
@include ms-md9;
|
||||||
|
@include ms-lg9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postToChannelButtonColumn {
|
||||||
|
@include ms-Grid-col;
|
||||||
|
@include ms-sm3;
|
||||||
|
@include ms-md3;
|
||||||
|
@include ms-lg3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postToChannelTextArea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postToChannelButton {
|
||||||
|
width: 50px;
|
||||||
|
padding: 2px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
import { IVisitService } from '../services/VisitService/IVisitService';
|
||||||
|
import { IWeatherService } from '../services/WeatherService/IWeatherService';
|
||||||
|
import { IMapService } from '../services/MapService/IMapService';
|
||||||
|
import { IDocumentService } from '../services/DocumentService/IDocumentService';
|
||||||
|
import { IActivityService } from '../services/ActivityService/IActivityService';
|
||||||
|
import { IConversationService } from '../services/ConversationService/IConversationService';
|
||||||
|
import { IPhotoService } from '../services/PhotoService/IPhotoService';
|
||||||
|
|
||||||
|
import { IVisit } from '../model/IVisit';
|
||||||
|
import { IUser } from '../model/IUser';
|
||||||
|
|
||||||
|
import { UserTabs } from './UserTabs';
|
||||||
|
import { VisitList } from './VisitList';
|
||||||
|
import { CompanyInfo } from './CompanyInfo';
|
||||||
|
import { Weather } from './Weather';
|
||||||
|
import { Map } from './Map';
|
||||||
|
import { Documents } from './Documents';
|
||||||
|
import { Activities } from './Activities';
|
||||||
|
import { PostToChannel } from './PostToChannel';
|
||||||
|
import { Photos } from './Photos';
|
||||||
|
|
||||||
|
export interface IFieldVisitsProps {
|
||||||
|
visitService: IVisitService;
|
||||||
|
weatherService: IWeatherService;
|
||||||
|
mapService: IMapService;
|
||||||
|
documentService: IDocumentService;
|
||||||
|
activityService: IActivityService;
|
||||||
|
conversationService: IConversationService;
|
||||||
|
photoService: IPhotoService;
|
||||||
|
currentUserEmail: string;
|
||||||
|
groupName?: string;
|
||||||
|
groupId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
teamsApplicationId: string;
|
||||||
|
entityId: string;
|
||||||
|
subEntityId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFieldVisitsState {
|
||||||
|
dataFetched?: boolean;
|
||||||
|
users?: IUser[];
|
||||||
|
allVisits?: IVisit[];
|
||||||
|
filteredVisits?: IVisit[];
|
||||||
|
selectedVisit?: IVisit;
|
||||||
|
subEntityId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldVisits extends React.Component<IFieldVisitsProps, IFieldVisitsState> {
|
||||||
|
|
||||||
|
constructor(props: IFieldVisitsProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
dataFetched: false,
|
||||||
|
users: [],
|
||||||
|
allVisits: [],
|
||||||
|
filteredVisits: [],
|
||||||
|
selectedVisit: undefined, // NOTE If defined, selectedVisit should reference a member of visits[]
|
||||||
|
subEntityId: props.subEntityId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IFieldVisitsProps> {
|
||||||
|
|
||||||
|
if (!this.state.dataFetched) {
|
||||||
|
this.props.visitService.getGroupVisits(this.props.groupId, this.props.groupName)
|
||||||
|
.then((visits) => {
|
||||||
|
var u = this.getUsersFromVisits(visits);
|
||||||
|
var fv = this.filterVisitsBySelectedUsers(visits, u);
|
||||||
|
this.setState({
|
||||||
|
users: u,
|
||||||
|
allVisits: visits,
|
||||||
|
filteredVisits: fv,
|
||||||
|
selectedVisit: undefined,
|
||||||
|
dataFetched: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.dataFetched) {
|
||||||
|
|
||||||
|
// Unpack data
|
||||||
|
let address: string = "";
|
||||||
|
let city: string = "";
|
||||||
|
let state: string = "";
|
||||||
|
let country: string = "";
|
||||||
|
let postalCode: string = "";
|
||||||
|
let customerId: string = "";
|
||||||
|
let customerName: string = "";
|
||||||
|
if (this.state.selectedVisit && this.state.selectedVisit.customer) {
|
||||||
|
address = this.state.selectedVisit.customer.Address;
|
||||||
|
city = this.state.selectedVisit.customer.City;
|
||||||
|
state = this.state.selectedVisit.customer.Region;
|
||||||
|
country = this.state.selectedVisit.customer.Country;
|
||||||
|
postalCode = this.state.selectedVisit.customer.PostalCode;
|
||||||
|
customerId = this.state.selectedVisit.customer.CustomerID;
|
||||||
|
customerName = this.state.selectedVisit.customer.CompanyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deep link, if any
|
||||||
|
let userChanged = false;
|
||||||
|
if (this.state.subEntityId) {
|
||||||
|
let [deeplinkUser, deeplinkCustomerId] = this.props.subEntityId ?
|
||||||
|
this.props.subEntityId.split(':') : ["", ""];
|
||||||
|
|
||||||
|
if (this.state.users) {
|
||||||
|
this.state.users.forEach(user => {
|
||||||
|
if (user.email == deeplinkUser && !user.isSelected) {
|
||||||
|
userChanged = true;
|
||||||
|
this.handleUserSelectionChanged(user);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userChanged) {
|
||||||
|
if (this.state.filteredVisits) {
|
||||||
|
this.state.filteredVisits.forEach(visit => {
|
||||||
|
if (visit.customer.CustomerID == deeplinkCustomerId) {
|
||||||
|
this.handleVisitSelectionChanged(visit);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
subEntityId: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get currently selected user
|
||||||
|
let selectedUser = "";
|
||||||
|
if (this.state.users) {
|
||||||
|
this.state.users.forEach((user) => { if (user.isSelected) { selectedUser = user.email; } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className={styles.fieldVisits}>
|
||||||
|
<div className={styles.fieldVisitsRow}>
|
||||||
|
<div className={styles.fieldVisitsLeftColumn}>
|
||||||
|
<UserTabs users={this.state.users}
|
||||||
|
userSelectionChanged={this.handleUserSelectionChanged.bind(this)}
|
||||||
|
/>
|
||||||
|
<VisitList visits={this.state.filteredVisits}
|
||||||
|
selectedVisit={this.state.selectedVisit}
|
||||||
|
visitSelectionChanged={this.handleVisitSelectionChanged.bind(this)}
|
||||||
|
/>
|
||||||
|
<Activities service={this.props.activityService}
|
||||||
|
customerId={customerId} />
|
||||||
|
<Documents service={this.props.documentService}
|
||||||
|
customerId={customerId} />
|
||||||
|
<Photos service={this.props.photoService}
|
||||||
|
customerId={customerId} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.fieldVisitsRightColumn}>
|
||||||
|
<Weather service={this.props.weatherService}
|
||||||
|
country={country} postalCode={postalCode} />
|
||||||
|
<CompanyInfo visit={this.state.selectedVisit} />
|
||||||
|
<PostToChannel channelId={this.props.channelId}
|
||||||
|
entityId={this.props.entityId}
|
||||||
|
teamsApplicationId={this.props.teamsApplicationId}
|
||||||
|
customerId={customerId}
|
||||||
|
customerName={customerName}
|
||||||
|
selectedUser={selectedUser}
|
||||||
|
address={address}
|
||||||
|
city={city}
|
||||||
|
state={state}
|
||||||
|
country={country}
|
||||||
|
postalCode={postalCode}
|
||||||
|
conversationService={this.props.conversationService}
|
||||||
|
mapService={this.props.mapService} />
|
||||||
|
<Map service={this.props.mapService}
|
||||||
|
address={address} city={city} state={state}
|
||||||
|
country={country} postalCode={postalCode} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (<div>Loading...</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleUserSelectionChanged(user: IUser) {
|
||||||
|
var oldUsers = this.state.users;
|
||||||
|
var newUsers: IUser[] = [];
|
||||||
|
// ** use this code to allow only one user to be selected **
|
||||||
|
if (oldUsers) {
|
||||||
|
oldUsers.forEach((u) => {
|
||||||
|
let newUser = u;
|
||||||
|
newUser.isSelected = u.email == user.email;
|
||||||
|
newUsers.push(newUser);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ** use this code to allow multuple users to be selected **
|
||||||
|
// oldUsers.forEach((u) => {
|
||||||
|
// let newUser = u;
|
||||||
|
// if (u.email == user.email) {
|
||||||
|
// newUser.isSelected = !u.isSelected;
|
||||||
|
// }
|
||||||
|
// newUsers.push(newUser);
|
||||||
|
// });
|
||||||
|
var fv = this.filterVisitsBySelectedUsers(this.state.allVisits, newUsers);
|
||||||
|
var sv = fv.filter((v) => (v == this.state.selectedVisit)).length > 0 ?
|
||||||
|
this.state.selectedVisit : undefined;
|
||||||
|
this.setState({
|
||||||
|
users: newUsers,
|
||||||
|
filteredVisits: fv,
|
||||||
|
selectedVisit: sv
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleVisitSelectionChanged(visit: IVisit) {
|
||||||
|
this.setState({
|
||||||
|
selectedVisit: visit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterVisitsBySelectedUsers(visits: IVisit[] | undefined, users: IUser[]): IVisit[] {
|
||||||
|
var result: IVisit[] = [];
|
||||||
|
|
||||||
|
if (visits) {
|
||||||
|
visits.forEach((visit) => {
|
||||||
|
let showVisit = false;
|
||||||
|
visit.calendarItem.Attendees.forEach((attendee) => {
|
||||||
|
if (users.filter((u) => (u.isSelected && u.email == attendee.email)).length > 0) {
|
||||||
|
showVisit = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (showVisit) {
|
||||||
|
result.push(visit);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUsersFromVisits(visits: IVisit[]) {
|
||||||
|
|
||||||
|
var result: IUser[] = [];
|
||||||
|
visits.forEach((visit) => {
|
||||||
|
if (visit.calendarItem.Attendees) {
|
||||||
|
visit.calendarItem.Attendees.forEach((attendee) => {
|
||||||
|
if ((attendee.email != "?? GROUP EMAIL ??") &&
|
||||||
|
(result.filter((i: IUser) => (i.email == attendee.email)).length == 0)) {
|
||||||
|
result.push({
|
||||||
|
fullName: attendee.fullName,
|
||||||
|
email: attendee.email,
|
||||||
|
isSelected: attendee.email == this.props.currentUserEmail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.sort((a, b) => (a.fullName < b.fullName ? -1 : 1));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { IMapService } from '../services/MapService/IMapService';
|
||||||
|
|
||||||
|
export interface IMapProps {
|
||||||
|
service: IMapService;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
country: string;
|
||||||
|
postalCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapState {
|
||||||
|
locationSignature?: string;
|
||||||
|
mapUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Map extends React.Component<IMapProps, IMapState> {
|
||||||
|
|
||||||
|
constructor(props: IMapProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
locationSignature: undefined,
|
||||||
|
mapUrl: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IMapProps> {
|
||||||
|
|
||||||
|
if (this.props.country &&
|
||||||
|
this.props.country.toLowerCase() == "usa" &&
|
||||||
|
this.props.postalCode) {
|
||||||
|
|
||||||
|
const locationSignature = this.getLocationSignature(
|
||||||
|
this.props.address, this.props.city,
|
||||||
|
this.props.state, this.props.country,
|
||||||
|
this.props.postalCode);
|
||||||
|
|
||||||
|
if (locationSignature === this.state.locationSignature) {
|
||||||
|
if (this.state.mapUrl !== '#') {
|
||||||
|
return (
|
||||||
|
<div className={styles.map}>
|
||||||
|
<img className={styles.mapImage}
|
||||||
|
src={this.state.mapUrl} />
|
||||||
|
<br />{`Map at ${this.props.address}, ${this.props.city}, ${this.props.state}`}
|
||||||
|
</div>);
|
||||||
|
} else {
|
||||||
|
return (<div>Map not found</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.props.service.getMapImageUrl(this.props.address,
|
||||||
|
this.props.city, this.props.state, this.props.country,
|
||||||
|
this.props.postalCode)
|
||||||
|
|
||||||
|
.then((mapUrl) => {
|
||||||
|
this.setState({
|
||||||
|
locationSignature: locationSignature,
|
||||||
|
mapUrl: mapUrl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<div>Loading</div>);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLocationSignature(address: string, city: string, state: string,
|
||||||
|
country: string, postalCode: string) {
|
||||||
|
return `${address}**${city}**${state}**${country}**${postalCode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IPhoto } from '../model/IPhoto';
|
||||||
|
|
||||||
|
import { IPhotoService } from '../services/PhotoService/IPhotoService';
|
||||||
|
|
||||||
|
export interface IPhotosProps {
|
||||||
|
customerId: string;
|
||||||
|
service: IPhotoService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPhotosState {
|
||||||
|
photos?: IPhoto[];
|
||||||
|
currentCustomerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Photos extends React.Component<IPhotosProps, IPhotosState> {
|
||||||
|
|
||||||
|
constructor(props: IPhotosProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
photos: undefined,
|
||||||
|
currentCustomerId: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IPhotosProps> {
|
||||||
|
|
||||||
|
if (this.props.customerId) {
|
||||||
|
|
||||||
|
if (this.state.currentCustomerId == this.props.customerId) {
|
||||||
|
|
||||||
|
if (this.state.photos && this.state.photos.length > 0) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.state.photos.map(photo => (
|
||||||
|
<div>
|
||||||
|
<img src={photo.url} width='200px' /><br />
|
||||||
|
{photo.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return (<div>No photos for this property</div>);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
this.props.service.getPhotos(this.props.customerId)
|
||||||
|
.then ((photos) => {
|
||||||
|
this.setState({
|
||||||
|
photos: photos,
|
||||||
|
currentCustomerId: this.props.customerId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<div>Loading...</div>);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (<div></div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { ContentType } from '../model/IConversation';
|
||||||
|
import { IConversationService } from '../services/ConversationService/IConversationService';
|
||||||
|
import { IMapService } from '../services/MapService/IMapService';
|
||||||
|
|
||||||
|
export interface IPostToChannelProps {
|
||||||
|
channelId?: string;
|
||||||
|
entityId: string;
|
||||||
|
teamsApplicationId: string;
|
||||||
|
selectedUser: string;
|
||||||
|
customerId: string;
|
||||||
|
customerName: string;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
country: string;
|
||||||
|
postalCode: string;
|
||||||
|
conversationService: IConversationService;
|
||||||
|
mapService: IMapService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPostToChannelState {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PostToChannel extends React.Component<IPostToChannelProps, IPostToChannelState> {
|
||||||
|
|
||||||
|
constructor(props: IPostToChannelProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = { value: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IPostToChannelProps> {
|
||||||
|
|
||||||
|
if (this.props.customerId && this.props.customerName) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.postToChannel}>
|
||||||
|
<div className={styles.postToChannelRow}>
|
||||||
|
<div className={styles.postToChannelTextColumn}>
|
||||||
|
<textarea className={styles.postToChannelTextArea}
|
||||||
|
onChange={this.handleChange.bind(this)}
|
||||||
|
value={this.state.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.postToChannelButtonColumn}>
|
||||||
|
<input type='button' value='Send'
|
||||||
|
onClick={this.handleClick.bind(this)}
|
||||||
|
className={styles.postToChannelButton}
|
||||||
|
{...this.props.entityId ? '' : 'disabled'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (<div />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleChange(event) {
|
||||||
|
this.setState({ value: event.target.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempting to post the map - getting an undefined URL
|
||||||
|
// SO it's commented out for now...
|
||||||
|
private handleClick(event) {
|
||||||
|
|
||||||
|
// Build a deep link to the current user tab and customer
|
||||||
|
const url = encodeURI(
|
||||||
|
'https://teams.microsoft.com/l/entity/' +
|
||||||
|
this.props.teamsApplicationId + '/' +
|
||||||
|
this.props.entityId +
|
||||||
|
'?label=Vi32&' +
|
||||||
|
'context={"subEntityId": "' +
|
||||||
|
this.props.selectedUser + ':' +
|
||||||
|
this.props.customerId +
|
||||||
|
'", "channelId": "' + this.props.channelId + '"}');
|
||||||
|
|
||||||
|
var message =
|
||||||
|
`
|
||||||
|
<div style="border-style:solid; border-width:1px; padding:10px;">
|
||||||
|
<div>${this.state.value}</div>
|
||||||
|
<hr />
|
||||||
|
<div style="background: #eaeaff; font-weight: bold ">
|
||||||
|
<a href="${url}">${this.props.customerName}</a>
|
||||||
|
</div>
|
||||||
|
${this.props.address}<br />
|
||||||
|
${this.props.city}, ${this.props.state} ${this.props.postalCode}<br />
|
||||||
|
</div><br />
|
||||||
|
`
|
||||||
|
;
|
||||||
|
|
||||||
|
this.props.conversationService
|
||||||
|
.createChatThread(message, ContentType.html)
|
||||||
|
.then(() => {
|
||||||
|
this.setState({ value: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { IUser } from '../model/IUser';
|
||||||
|
|
||||||
|
export interface IUserTabsProps {
|
||||||
|
users?: IUser[];
|
||||||
|
userSelectionChanged: (IUser) => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserTabs extends React.Component<IUserTabsProps, {}> {
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IUserTabsProps> {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={styles.userTabs}>
|
||||||
|
{this.props.users? this.props.users.map(user => (
|
||||||
|
<li className={user.isSelected ? styles.userTab + " " + styles.userTabSelected :
|
||||||
|
styles.userTab}
|
||||||
|
onClick={ () => { this.props.userSelectionChanged(user); }} >
|
||||||
|
{user.fullName}
|
||||||
|
</li>
|
||||||
|
)) : <li></li> }
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { IVisit } from '../model/IVisit';
|
||||||
|
|
||||||
|
export interface IVisitListProps {
|
||||||
|
visits?: IVisit[];
|
||||||
|
selectedVisit?: IVisit;
|
||||||
|
visitSelectionChanged: (IVisit) => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VisitList extends React.Component<IVisitListProps, {}> {
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IVisitListProps> {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.visitList}>
|
||||||
|
{this.props.visits ? this.props.visits.map(item => (
|
||||||
|
<div className={ (item == this.props.selectedVisit) ?
|
||||||
|
styles.visitListRow + ' ' + styles.visitListRowSelected : styles.visitListRow }
|
||||||
|
onClick={ () => { this.props.visitSelectionChanged(item); }}
|
||||||
|
>
|
||||||
|
<div className={styles.visitListDateColumn}>
|
||||||
|
<div className={styles.visitListTime}>
|
||||||
|
{item.calendarItem.DateTime.getHours() % 12}:
|
||||||
|
{item.calendarItem.DateTime.getMinutes()<10 ? "0" : ""}
|
||||||
|
{item.calendarItem.DateTime.getMinutes()}
|
||||||
|
{item.calendarItem.DateTime.getHours() < 12 ? 'am' : 'pm'}
|
||||||
|
</div>
|
||||||
|
<div className={styles.visitListDate}>
|
||||||
|
{item.calendarItem.DateTime.toDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.visitListDetailColumn}>
|
||||||
|
<div className={styles.visitListTitle}>{item.calendarItem.Title}</div>
|
||||||
|
<div className={styles.visitListContact}>
|
||||||
|
{item.customer.CompanyName}
|
||||||
|
({item.customer.ContactName})
|
||||||
|
</div>
|
||||||
|
<div className={styles.visitListLocation}>
|
||||||
|
{item.customer.Address},
|
||||||
|
{item.customer.City},
|
||||||
|
{item.customer.Region}
|
||||||
|
{item.customer.Country}
|
||||||
|
{item.customer.PostalCode}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)) : <div></div> }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './FieldVisits.module.scss';
|
||||||
|
|
||||||
|
import { IWeatherService } from '../services/WeatherService/IWeatherService';
|
||||||
|
import { IWeatherConditions } from '../model/IWeatherConditions';
|
||||||
|
|
||||||
|
export interface IWeatherProps {
|
||||||
|
service: IWeatherService;
|
||||||
|
country: string;
|
||||||
|
postalCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWeatherState {
|
||||||
|
conditions?: IWeatherConditions;
|
||||||
|
locationSignature?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Weather extends React.Component<IWeatherProps, IWeatherState> {
|
||||||
|
|
||||||
|
constructor(props: IWeatherProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
conditions: undefined,
|
||||||
|
locationSignature: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IWeatherProps> {
|
||||||
|
|
||||||
|
if (this.props.country &&
|
||||||
|
this.props.country.toLowerCase() == "usa" &&
|
||||||
|
this.props.postalCode) {
|
||||||
|
|
||||||
|
const locationSignature = this.getLocationSignature(
|
||||||
|
this.props.country, this.props.postalCode);
|
||||||
|
|
||||||
|
if (this.state.conditions &&
|
||||||
|
this.state.locationSignature === locationSignature ) {
|
||||||
|
|
||||||
|
const c = this.state.conditions;
|
||||||
|
const tempC = c.main.temp-273;
|
||||||
|
const tempF = Math.round(9/5*tempC+32);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.weather}>
|
||||||
|
<div className={styles.weatherContainer}>
|
||||||
|
<div className={styles.weatherrow}>
|
||||||
|
Weather conditions in {c.name}<hr />
|
||||||
|
<div className={styles.weathercolumn1 + ' ' + styles.weatherTemp}>
|
||||||
|
{tempF}° F<br />
|
||||||
|
<img src={`http://openweathermap.org/img/w/${c.weather[0].icon}.png`} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.weathercolumn2}>
|
||||||
|
{`${c.weather[0].main}`}<br />
|
||||||
|
{`Barometric pressure ${c.main.pressure}`}<br />
|
||||||
|
{`Humidity ${c.main.humidity}%`}<br />
|
||||||
|
{`Wind at ${c.wind.speed} MPH`}<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
} else {
|
||||||
|
this.props.service.getConditions(this.props.postalCode)
|
||||||
|
.then((conditions: IWeatherConditions) => {
|
||||||
|
this.setState({
|
||||||
|
conditions: conditions,
|
||||||
|
locationSignature: locationSignature
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<div>Loading</div>);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLocationSignature(country: string, postalCode: string) {
|
||||||
|
return `${country}**${postalCode}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const owmApiKey: string = "";
|
||||||
|
export const mapApiKey: string = "";
|
||||||
|
|
9
samples/react-teams-tab-field-visit-mashup/src/webparts/fieldVisitTab/loc/en-us.js
vendored
Normal file
9
samples/react-teams-tab-field-visit-mashup/src/webparts/fieldVisitTab/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
define([], function() {
|
||||||
|
return {
|
||||||
|
"PropertyPaneDescription": "Web part to display team calendar and field visit details",
|
||||||
|
"BasicGroupName": "These settings are automatic in a Teams tab",
|
||||||
|
"GroupNameLabel": "Group Name",
|
||||||
|
"GroupIdLabel": "Group/Team ID",
|
||||||
|
"ChannelIdLabel": "Channel ID"
|
||||||
|
}
|
||||||
|
});
|
12
samples/react-teams-tab-field-visit-mashup/src/webparts/fieldVisitTab/loc/mystrings.d.ts
vendored
Normal file
12
samples/react-teams-tab-field-visit-mashup/src/webparts/fieldVisitTab/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
declare interface IFieldVisitTabWebPartStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
BasicGroupName: string;
|
||||||
|
GroupNameLabel: string;
|
||||||
|
GroupIdLabel: string;
|
||||||
|
ChannelIdLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'FieldVisitTabWebPartStrings' {
|
||||||
|
const strings: IFieldVisitTabWebPartStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface IActivity {
|
||||||
|
customerId: string;
|
||||||
|
date: Date;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IUser } from './IUser';
|
||||||
|
|
||||||
|
export interface ICalendarItem {
|
||||||
|
Title?: string;
|
||||||
|
DateTime: Date;
|
||||||
|
Attendees: IUser[];
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// For use with this API:
|
||||||
|
// https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/channel_post_chatthreads
|
||||||
|
// TODO: Replace with something more official
|
||||||
|
|
||||||
|
export const enum ContentType {
|
||||||
|
text = 0,
|
||||||
|
html = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChatMessage {
|
||||||
|
body: {
|
||||||
|
contentType: ContentType;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INewChatThread {
|
||||||
|
rootMessage: IChatMessage;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export interface ICustomer {
|
||||||
|
CustomerID: string;
|
||||||
|
CompanyName: string;
|
||||||
|
ContactName: string;
|
||||||
|
ContactTitle: string;
|
||||||
|
Address: string;
|
||||||
|
City: string;
|
||||||
|
Region: string;
|
||||||
|
PostalCode: string;
|
||||||
|
Country: string;
|
||||||
|
Phone: string;
|
||||||
|
Fax: string;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface IDocument {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
author: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
export interface IPoint {
|
||||||
|
type: string;
|
||||||
|
coordinates: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAddress {
|
||||||
|
addressLine: string;
|
||||||
|
adminDistrict: string;
|
||||||
|
adminDistrict2: string;
|
||||||
|
countryRegion: string;
|
||||||
|
formattedAddress: string;
|
||||||
|
locality: string;
|
||||||
|
postalCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGeocodePoint {
|
||||||
|
type: string;
|
||||||
|
coordinates: number[];
|
||||||
|
calculationMethod: string;
|
||||||
|
usageTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResource {
|
||||||
|
__type: string;
|
||||||
|
bbox: number[];
|
||||||
|
name: string;
|
||||||
|
point: IPoint;
|
||||||
|
address: IAddress;
|
||||||
|
confidence: string;
|
||||||
|
entityType: string;
|
||||||
|
geocodePoints: IGeocodePoint[];
|
||||||
|
matchCodes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResourceSet {
|
||||||
|
estimatedTotal: number;
|
||||||
|
resources: IResource[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapLocation {
|
||||||
|
authenticationResultCode: string;
|
||||||
|
brandLogoUri: string;
|
||||||
|
copyright: string;
|
||||||
|
resourceSets: IResourceSet[];
|
||||||
|
statusCode: number;
|
||||||
|
statusDescription: string;
|
||||||
|
traceId: string;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface IPhoto {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface IUser {
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
isSelected? : boolean;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ICalendarItem } from './ICalendarItem';
|
||||||
|
import { ICustomer } from './ICustomer';
|
||||||
|
|
||||||
|
export interface IVisit {
|
||||||
|
calendarItem: ICalendarItem;
|
||||||
|
customer: ICustomer;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
export interface IWeatherCoord {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWeather {
|
||||||
|
id: number;
|
||||||
|
main: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGauges {
|
||||||
|
temp: number;
|
||||||
|
pressure: number;
|
||||||
|
humidity: number;
|
||||||
|
temp_min: number;
|
||||||
|
temp_max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWind {
|
||||||
|
speed: number;
|
||||||
|
deg: number;
|
||||||
|
gust: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IClouds {
|
||||||
|
all: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISys {
|
||||||
|
type: number;
|
||||||
|
id: number;
|
||||||
|
message: number;
|
||||||
|
country: string;
|
||||||
|
sunrise: number;
|
||||||
|
sunset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWeatherConditions {
|
||||||
|
coord: IWeatherCoord;
|
||||||
|
weather: IWeather[];
|
||||||
|
base: string;
|
||||||
|
main: IGauges;
|
||||||
|
visibility: number;
|
||||||
|
wind: IWind;
|
||||||
|
clouds: IClouds;
|
||||||
|
dt: number;
|
||||||
|
sys: ISys;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
cod: number;
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { IActivity } from '../../model/IActivity';
|
||||||
|
import { IActivityService } from './IActivityService';
|
||||||
|
|
||||||
|
export default class ActivityServiceMock implements IActivityService {
|
||||||
|
|
||||||
|
// US customers from Northwind database
|
||||||
|
private mockItems: IActivity[] = [
|
||||||
|
{
|
||||||
|
customerId: 'THEBI',
|
||||||
|
date: new Date(2018, 6, 21),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 3253.22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'GREAL',
|
||||||
|
date: new Date(2018, 6, 17),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 244.62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'HUNGC',
|
||||||
|
date: new Date(2018, 6, 30),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 1644.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'LAZYK',
|
||||||
|
date: new Date(2018, 6, 15),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 4033.75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'LETSS',
|
||||||
|
date: new Date(2018, 6, 8),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 516.70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'LONEP',
|
||||||
|
date: new Date(2018, 6, 25),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 2050.39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'OLDWO',
|
||||||
|
date: new Date(2018, 6, 11),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 1677.06
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'RATTC',
|
||||||
|
date: new Date(2018, 6, 20),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Partial payment',
|
||||||
|
amount: 1400.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'SAVEA',
|
||||||
|
date: new Date(2018, 6, 27),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 2555.51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'SPLIR',
|
||||||
|
date: new Date(2018, 6, 29),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 877.11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'THECR',
|
||||||
|
date: new Date(2018, 6, 21),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 1205.63
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'TRAIH',
|
||||||
|
date: new Date(2018, 6, 18),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 4020.55
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'WHITC',
|
||||||
|
date: new Date(2018, 6, 24),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 309.22
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
customerId: 'THEBI',
|
||||||
|
date: new Date(2018, 7, 21),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 3253.22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'GREAL',
|
||||||
|
date: new Date(2018, 7, 17),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 244.62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'HUNGC',
|
||||||
|
date: new Date(2018, 7, 30),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 1644.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'LAZYK',
|
||||||
|
date: new Date(2018, 7, 15),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 4033.75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'LETSS',
|
||||||
|
date: new Date(2018, 7, 8),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 516.70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'LONEP',
|
||||||
|
date: new Date(2018, 7, 25),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 2050.39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'OLDWO',
|
||||||
|
date: new Date(2018, 7, 11),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 1677.06
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'RATTC',
|
||||||
|
date: new Date(2018, 7, 20),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Partial payment',
|
||||||
|
amount: 1400.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'SAVEA',
|
||||||
|
date: new Date(2018, 7, 27),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 2555.51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'SPLIR',
|
||||||
|
date: new Date(2018, 7, 29),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 877.11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'THECR',
|
||||||
|
date: new Date(2018, 7, 21),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 1205.63
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'TRAIH',
|
||||||
|
date: new Date(2018, 7, 18),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 4020.55
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'WHITC',
|
||||||
|
date: new Date(2018, 7, 24),
|
||||||
|
name: 'Payment',
|
||||||
|
description: 'Paid in full',
|
||||||
|
amount: 309.22
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
customerId: 'LAZYK',
|
||||||
|
date: new Date(2018, 7, 29),
|
||||||
|
name: 'Claim',
|
||||||
|
description: 'Fire damage',
|
||||||
|
amount: 30000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customerId: 'THEBI',
|
||||||
|
date: new Date(2018, 8, 1),
|
||||||
|
name: 'Policy expiration',
|
||||||
|
description: 'First warning',
|
||||||
|
amount: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
public getDocuments (customerId: string):
|
||||||
|
Promise<IActivity[]> {
|
||||||
|
|
||||||
|
var result: IActivity[] = [];
|
||||||
|
result = this.mockItems.filter(a => a.customerId == customerId);
|
||||||
|
|
||||||
|
return new Promise<IActivity[]>((resolve) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IActivity } from '../../model/IActivity';
|
||||||
|
|
||||||
|
// US only for now
|
||||||
|
export interface IActivityService {
|
||||||
|
getDocuments (customerId: string):
|
||||||
|
Promise<IActivity[]>;
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { ICalendarService } from './ICalendarService';
|
||||||
|
import { ICalendarItem } from '../../model/ICalendarItem';
|
||||||
|
import { IUser } from '../../model/IUser';
|
||||||
|
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { MSGraphClient } from '@microsoft/sp-http';
|
||||||
|
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||||
|
|
||||||
|
interface CalendarView { value: MicrosoftGraph.Event[]; }
|
||||||
|
|
||||||
|
export default class CalendarService implements ICalendarService {
|
||||||
|
|
||||||
|
private context: WebPartContext;
|
||||||
|
constructor(context: WebPartContext, serviceScope: ServiceScope) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGroupCalendarItems(groupId?: string, groupName?: string) {
|
||||||
|
|
||||||
|
if (groupId && groupName) {
|
||||||
|
var result = new Promise<ICalendarItem[]>((resolve, reject) => {
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const startDateTime = this.formatDateForRest(new Date(now));
|
||||||
|
const endDateTime = this.formatDateForRest(new Date(now + 7 * 24 * 60 * 60 * 1000));
|
||||||
|
|
||||||
|
this.context.msGraphClientFactory
|
||||||
|
.getClient()
|
||||||
|
.then((graphClient: MSGraphClient): void => {
|
||||||
|
graphClient.api(`/groups/${groupId}/calendarview?startdatetime=${startDateTime}&enddatetime=${endDateTime}`)
|
||||||
|
.get((error, data: CalendarView, rawResponse?: any) => {
|
||||||
|
|
||||||
|
let calendarItems: ICalendarItem[] = [];
|
||||||
|
data.value.forEach((event) => {
|
||||||
|
|
||||||
|
let attendees: IUser[] = [];
|
||||||
|
if (event.attendees) {
|
||||||
|
|
||||||
|
event.attendees.forEach((user) => {
|
||||||
|
if (user.emailAddress &&
|
||||||
|
user.emailAddress.name &&
|
||||||
|
user.emailAddress.address &&
|
||||||
|
user.emailAddress.name.toLowerCase() != groupName.toLowerCase()) {
|
||||||
|
attendees.push({
|
||||||
|
fullName: user.emailAddress.name,
|
||||||
|
email: user.emailAddress.address
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (event.start && event.start.dateTime) {
|
||||||
|
calendarItems.push({
|
||||||
|
Title: event.subject,
|
||||||
|
DateTime: new Date(event.start.dateTime),
|
||||||
|
Attendees: attendees
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
resolve(calendarItems);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// If here, either group id or name were blank
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDateForRest() - The O365 REST API wants ISO format with the milliseconds not present,
|
||||||
|
// in UTC. This function will return the correctly formatted time for midnight, local time, on
|
||||||
|
// the date specified. Example of a correctly formatted time: 2015-09-06T00:00:00Z
|
||||||
|
private formatDateForRest(date) {
|
||||||
|
|
||||||
|
var midnightLocalTime = date;
|
||||||
|
midnightLocalTime.setHours(0, 0, 0, 0);
|
||||||
|
var utcDate = new Date(midnightLocalTime.getTime() + midnightLocalTime.getTimezoneOffset() * 60 * 1000);
|
||||||
|
return utcDate.getFullYear() + "-" +
|
||||||
|
('0' + (utcDate.getMonth() + 1)).substr(-2) + "-" +
|
||||||
|
('0' + utcDate.getDate()).substr(-2) + "T" +
|
||||||
|
('0' + utcDate.getHours()).substr(-2) + ":00:00Z";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ICalendarService } from './ICalendarService';
|
||||||
|
import { ICalendarItem } from '../../model/ICalendarItem';
|
||||||
|
|
||||||
|
export default class CalendarServiceMock implements ICalendarService {
|
||||||
|
|
||||||
|
private mockItems: ICalendarItem[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
Title: "Damage assessment: Lonseome Pine follow-up (LONEP)",
|
||||||
|
DateTime: new Date(2018, 6, 30, 9, 30, 0),
|
||||||
|
Attendees: [
|
||||||
|
{ fullName: "User 2", email: "user2@contoso.com" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Damage assessment: Big Cheese annual inspection (THEBI)",
|
||||||
|
DateTime: new Date(2018, 6, 30, 11, 0, 0),
|
||||||
|
Attendees: [
|
||||||
|
{ fullName: "User 1", email: "user1@contoso.com" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Fire inspection: Lazy K Kountry Store (LAZYK)",
|
||||||
|
DateTime: new Date(2018, 6, 30, 15, 30, 0),
|
||||||
|
Attendees: [
|
||||||
|
{ fullName: "User 3", email: "user3@contoso.com" },
|
||||||
|
{ fullName: "User 2", email: "user2@contoso.com" },
|
||||||
|
{ fullName: "User 1", email: "user1@contoso.com" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
public getGroupCalendarItems(groupId?: string) {
|
||||||
|
return new Promise<ICalendarItem[]>((resolve) => {
|
||||||
|
resolve(this.mockItems);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ICalendarItem } from '../../model/ICalendarItem';
|
||||||
|
|
||||||
|
export interface ICalendarService {
|
||||||
|
|
||||||
|
getGroupCalendarItems (groupId?: string, groupEmail?: string) : Promise<ICalendarItem[]>;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { IConversationService } from './IConversationService';
|
||||||
|
import { ContentType } from '../../model/IConversation';
|
||||||
|
|
||||||
|
export default class ConversationServiceMock implements IConversationService {
|
||||||
|
|
||||||
|
public createChatThread(content: string, contentType: ContentType) {
|
||||||
|
|
||||||
|
console.log(`New chat thread: ${content}`);
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { INewChatThread, ContentType } from '../../model/IConversation';
|
||||||
|
import { IConversationService } from '../../services/ConversationService/IConversationService';
|
||||||
|
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { MSGraphClient } from '@microsoft/sp-http';
|
||||||
|
|
||||||
|
export default class ConversationServiceTeams implements IConversationService {
|
||||||
|
|
||||||
|
private context: WebPartContext;
|
||||||
|
private teamId: string;
|
||||||
|
private channelId: string;
|
||||||
|
|
||||||
|
constructor(context: WebPartContext, serviceScope: ServiceScope,
|
||||||
|
teamId: string, channelId: string) {
|
||||||
|
this.context = context;
|
||||||
|
this.teamId = teamId;
|
||||||
|
this.channelId = channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createChatThread(content: string, contentType: ContentType) {
|
||||||
|
|
||||||
|
const result = new Promise<void>((resolve, reject) => {
|
||||||
|
|
||||||
|
const postContent: INewChatThread =
|
||||||
|
{
|
||||||
|
rootMessage: {
|
||||||
|
body: {
|
||||||
|
content: content,
|
||||||
|
contentType: contentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.context.msGraphClientFactory
|
||||||
|
.getClient()
|
||||||
|
.then((graphClient: MSGraphClient): void => {
|
||||||
|
graphClient.api(`https://graph.microsoft.com/beta/teams/${this.teamId}/channels/${this.channelId}/chatThreads`)
|
||||||
|
.post(postContent, ((err, res) => {
|
||||||
|
resolve();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ContentType } from '../../model/IConversation';
|
||||||
|
|
||||||
|
export interface IConversationService {
|
||||||
|
|
||||||
|
createChatThread(content: string, contentType: ContentType):
|
||||||
|
Promise<void>;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { ICustomerService } from './ICustomerService';
|
||||||
|
import { ICustomer } from '../../model/ICustomer';
|
||||||
|
|
||||||
|
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { HttpClient } from '@microsoft/sp-http';
|
||||||
|
|
||||||
|
|
||||||
|
export default class CustomerService implements ICustomerService {
|
||||||
|
|
||||||
|
private context: IWebPartContext;
|
||||||
|
constructor(context: IWebPartContext, serviceScope: ServiceScope) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCustomer(customerID: string): Promise<ICustomer> {
|
||||||
|
|
||||||
|
var result: Promise<ICustomer> = new Promise<ICustomer>
|
||||||
|
((resolve, reject) => {
|
||||||
|
|
||||||
|
this.context.httpClient
|
||||||
|
.fetch(`https://services.odata.org/V3/Northwind/Northwind.svc/Customers?$filter=CustomerID eq '${customerID}'`,
|
||||||
|
HttpClient.configurations.v1,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {"accept": "application/json"},
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'default'
|
||||||
|
})
|
||||||
|
.then ((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw (`Error ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// TODO: Kill the any
|
||||||
|
.then ((o: any) => {
|
||||||
|
resolve(o.value[0]);
|
||||||
|
});
|
||||||
|
// TODO: Handle exception
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { ICustomerService } from './ICustomerService';
|
||||||
|
import { ICustomer } from '../../model/ICustomer';
|
||||||
|
|
||||||
|
export default class CustomerServiceMock implements ICustomerService {
|
||||||
|
|
||||||
|
// US customers from Northwind database
|
||||||
|
private mockItems: ICustomer[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"CustomerID": "GREAL",
|
||||||
|
"CompanyName": "Great Lakes Food Market",
|
||||||
|
"ContactName": "Howard Snyder",
|
||||||
|
"ContactTitle": "Marketing Manager",
|
||||||
|
"Address": "2732 Baker Blvd.",
|
||||||
|
"City": "Eugene",
|
||||||
|
"Region": "OR",
|
||||||
|
"PostalCode": "97403",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(503) 555-7555",
|
||||||
|
"Fax": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "HUNGC",
|
||||||
|
"CompanyName": "Hungry Coyote Import Store",
|
||||||
|
"ContactName": "Yoshi Latimer",
|
||||||
|
"ContactTitle": "Sales Representative",
|
||||||
|
"Address": "City Center Plaza 516 Main St.",
|
||||||
|
"City": "Elgin",
|
||||||
|
"Region": "OR",
|
||||||
|
"PostalCode": "97827",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(503) 555-6874",
|
||||||
|
"Fax": "(503) 555-2376"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "LAZYK",
|
||||||
|
"CompanyName": "Lazy K Kountry Store",
|
||||||
|
"ContactName": "John Steel",
|
||||||
|
"ContactTitle": "Marketing Manager",
|
||||||
|
"Address": "12 Orchestra Terrace",
|
||||||
|
"City": "Walla Walla",
|
||||||
|
"Region": "WA",
|
||||||
|
"PostalCode": "99362",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(509) 555-7969",
|
||||||
|
"Fax": "(509) 555-6221"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "LETSS",
|
||||||
|
"CompanyName": "Let's Stop N Shop",
|
||||||
|
"ContactName": "Jaime Yorres",
|
||||||
|
"ContactTitle": "Owner",
|
||||||
|
"Address": "87 Polk St. Suite 5",
|
||||||
|
"City": "San Francisco",
|
||||||
|
"Region": "CA",
|
||||||
|
"PostalCode": "94117",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(415) 555-5938",
|
||||||
|
"Fax": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "LONEP",
|
||||||
|
"CompanyName": "Lonesome Pine Restaurant",
|
||||||
|
"ContactName": "Fran Wilson",
|
||||||
|
"ContactTitle": "Sales Manager",
|
||||||
|
"Address": "89 Chiaroscuro Rd.",
|
||||||
|
"City": "Portland",
|
||||||
|
"Region": "OR",
|
||||||
|
"PostalCode": "97219",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(503) 555-9573",
|
||||||
|
"Fax": "(503) 555-9646"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "OLDWO",
|
||||||
|
"CompanyName": "Old World Delicatessen",
|
||||||
|
"ContactName": "Rene Phillips",
|
||||||
|
"ContactTitle": "Sales Representative",
|
||||||
|
"Address": "2743 Bering St.",
|
||||||
|
"City": "Anchorage",
|
||||||
|
"Region": "AK",
|
||||||
|
"PostalCode": "99508",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(907) 555-7584",
|
||||||
|
"Fax": "(907) 555-2880"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "RATTC",
|
||||||
|
"CompanyName": "Rattlesnake Canyon Grocery",
|
||||||
|
"ContactName": "Paula Wilson",
|
||||||
|
"ContactTitle": "Assistant Sales Representative",
|
||||||
|
"Address": "2817 Milton Dr.",
|
||||||
|
"City": "Albuquerque",
|
||||||
|
"Region": "NM",
|
||||||
|
"PostalCode": "87110",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(505) 555-5939",
|
||||||
|
"Fax": "(505) 555-3620"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "SAVEA",
|
||||||
|
"CompanyName": "Save-a-lot Markets",
|
||||||
|
"ContactName": "Jose Pavarotti",
|
||||||
|
"ContactTitle": "Sales Representative",
|
||||||
|
"Address": "187 Suffolk Ln.",
|
||||||
|
"City": "Boise",
|
||||||
|
"Region": "ID",
|
||||||
|
"PostalCode": "83720",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(208) 555-8097",
|
||||||
|
"Fax": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "SPLIR",
|
||||||
|
"CompanyName": "Split Rail Beer & Ale",
|
||||||
|
"ContactName": "Art Braunschweiger",
|
||||||
|
"ContactTitle": "Sales Manager",
|
||||||
|
"Address": "P.O. Box 555",
|
||||||
|
"City": "Lander",
|
||||||
|
"Region": "WY",
|
||||||
|
"PostalCode": "82520",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(307) 555-4680",
|
||||||
|
"Fax": "(307) 555-6525"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "THEBI",
|
||||||
|
"CompanyName": "The Big Cheese",
|
||||||
|
"ContactName": "Liz Nixon",
|
||||||
|
"ContactTitle": "Marketing Manager",
|
||||||
|
"Address": "89 Jefferson Way Suite 2",
|
||||||
|
"City": "Portland",
|
||||||
|
"Region": "OR",
|
||||||
|
"PostalCode": "97201",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(503) 555-3612",
|
||||||
|
"Fax": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "THECR",
|
||||||
|
"CompanyName": "The Cracker Box",
|
||||||
|
"ContactName": "Liu Wong",
|
||||||
|
"ContactTitle": "Marketing Assistant",
|
||||||
|
"Address": "55 Grizzly Peak Rd.",
|
||||||
|
"City": "Butte",
|
||||||
|
"Region": "MT",
|
||||||
|
"PostalCode": "59801",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(406) 555-5834",
|
||||||
|
"Fax": "(406) 555-8083"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "TRAIH",
|
||||||
|
"CompanyName": "Trail's Head Gourmet Provisioners",
|
||||||
|
"ContactName": "Helvetius Nagy",
|
||||||
|
"ContactTitle": "Sales Associate",
|
||||||
|
"Address": "722 DaVinci Blvd.",
|
||||||
|
"City": "Kirkland",
|
||||||
|
"Region": "WA",
|
||||||
|
"PostalCode": "98034",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(206) 555-8257",
|
||||||
|
"Fax": "(206) 555-2174"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CustomerID": "WHITC",
|
||||||
|
"CompanyName": "White Clover Markets",
|
||||||
|
"ContactName": "Karl Jablonski",
|
||||||
|
"ContactTitle": "Owner",
|
||||||
|
"Address": "305 - 14th Ave. S. Suite 3B",
|
||||||
|
"City": "Seattle",
|
||||||
|
"Region": "WA",
|
||||||
|
"PostalCode": "98128",
|
||||||
|
"Country": "USA",
|
||||||
|
"Phone": "(206) 555-4112",
|
||||||
|
"Fax": "(206) 555-4115"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
public getCustomer(customerID: string):Promise<ICustomer> {
|
||||||
|
var result: ICustomer;
|
||||||
|
|
||||||
|
result = this.mockItems.filter(c => c.CustomerID == customerID)[0];
|
||||||
|
|
||||||
|
return new Promise<ICustomer>((resolve) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { ICustomer } from '../../model/ICustomer';
|
||||||
|
|
||||||
|
export interface ICustomerService {
|
||||||
|
getCustomer (customerID: string) : Promise<ICustomer>;
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { IDocument } from '../../model/IDocument';
|
||||||
|
import { IDocumentService } from './IDocumentService';
|
||||||
|
import { IDocumentsResponse } from './IDocumentsResponse';
|
||||||
|
|
||||||
|
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { SPHttpClient } from '@microsoft/sp-http';
|
||||||
|
|
||||||
|
export default class DocumentService implements IDocumentService {
|
||||||
|
|
||||||
|
private context: IWebPartContext;
|
||||||
|
constructor(context: IWebPartContext, serviceScope: ServiceScope) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDocuments(customerId: string):
|
||||||
|
Promise<IDocument[]> {
|
||||||
|
|
||||||
|
var result = new Promise<IDocument[]>((resolve, reject) => {
|
||||||
|
|
||||||
|
const siteUrl = this.context.pageContext.web.absoluteUrl;
|
||||||
|
|
||||||
|
this.context.spHttpClient
|
||||||
|
.fetch(`${siteUrl}/_api/lists/GetByTitle('Documents')/items?$filter=Customer eq '${customerId}'&$select=Title,FileLeafRef,FileRef,UniqueId,Modified,Author/Name,Author/Title&$expand=Author/Id&$orderby=Title`,
|
||||||
|
SPHttpClient.configurations.v1,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: { "accept": "application/json" },
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'default'
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw (`Error ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((o: IDocumentsResponse) => {
|
||||||
|
let docs: IDocument[] = [];
|
||||||
|
o.value.forEach((doc) => {
|
||||||
|
docs.push({
|
||||||
|
name: doc.FileLeafRef,
|
||||||
|
url: doc.FileRef,
|
||||||
|
author: doc.Author.Title,
|
||||||
|
date: new Date(doc.Modified)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
resolve(docs);
|
||||||
|
});
|
||||||
|
// TODO: Handle exception
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { IDocument } from '../../model/IDocument';
|
||||||
|
import { IDocumentService } from './IDocumentService';
|
||||||
|
|
||||||
|
export default class DocumentServiceMock implements IDocumentService {
|
||||||
|
|
||||||
|
// US customers from Northwind database
|
||||||
|
private mockData: IDocument[] = [
|
||||||
|
{
|
||||||
|
name: "Document A",
|
||||||
|
url: "#",
|
||||||
|
author: "Ada Lovelace",
|
||||||
|
date: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Document B",
|
||||||
|
url: "#",
|
||||||
|
author: "Charles Babbage",
|
||||||
|
date: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Document C",
|
||||||
|
url: "#",
|
||||||
|
author: "Grace Hopper",
|
||||||
|
date: new Date()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
public getDocuments (customerId: string):
|
||||||
|
Promise<IDocument[]> {
|
||||||
|
|
||||||
|
var result = this.mockData;
|
||||||
|
|
||||||
|
return new Promise<IDocument[]>((resolve) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IDocument } from '../../model/IDocument';
|
||||||
|
|
||||||
|
// US only for now
|
||||||
|
export interface IDocumentService {
|
||||||
|
getDocuments (customerId: string):
|
||||||
|
Promise<IDocument[]>;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export interface Author {
|
||||||
|
Name: string;
|
||||||
|
Title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Value {
|
||||||
|
Modified: string;
|
||||||
|
FileLeafRef: string;
|
||||||
|
Title: string;
|
||||||
|
FileRef: string;
|
||||||
|
UniqueId: string;
|
||||||
|
Author: Author;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDocumentsResponse {
|
||||||
|
value: Value[];
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { IMapLocation } from '../../model/IMapLocation';
|
||||||
|
|
||||||
|
// US only for now
|
||||||
|
export interface IMapService {
|
||||||
|
getLocation (address: string, city: string, state: string, zip: string):
|
||||||
|
Promise<IMapLocation>;
|
||||||
|
getMapApiKey (): string;
|
||||||
|
getMapImageUrl (address: string, city: string, state: string,
|
||||||
|
country: string, postalCode: string):
|
||||||
|
Promise<string>;
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { IMapService } from './IMapService';
|
||||||
|
import { IMapLocation } from '../../model/IMapLocation';
|
||||||
|
|
||||||
|
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { HttpClient } from '@microsoft/sp-http';
|
||||||
|
import * as constants from '../../constants';
|
||||||
|
|
||||||
|
export default class MapService implements IMapService {
|
||||||
|
|
||||||
|
private context: IWebPartContext;
|
||||||
|
constructor(context: IWebPartContext, serviceScope: ServiceScope) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLocation(address: string, city: string, state: string, zip: string):
|
||||||
|
Promise<IMapLocation> {
|
||||||
|
|
||||||
|
// Remove "." and trim address to make Bing maps happy
|
||||||
|
const adjustedAddress = address.replace('.',' ').trim();
|
||||||
|
var result = new Promise<IMapLocation>((resolve, reject) => {
|
||||||
|
this.context.httpClient
|
||||||
|
.fetch(`https://dev.virtualearth.net/REST/v1/Locations/US/${state}/${zip}/${city}/${adjustedAddress}?key=${constants.mapApiKey}`,
|
||||||
|
HttpClient.configurations.v1,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: { "accept": "application/json" },
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'default'
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw (`Error ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((o: IMapLocation) => {
|
||||||
|
resolve(o);
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMapApiKey(): string {
|
||||||
|
return constants.mapApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMapImageUrl(address: string, city: string, state: string,
|
||||||
|
country: string, postalCode: string):
|
||||||
|
Promise<string> {
|
||||||
|
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
|
if (country &&
|
||||||
|
country.toLowerCase() == "usa" &&
|
||||||
|
postalCode) {
|
||||||
|
|
||||||
|
const locationSignature = this.getLocationSignature(
|
||||||
|
address, city, state, country, postalCode);
|
||||||
|
|
||||||
|
if (this.locationSignature === locationSignature) {
|
||||||
|
|
||||||
|
// If here, the cached location is valid
|
||||||
|
resolve (this.getMapImageUrlFromLocation(this.location));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// If here we have no location cached; call the web service
|
||||||
|
this.getLocation(address, city, state, postalCode)
|
||||||
|
.then((location: IMapLocation) => {
|
||||||
|
this.location = location;
|
||||||
|
this.locationSignature = locationSignature;
|
||||||
|
resolve (this.getMapImageUrlFromLocation(this.location));
|
||||||
|
})
|
||||||
|
.catch ((error: string) => {
|
||||||
|
resolve('#');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private locationSignature: string;
|
||||||
|
private location: IMapLocation;
|
||||||
|
|
||||||
|
private getLocationSignature(address: string, city: string, state: string,
|
||||||
|
country: string, postalCode: string) {
|
||||||
|
return `${address}**${city}**${state}**${country}**${postalCode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMapImageUrlFromLocation(location: IMapLocation) {
|
||||||
|
|
||||||
|
const coordinates =
|
||||||
|
location.resourceSets[0].resources[0].point.coordinates;
|
||||||
|
const latitude = coordinates[0];
|
||||||
|
const longitude = coordinates[1];
|
||||||
|
|
||||||
|
const apiKey = this.getMapApiKey();
|
||||||
|
|
||||||
|
return `https://dev.virtualearth.net/REST/v1/Imagery/Map/Road/${latitude},${longitude}/16?mapSize=450,600&pp=${latitude},${longitude}&key=${apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { IMapService } from './IMapService';
|
||||||
|
import { IMapLocation } from '../../model/IMapLocation';
|
||||||
|
import * as constants from '../../constants';
|
||||||
|
|
||||||
|
export default class MapServiceMock implements IMapService {
|
||||||
|
|
||||||
|
// US customers from Northwind database
|
||||||
|
private mockData: IMapLocation =
|
||||||
|
{
|
||||||
|
"authenticationResultCode": "ValidCredentials",
|
||||||
|
"brandLogoUri": "http://dev.virtualearth.net/Branding/logo_powered_by.png",
|
||||||
|
"copyright": "Copyright © 2018 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",
|
||||||
|
"resourceSets": [
|
||||||
|
{
|
||||||
|
"estimatedTotal": 1,
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"__type": "Location:http://schemas.microsoft.com/search/local/ws/rest/v1",
|
||||||
|
"bbox": [
|
||||||
|
42.3640072824293,
|
||||||
|
-71.1962504127409,
|
||||||
|
42.3717327175707,
|
||||||
|
-71.1823095872591
|
||||||
|
],
|
||||||
|
"name": "1 Main St, Watertown, MA 02472",
|
||||||
|
"point": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [
|
||||||
|
42.36787,
|
||||||
|
-71.18928
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"addressLine": "1 Main St",
|
||||||
|
"adminDistrict": "MA",
|
||||||
|
"adminDistrict2": "Middlesex County",
|
||||||
|
"countryRegion": "United States",
|
||||||
|
"formattedAddress": "1 Main St, Watertown, MA 02472",
|
||||||
|
"locality": "Watertown",
|
||||||
|
"postalCode": "02472"
|
||||||
|
},
|
||||||
|
"confidence": "High",
|
||||||
|
"entityType": "Address",
|
||||||
|
"geocodePoints": [
|
||||||
|
{
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [
|
||||||
|
42.36787,
|
||||||
|
-71.18928
|
||||||
|
],
|
||||||
|
"calculationMethod": "Rooftop",
|
||||||
|
"usageTypes": [
|
||||||
|
"Display"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [
|
||||||
|
42.3682298194267,
|
||||||
|
-71.1888233197147
|
||||||
|
],
|
||||||
|
"calculationMethod": "Rooftop",
|
||||||
|
"usageTypes": [
|
||||||
|
"Route"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"matchCodes": [
|
||||||
|
"Good"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"statusCode": 200,
|
||||||
|
"statusDescription": "OK",
|
||||||
|
"traceId": "98ce3776abf44779a9b55622ef8b4be7|BN1CD9C1AB|7.7.0.0|Ref A: 0409F12560074D0B806E088436EF5946 Ref B: BN3EDGE0208 Ref C: 2018-07-26T01:03:06Z"
|
||||||
|
};
|
||||||
|
|
||||||
|
public getLocation (address: string, city: string, state: string, zip: string):
|
||||||
|
Promise<IMapLocation> {
|
||||||
|
|
||||||
|
var result = this.mockData;
|
||||||
|
|
||||||
|
return new Promise<IMapLocation>((resolve) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMapApiKey(): string {
|
||||||
|
return constants.mapApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMapImageUrl(address: string, city: string, state: string,
|
||||||
|
country: string, postalCode: string): Promise<string> {
|
||||||
|
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const apiKey = this.getMapApiKey();
|
||||||
|
const latitude = 42.36787;
|
||||||
|
const longitude = -71.18928;
|
||||||
|
resolve (`https://dev.virtualearth.net/REST/v1/Imagery/Map/Road/${latitude},${longitude}/16?mapSize=450,600&pp=${latitude},${longitude}&key=${apiKey}`);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IPhoto } from '../../model/IPhoto';
|
||||||
|
|
||||||
|
export interface IPhotoService {
|
||||||
|
|
||||||
|
getPhotos(customerId: string): Promise<IPhoto[]>;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
export interface Value {
|
||||||
|
CheckInComment: string;
|
||||||
|
CheckOutType: number;
|
||||||
|
ContentTag: string;
|
||||||
|
CustomizedPageStatus: number;
|
||||||
|
ETag: string;
|
||||||
|
Exists: boolean;
|
||||||
|
IrmEnabled: boolean;
|
||||||
|
Length: number;
|
||||||
|
Level: number;
|
||||||
|
LinkingUri?: any;
|
||||||
|
LinkingUrl: string;
|
||||||
|
MajorVersion: number;
|
||||||
|
MinorVersion: number;
|
||||||
|
Name: string;
|
||||||
|
ServerRelativeUrl: string;
|
||||||
|
TimeCreated: Date;
|
||||||
|
TimeLastModified: Date;
|
||||||
|
Title?: any;
|
||||||
|
UIVersion: number;
|
||||||
|
UIVersionLabel: string;
|
||||||
|
UniqueId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPhotosResponse {
|
||||||
|
value: Value[];
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { IPhotoService } from './IPhotoService';
|
||||||
|
import { IPhotosResponse } from './IPhotosResponse';
|
||||||
|
import { IPhoto } from '../../model/IPhoto';
|
||||||
|
|
||||||
|
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { SPHttpClient } from '@microsoft/sp-http';
|
||||||
|
|
||||||
|
export default class PhotoService implements IPhotoService {
|
||||||
|
|
||||||
|
private context: IWebPartContext;
|
||||||
|
constructor(context: IWebPartContext, serviceScope: ServiceScope) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPhotos(customerId: string): Promise<IPhoto[]> {
|
||||||
|
|
||||||
|
var result = new Promise<IPhoto[]>((resolve, reject) => {
|
||||||
|
|
||||||
|
const absoluteUrl = this.context.pageContext.web.absoluteUrl;
|
||||||
|
const serverRelativeUrl = this.context.pageContext.web.serverRelativeUrl;
|
||||||
|
|
||||||
|
this.context.spHttpClient
|
||||||
|
.fetch(`${absoluteUrl}/_api/web/GetFolderByServerRelativeUrl('${serverRelativeUrl}/Photos/${customerId}')/Files`,
|
||||||
|
SPHttpClient.configurations.v1,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: { "accept": "application/json" },
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'default'
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw (`Error ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((o: IPhotosResponse) => {
|
||||||
|
let files: IPhoto[] = [];
|
||||||
|
o.value.forEach((file) => {
|
||||||
|
files.push({
|
||||||
|
name: file.Name,
|
||||||
|
url: file.ServerRelativeUrl
|
||||||
|
});
|
||||||
|
});
|
||||||
|
resolve(files);
|
||||||
|
});
|
||||||
|
// TODO: Handle exception
|
||||||
|
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { IPhotoService } from './IPhotoService';
|
||||||
|
import { IPhoto } from '../../model/IPhoto';
|
||||||
|
|
||||||
|
export default class PhotoServiceMock implements IPhotoService {
|
||||||
|
|
||||||
|
// US customers from Northwind database
|
||||||
|
private mockData: IPhoto[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "Surface 1",
|
||||||
|
url: "https://c.s-microsoft.com/en-au/CMSImages/Windows_Homepage_Hero_RS4_1920.jpg?version=85d2e084-dab6-5d93-619a-2af2c228e9a2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Surface 2",
|
||||||
|
url: "https://c.s-microsoft.com/en-au/CMSImages/1920_panel2_hero_EdgeSurface.jpg?version=c9920e78-cb6c-64d4-c967-fd1d34791f6e"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
public getPhotos(customerID: string): Promise<IPhoto[]> {
|
||||||
|
|
||||||
|
var result = this.mockData;
|
||||||
|
|
||||||
|
return new Promise<IPhoto[]>((resolve) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { IVisitService } from './VisitService/IVisitService';
|
||||||
|
import VisitService from './VisitService/VisitService';
|
||||||
|
import { ICustomerService } from './CustomerService/ICustomerService';
|
||||||
|
import CustomerService from './CustomerService/CustomerService';
|
||||||
|
import CustomerServiceMock from './CustomerService/CustomerServiceMock';
|
||||||
|
import { ICalendarService } from './CalendarService/ICalendarService';
|
||||||
|
import CalendarService from './CalendarService/CalendarService';
|
||||||
|
import CalendarServiceMock from './CalendarService/CalendarServiceMock';
|
||||||
|
import { IWeatherService } from './WeatherService/IWeatherService';
|
||||||
|
import WeatherService from './WeatherService/WeatherService';
|
||||||
|
import WeatherServiceMock from './WeatherService/WeatherServiceMock';
|
||||||
|
import { IMapService } from './MapService/IMapService';
|
||||||
|
import MapService from './MapService/MapService';
|
||||||
|
import MapServiceMock from './MapService/MapServiceMock';
|
||||||
|
import { IDocumentService } from './DocumentService/IDocumentService';
|
||||||
|
import DocumentService from './DocumentService/DocumentService';
|
||||||
|
import DocumentServiceMock from './DocumentService/DocumentServiceMock';
|
||||||
|
import { IActivityService } from './ActivityService/IActivityService';
|
||||||
|
import ActivityServiceMock from './ActivityService/ActivityServiceMock';
|
||||||
|
import { IConversationService } from './ConversationService/IConversationService';
|
||||||
|
import ConversationServiceMock from './ConversationService/ConversationServiceMock';
|
||||||
|
import ConversationServiceTeams from './ConversationService/ConversationServiceTeams';
|
||||||
|
import { IPhotoService } from './PhotoService/IPhotoService';
|
||||||
|
import PhotoServiceMock from './PhotoService/PhotoServiceMock';
|
||||||
|
import PhotoService from './PhotoService/PhotoService';
|
||||||
|
|
||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { EnvironmentType } from '@microsoft/sp-core-library';
|
||||||
|
|
||||||
|
export default class ServiceFactory {
|
||||||
|
|
||||||
|
public static getVisitService(
|
||||||
|
environmentType: EnvironmentType,
|
||||||
|
context: WebPartContext,
|
||||||
|
serviceScope: ServiceScope): IVisitService {
|
||||||
|
|
||||||
|
var calendarService: ICalendarService;
|
||||||
|
var customerService: ICustomerService;
|
||||||
|
|
||||||
|
if (environmentType === EnvironmentType.Local) {
|
||||||
|
calendarService = new CalendarServiceMock();
|
||||||
|
customerService = new CustomerServiceMock();
|
||||||
|
} else {
|
||||||
|
calendarService = new CalendarService(context, serviceScope);
|
||||||
|
customerService = new CustomerService(context, serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VisitService(calendarService, customerService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getWeatherService(
|
||||||
|
environmentType: EnvironmentType,
|
||||||
|
context: WebPartContext,
|
||||||
|
serviceScope: ServiceScope): IWeatherService {
|
||||||
|
|
||||||
|
return (environmentType === EnvironmentType.Local) ?
|
||||||
|
new WeatherServiceMock() :
|
||||||
|
new WeatherService(context, serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getMapService(
|
||||||
|
environmentType: EnvironmentType,
|
||||||
|
context: WebPartContext,
|
||||||
|
serviceScope: ServiceScope): IMapService {
|
||||||
|
|
||||||
|
return (environmentType === EnvironmentType.Local) ?
|
||||||
|
new MapServiceMock() :
|
||||||
|
new MapService(context, serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getDocumentService(
|
||||||
|
environmentType: EnvironmentType,
|
||||||
|
context: WebPartContext,
|
||||||
|
serviceScope: ServiceScope): IDocumentService {
|
||||||
|
|
||||||
|
return (environmentType === EnvironmentType.Local) ?
|
||||||
|
new DocumentServiceMock() :
|
||||||
|
new DocumentService(context, serviceScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getActivityService(
|
||||||
|
environmentType: EnvironmentType,
|
||||||
|
context: WebPartContext,
|
||||||
|
serviceScope: ServiceScope): IActivityService {
|
||||||
|
|
||||||
|
return (environmentType === EnvironmentType.Local) ?
|
||||||
|
new ActivityServiceMock() :
|
||||||
|
new ActivityServiceMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getConversationService(
|
||||||
|
environmentType: EnvironmentType,
|
||||||
|
context: WebPartContext,
|
||||||
|
serviceScope: ServiceScope,
|
||||||
|
teamId?: string,
|
||||||
|
channelId?: string): IConversationService {
|
||||||
|
|
||||||
|
return (environmentType === EnvironmentType.Local ||
|
||||||
|
!teamId || !channelId ) ?
|
||||||
|
new ConversationServiceMock() :
|
||||||
|
new ConversationServiceTeams(context, serviceScope, teamId, channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getPhotoService(
|
||||||
|
environmentType: EnvironmentType,
|
||||||
|
context: WebPartContext,
|
||||||
|
serviceScope: ServiceScope): IPhotoService {
|
||||||
|
|
||||||
|
return (environmentType === EnvironmentType.Local) ?
|
||||||
|
new PhotoServiceMock() :
|
||||||
|
new PhotoService(context, serviceScope);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { IVisit } from '../../model/IVisit';
|
||||||
|
|
||||||
|
export interface IVisitService {
|
||||||
|
getGroupVisits (groupId?: string, groupEmail?: string) : Promise<IVisit[]>;
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { IVisitService } from './IVisitService';
|
||||||
|
import { IVisit } from '../../model/IVisit';
|
||||||
|
import { ICalendarService } from '../CalendarService/ICalendarService';
|
||||||
|
import { ICustomerService } from '../CustomerService/ICustomerService';
|
||||||
|
|
||||||
|
export default class VisitService implements IVisitService {
|
||||||
|
|
||||||
|
private calendarService: ICalendarService;
|
||||||
|
private customerService: ICustomerService;
|
||||||
|
constructor(calendarService: ICalendarService,
|
||||||
|
customerService: ICustomerService) {
|
||||||
|
|
||||||
|
this.calendarService = calendarService;
|
||||||
|
this.customerService = customerService;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public getGroupVisits(groupId?: string, groupName?: string) {
|
||||||
|
|
||||||
|
return new Promise<IVisit[]>((resolve, reject) => {
|
||||||
|
|
||||||
|
this.calendarService.getGroupCalendarItems(groupId, groupName)
|
||||||
|
.then((calendarItems) => {
|
||||||
|
var items: IVisit[] = new Array<IVisit>();
|
||||||
|
let outstandingPromises = 0;
|
||||||
|
|
||||||
|
calendarItems.forEach(element => {
|
||||||
|
|
||||||
|
// Parse title looking for customer ID
|
||||||
|
let regex = /\(([^)]+)\)/;
|
||||||
|
if (element.Title) {
|
||||||
|
let matches = regex.exec(element.Title);
|
||||||
|
if (matches && matches.length > 1) {
|
||||||
|
// If here, we found a potential customer ID
|
||||||
|
let customerId = matches[1];
|
||||||
|
outstandingPromises++;
|
||||||
|
this.customerService.getCustomer(customerId)
|
||||||
|
.then((customer) => {
|
||||||
|
if (customer) {
|
||||||
|
// If here, we found an actual customer; add it to the list
|
||||||
|
items.push({
|
||||||
|
calendarItem: element,
|
||||||
|
customer: customer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!--outstandingPromises) {
|
||||||
|
resolve(items.sort((i, j) =>
|
||||||
|
(i.calendarItem.DateTime.getTime() -
|
||||||
|
j.calendarItem.DateTime.getTime())));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { IWeatherConditions } from '../../model/IWeatherConditions';
|
||||||
|
|
||||||
|
// US only for now
|
||||||
|
export interface IWeatherService {
|
||||||
|
getConditions (zip: string) : Promise<IWeatherConditions>;
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { IWeatherService } from './IWeatherService';
|
||||||
|
import { IWeatherConditions } from '../../model/IWeatherConditions';
|
||||||
|
|
||||||
|
import { IWebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||||
|
import { HttpClient } from '@microsoft/sp-http';
|
||||||
|
import * as constants from '../../constants';
|
||||||
|
|
||||||
|
export default class WeatherService implements IWeatherService {
|
||||||
|
|
||||||
|
private context: IWebPartContext;
|
||||||
|
constructor(context: IWebPartContext, serviceScope: ServiceScope) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConditions (zip: string) : Promise<IWeatherConditions> {
|
||||||
|
|
||||||
|
var result: Promise<IWeatherConditions> = new Promise<IWeatherConditions>
|
||||||
|
((resolve, reject) => {
|
||||||
|
|
||||||
|
this.context.httpClient
|
||||||
|
.fetch(`https://api.openweathermap.org/data/2.5/weather?zip=${zip},us&appid=${constants.owmApiKey}`,
|
||||||
|
HttpClient.configurations.v1,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {"accept": "application/json"},
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'default'
|
||||||
|
})
|
||||||
|
.then ((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw (`Error ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then ((o: IWeatherConditions) => {
|
||||||
|
resolve(o);
|
||||||
|
});
|
||||||
|
// TODO: Handle exception
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { IWeatherService } from './IWeatherService';
|
||||||
|
import { IWeatherConditions } from '../../model/IWeatherConditions';
|
||||||
|
|
||||||
|
export default class WeatherServiceMock implements IWeatherService {
|
||||||
|
|
||||||
|
// US customers from Northwind database
|
||||||
|
private mockData: IWeatherConditions =
|
||||||
|
{
|
||||||
|
"coord": {
|
||||||
|
"lon": -71.24,
|
||||||
|
"lat": 42.4
|
||||||
|
},
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 500,
|
||||||
|
"main": "Rain",
|
||||||
|
"description": "light rain",
|
||||||
|
"icon": "10d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 701,
|
||||||
|
"main": "Mist",
|
||||||
|
"description": "mist",
|
||||||
|
"icon": "50d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 721,
|
||||||
|
"main": "Haze",
|
||||||
|
"description": "haze",
|
||||||
|
"icon": "50d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base": "stations",
|
||||||
|
"main": {
|
||||||
|
"temp": 300.61,
|
||||||
|
"pressure": 1016,
|
||||||
|
"humidity": 66,
|
||||||
|
"temp_min": 297.15,
|
||||||
|
"temp_max": 304.15
|
||||||
|
},
|
||||||
|
"visibility": 16093,
|
||||||
|
"wind": {
|
||||||
|
"speed": 7.2,
|
||||||
|
"deg": 180,
|
||||||
|
"gust": 10.8
|
||||||
|
},
|
||||||
|
"clouds": {
|
||||||
|
"all": 75
|
||||||
|
},
|
||||||
|
"dt": 1532542500,
|
||||||
|
"sys": {
|
||||||
|
"type": 1,
|
||||||
|
"id": 1272,
|
||||||
|
"message": 0.0051,
|
||||||
|
"country": "US",
|
||||||
|
"sunrise": 1532511070,
|
||||||
|
"sunset": 1532563874
|
||||||
|
},
|
||||||
|
"id": 420016955,
|
||||||
|
"name": "Boston",
|
||||||
|
"cod": 200
|
||||||
|
};
|
||||||
|
|
||||||
|
public getConditions(customerID: string): Promise<IWeatherConditions> {
|
||||||
|
|
||||||
|
var result = this.mockData;
|
||||||
|
|
||||||
|
return new Promise<IWeatherConditions>((resolve) => {
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.2/MicrosoftTeams.schema.json",
|
||||||
|
"manifestVersion": "1.2",
|
||||||
|
"packageName": "FieldVisitTab",
|
||||||
|
"id": "ed686d10-9382-4c3b-be6b-1957d4ec9692",
|
||||||
|
"version": "0.1",
|
||||||
|
"developer": {
|
||||||
|
"name": "SPFx + Teams Dev",
|
||||||
|
"websiteUrl": "https://products.office.com/en-us/sharepoint/collaboration",
|
||||||
|
"privacyUrl": "https://privacy.microsoft.com/en-us/privacystatement",
|
||||||
|
"termsOfUseUrl": "https://www.microsoft.com/en-us/servicesagreement"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"short": "FieldVisitTab"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"short": "Displays details of upcoming field visits in a Microsoft Teams tab",
|
||||||
|
"full": "Displays details of upcoming field visits in a Microsoft Teams tab"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"outline": "tab20x20.png",
|
||||||
|
"color": "tab96x96.png"
|
||||||
|
},
|
||||||
|
"accentColor": "#004578",
|
||||||
|
"configurableTabs": [
|
||||||
|
{
|
||||||
|
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=ed686d10-9382-4c3b-be6b-1957d4ec9692",
|
||||||
|
"canUpdateConfiguration": false,
|
||||||
|
"scopes": [
|
||||||
|
"team"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"validDomains": [
|
||||||
|
"*.login.microsoftonline.com",
|
||||||
|
"*.sharepoint.com",
|
||||||
|
"*.sharepoint-df.com",
|
||||||
|
"spoppe-a.akamaihd.net",
|
||||||
|
"spoprod-a.akamaihd.net",
|
||||||
|
"resourceseng.blob.core.windows.net",
|
||||||
|
"msft.spoppe.com"
|
||||||
|
],
|
||||||
|
"webApplicationInfo": {
|
||||||
|
"resource": "https://{teamSiteDomain}",
|
||||||
|
"id": "00000003-0000-0ff1-ce00-000000000000"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 933 B |
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@microsoft"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"es6-promise",
|
||||||
|
"webpack-env"
|
||||||
|
],
|
||||||
|
"lib": [
|
||||||
|
"es5",
|
||||||
|
"dom",
|
||||||
|
"es2015.collection"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"lib"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||||
|
"rules": {
|
||||||
|
"class-name": false,
|
||||||
|
"export-name": false,
|
||||||
|
"forin": false,
|
||||||
|
"label-position": false,
|
||||||
|
"member-access": true,
|
||||||
|
"no-arg": false,
|
||||||
|
"no-console": false,
|
||||||
|
"no-construct": false,
|
||||||
|
"no-duplicate-variable": true,
|
||||||
|
"no-eval": false,
|
||||||
|
"no-function-expression": true,
|
||||||
|
"no-internal-module": true,
|
||||||
|
"no-shadowed-variable": true,
|
||||||
|
"no-switch-case-fall-through": true,
|
||||||
|
"no-unnecessary-semicolons": true,
|
||||||
|
"no-unused-expression": true,
|
||||||
|
"no-use-before-declare": true,
|
||||||
|
"no-with-statement": true,
|
||||||
|
"semicolon": true,
|
||||||
|
"trailing-comma": false,
|
||||||
|
"typedef": false,
|
||||||
|
"typedef-whitespace": false,
|
||||||
|
"use-named-parameter": true,
|
||||||
|
"variable-name": false,
|
||||||
|
"whitespace": false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue