├── .gitattributes ├── .gitignore ├── .nuget ├── NuGet.Config ├── NuGet.exe └── NuGet.targets ├── ExpenseManager.sln ├── ExpenseManager ├── App_Start │ ├── BundleConfig.cs │ ├── FilterConfig.cs │ ├── RouteConfig.cs │ └── Startup.Auth.cs ├── Content │ ├── Site.css │ ├── animations.css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.icon-large.min.css │ ├── bootstrap.min.css │ ├── images │ │ ├── ColorPalette.png │ │ ├── angularShield.png │ │ ├── appFolders.png │ │ ├── customerApp.png │ │ ├── expenseBackground.jpg │ │ ├── expensesBackground_Dreamstime.jpg │ │ ├── female.png │ │ ├── male.png │ │ ├── people.png │ │ ├── readmeImages │ │ │ ├── 1-GoToAdminScreen.png │ │ │ ├── 10-selectexpensesandclickok.png │ │ │ ├── 11-setdefaultgroups.png │ │ │ ├── 12-HomePageTopLevelSiteWithLists.png │ │ │ ├── 2-CreateNewSiteCollection.png │ │ │ ├── 3-FillInSiteCollectionFormChooseTemplateLater.png │ │ │ ├── 4-clickonsolutiongallery.png │ │ │ ├── 5-clickUploadSolution.png │ │ │ ├── 6-browsetosolutionfromgithubfolder.png │ │ │ ├── 7-ActivateSolution.png │ │ │ ├── 8-clickbrowse.png │ │ │ ├── 9-clickcustomtab.png │ │ │ ├── ADPermissions.png │ │ │ ├── AddApplication.png │ │ │ ├── ApplicationProperties.png │ │ │ ├── ClientID.png │ │ │ ├── DefaultDirectory.png │ │ │ ├── ManagementServicesMenuItem.png │ │ │ ├── Permissions.png │ │ │ ├── channel9scrnsht.png │ │ │ └── screenshot.png │ │ ├── report.png │ │ └── spinner.gif │ └── styles.css ├── Controllers │ └── HomeController.cs ├── ExpenseManager.csproj ├── ExpenseManager.csproj.user ├── Global.asax ├── Global.asax.cs ├── Handlers │ ├── WebProxy.ashx │ └── WebProxy.ashx.cs ├── Models │ └── PerWebUserCache.cs ├── Properties │ └── AssemblyInfo.cs ├── Scripts │ ├── angular-ui-bootstrap.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-2.1.1.intellisense.js │ ├── jquery-2.1.1.js │ ├── jquery-2.1.1.min.js │ ├── jquery-2.1.1.min.map │ ├── modernizr-2.6.2.js │ ├── modernizr-2.8.3.js │ ├── npm.js │ └── tweenMax.min.js ├── SharePointHelpers │ └── SharePointAuth.cs ├── Startup.cs ├── Utils │ ├── EFADALContext.cs │ ├── EFADALTokenCache.cs │ ├── NaiveSessionCache.cs │ └── SettingsHelper.cs ├── Views │ ├── Home │ │ ├── Index.cshtml │ │ └── Title.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ └── _Layout.cshtml │ ├── _ViewStart.cshtml │ └── web.config ├── Web.Debug.config ├── Web.Release.config ├── Web.config ├── app │ ├── expenseApp │ │ ├── animations │ │ │ └── listAnimations.js │ │ ├── app.js │ │ ├── controllers │ │ │ ├── aboutController.js │ │ │ ├── employees │ │ │ │ ├── employeeEditController.js │ │ │ │ ├── employeeExpensesController.js │ │ │ │ └── employeesController.js │ │ │ ├── expenses │ │ │ │ ├── expenseChildController.js │ │ │ │ └── expensesController.js │ │ │ └── navbarController.js │ │ ├── directives │ │ │ ├── lazyLoader.js │ │ │ └── wcUnique.js │ │ ├── filters │ │ │ ├── nameCityStateFilter.js │ │ │ └── nameExpenseFilter.js │ │ ├── partials │ │ │ ├── dialog.html │ │ │ └── modal.html │ │ ├── services │ │ │ ├── config.js │ │ │ ├── dataService.js │ │ │ ├── dialogService.js │ │ │ ├── employeesSharePointService.js │ │ │ ├── httpInterceptors.js │ │ │ ├── modalService.js │ │ │ └── routeResolver.js │ │ └── views │ │ │ ├── about.html │ │ │ ├── employees │ │ │ ├── employeeEdit.html │ │ │ ├── employeeExpenses.html │ │ │ └── employees.html │ │ │ └── expenses │ │ │ ├── expenses.html │ │ │ └── expensesTable.html │ └── wc.directives │ │ └── directives │ │ ├── lazyLoader.js │ │ ├── menuHighlighter.js │ │ └── wcOverlay.js ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── index.html └── packages.config ├── ExpensesTrackerSiteTemplate.wsp ├── LICENSE └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | *.suo 4 | packages 5 | *.mdf 6 | *.ldf 7 | *.nogit -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/.nuget/NuGet.exe -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | 37 | 38 | 39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 41 | 42 | 43 | 44 | $(MSBuildProjectDirectory)\packages.config 45 | $(PackagesProjectConfig) 46 | 47 | 48 | 49 | 50 | $(NuGetToolsPath)\NuGet.exe 51 | @(PackageSource) 52 | 53 | "$(NuGetExePath)" 54 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 55 | 56 | $(TargetDir.Trim('\\')) 57 | 58 | -RequireConsent 59 | -NonInteractive 60 | 61 | "$(SolutionDir) " 62 | "$(SolutionDir)" 63 | 64 | 65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 67 | 68 | 69 | 70 | RestorePackages; 71 | $(BuildDependsOn); 72 | 73 | 74 | 75 | 76 | $(BuildDependsOn); 77 | BuildPackage; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /ExpenseManager.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExpenseManager", "ExpenseManager\ExpenseManager.csproj", "{DEF72E54-F016-410D-B431-AFE37C12A67B}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{29EF2974-97D1-448B-9E3B-1E3CC5C216A4}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.Config = .nuget\NuGet.Config 11 | .nuget\NuGet.exe = .nuget\NuGet.exe 12 | .nuget\NuGet.targets = .nuget\NuGet.targets 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {DEF72E54-F016-410D-B431-AFE37C12A67B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {DEF72E54-F016-410D-B431-AFE37C12A67B}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {DEF72E54-F016-410D-B431-AFE37C12A67B}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {DEF72E54-F016-410D-B431-AFE37C12A67B}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /ExpenseManager/App_Start/BundleConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Optimization; 3 | 4 | namespace ExpenseManager 5 | { 6 | public class BundleConfig 7 | { 8 | // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862 9 | public static void RegisterBundles(BundleCollection bundles) 10 | { 11 | bundles.Add(new ScriptBundle("~/bundles/jquery").Include( 12 | "~/Scripts/jquery-{version}.js")); 13 | 14 | bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( 15 | "~/Scripts/jquery.validate*")); 16 | 17 | // Use the development version of Modernizr to develop with and learn from. Then, when you're 18 | // ready for production, use the build tool at http://modernizr.com to pick only the tests you need. 19 | bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( 20 | "~/Scripts/modernizr-*")); 21 | 22 | bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( 23 | "~/Scripts/bootstrap.js")); 24 | 25 | bundles.Add(new StyleBundle("~/Content/css").Include( 26 | "~/Content/bootstrap.css", 27 | "~/Content/site.css", 28 | "~/Content/bootstrap-responsive.css")); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ExpenseManager/App_Start/FilterConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Mvc; 3 | 4 | namespace ExpenseManager 5 | { 6 | public class FilterConfig 7 | { 8 | public static void RegisterGlobalFilters(GlobalFilterCollection filters) 9 | { 10 | filters.Add(new HandleErrorAttribute()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ExpenseManager/App_Start/RouteConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Web.Routing; 7 | 8 | namespace ExpenseManager 9 | { 10 | public class RouteConfig 11 | { 12 | public static void RegisterRoutes(RouteCollection routes) 13 | { 14 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 | 16 | routes.MapRoute( 17 | name: "Default", 18 | url: "{controller}/{action}/{id}", 19 | defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 20 | ); 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /ExpenseManager/App_Start/Startup.Auth.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 2 | using Microsoft.Owin.Security; 3 | using Microsoft.Owin.Security.Cookies; 4 | using Microsoft.Owin.Security.OpenIdConnect; 5 | using Owin; 6 | using ExpenseManager.Utils; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Configuration; 10 | using System.Globalization; 11 | using System.Linq; 12 | using System.Security.Claims; 13 | using System.Threading.Tasks; 14 | using System.Web; 15 | using System.Web.Configuration; 16 | 17 | namespace ExpenseManager 18 | { 19 | public partial class Startup 20 | { 21 | public void ConfigureAuth(IAppBuilder app) 22 | { 23 | app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); 24 | app.UseCookieAuthentication(new CookieAuthenticationOptions()); 25 | app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions 26 | { 27 | ClientId = SettingsHelper.ClientId, 28 | Authority = SettingsHelper.AzureADAuthority, 29 | Notifications = new OpenIdConnectAuthenticationNotifications() 30 | { 31 | AuthorizationCodeReceived = (context) => 32 | { 33 | var code = context.Code; 34 | var creds = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret); 35 | var userObjectId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value; 36 | //var tokenCache = new NaiveSessionCache(userObjectId); 37 | var tokenCache = new EFADALTokenCache(userObjectId); 38 | 39 | var authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority, tokenCache); 40 | var redirectUri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)); 41 | var authResult = authContext.AcquireTokenByAuthorizationCode( 42 | code, redirectUri, creds, SettingsHelper.GraphResourceId); 43 | return Task.FromResult(0); 44 | }, 45 | AuthenticationFailed = (context) => 46 | { 47 | context.HandleResponse(); 48 | return Task.FromResult(0); 49 | }, 50 | RedirectToIdentityProvider = (context) => 51 | { 52 | // This ensures that the address used for sign in and sign out is picked up dynamically from the request 53 | // this allows you to deploy your app (to Azure Web Sites, for example) without having to change settings 54 | // Remember that the base URL of the address used here must be provisioned in Azure AD beforehand. 55 | string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase; 56 | context.ProtocolMessage.RedirectUri = appBaseUrl + "/"; 57 | context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl; 58 | 59 | return Task.FromResult(0); 60 | } 61 | }, 62 | TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters 63 | { 64 | ValidateIssuer = false 65 | } 66 | }); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /ExpenseManager/Content/Site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Set padding to keep content from hitting the edges */ 7 | .body-content { 8 | padding-left: 15px; 9 | padding-right: 15px; 10 | } 11 | 12 | /* Set width on the form input elements since they're 100% wide by default */ 13 | input, 14 | select, 15 | textarea { 16 | max-width: 280px; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /ExpenseManager/Content/animations.css: -------------------------------------------------------------------------------- 1 | /* Animations */ 2 | .slide-animation-container { 3 | position:relative; 4 | } 5 | 6 | .slide-animation.ng-enter, .slide-animation.ng-leave { 7 | -webkit-transition: 0.5s linear all; 8 | -moz-transition: 0.5s linear all; 9 | -o-transition: 0.5s linear all; 10 | transition: 0.5s linear all; 11 | position:relative; 12 | /*top: 0; 13 | left: 0; 14 | right: 0;*/ 15 | height: 1000px; 16 | } 17 | 18 | .slide-animation.ng-enter { 19 | z-index:100; 20 | left:100px; 21 | opacity:0; 22 | } 23 | 24 | .slide-animation.ng-enter.ng-enter-active { 25 | left:0; 26 | opacity:1; 27 | } 28 | 29 | .slide-animation.ng-leave { 30 | z-index:101; 31 | opacity:1; 32 | left:0; 33 | } 34 | 35 | .slide-animation.ng-leave.ng-leave-active { 36 | left:-100px; 37 | opacity:0; 38 | } 39 | 40 | body.skip-animations * { 41 | -webkit-transition:none!important; 42 | -moz-transition:none!important; 43 | -o-transition:none!important; 44 | transition:none!important; 45 | } 46 | 47 | .show-hide-animation.ng-hide-add, 48 | .show-hide-animation.ng-hide-remove { 49 | -webkit-transition:all linear 0.3s; 50 | -moz-transition:all linear 0.3s; 51 | -o-transition:all linear 0.3s; 52 | transition:all linear 0.3s; 53 | display:block!important; 54 | height: 1000px; 55 | } 56 | 57 | .show-hide-animation.ng-hide-remove { 58 | opacity:0; 59 | } 60 | .show-hide-animation.ng-hide-remove.ng-hide-remove-active { 61 | opacity:1; 62 | } 63 | .show-hide-animation.ng-hide-add { 64 | opacity:1; 65 | } 66 | .show-hide-animation.ng-hide-add.ng-hide-add-active { 67 | opacity:0; 68 | } 69 | 70 | .repeat-animation.ng-enter, 71 | .repeat-animation.ng-leave, 72 | .repeat-animation.ng-move { 73 | -webkit-transition: 0.5s linear all; 74 | -moz-transition: 0.5s linear all; 75 | -o-transition: 0.5s linear all; 76 | transition: 0.5s linear all; 77 | position:relative; 78 | } 79 | 80 | .repeat-animation.ng-enter { 81 | left:10px; 82 | opacity:0; 83 | } 84 | .repeat-animation.ng-enter.ng-enter-active { 85 | left:0; 86 | opacity:1; 87 | } 88 | 89 | .repeat-animation.ng-leave { 90 | left:10px; 91 | opacity:1; 92 | } 93 | .repeat-animation.ng-leave.ng-leave-active { 94 | left:-10px; 95 | opacity:0; 96 | } 97 | 98 | .repeat-animation.ng-move { 99 | opacity:0.5; 100 | } 101 | .repeat-animation.ng-move.ng-move-active { 102 | opacity:1; 103 | } -------------------------------------------------------------------------------- /ExpenseManager/Content/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f3f3f3));background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-o-linear-gradient(top,#222 0,#282828 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#222),to(#282828));background-image:linear-gradient(to bottom,#222 0,#282828 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /ExpenseManager/Content/images/ColorPalette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/ColorPalette.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/angularShield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/angularShield.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/appFolders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/appFolders.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/customerApp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/customerApp.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/expenseBackground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/expenseBackground.jpg -------------------------------------------------------------------------------- /ExpenseManager/Content/images/expensesBackground_Dreamstime.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/expensesBackground_Dreamstime.jpg -------------------------------------------------------------------------------- /ExpenseManager/Content/images/female.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/female.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/male.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/male.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/people.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/1-GoToAdminScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/1-GoToAdminScreen.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/10-selectexpensesandclickok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/10-selectexpensesandclickok.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/11-setdefaultgroups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/11-setdefaultgroups.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/12-HomePageTopLevelSiteWithLists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/12-HomePageTopLevelSiteWithLists.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/2-CreateNewSiteCollection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/2-CreateNewSiteCollection.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/3-FillInSiteCollectionFormChooseTemplateLater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/3-FillInSiteCollectionFormChooseTemplateLater.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/4-clickonsolutiongallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/4-clickonsolutiongallery.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/5-clickUploadSolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/5-clickUploadSolution.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/6-browsetosolutionfromgithubfolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/6-browsetosolutionfromgithubfolder.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/7-ActivateSolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/7-ActivateSolution.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/8-clickbrowse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/8-clickbrowse.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/9-clickcustomtab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/9-clickcustomtab.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/ADPermissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/ADPermissions.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/AddApplication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/AddApplication.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/ApplicationProperties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/ApplicationProperties.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/ClientID.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/ClientID.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/DefaultDirectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/DefaultDirectory.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/ManagementServicesMenuItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/ManagementServicesMenuItem.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/Permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/Permissions.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/channel9scrnsht.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/channel9scrnsht.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/readmeImages/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/readmeImages/screenshot.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/report.png -------------------------------------------------------------------------------- /ExpenseManager/Content/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/Content/images/spinner.gif -------------------------------------------------------------------------------- /ExpenseManager/Content/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Roboto Light'), local('Roboto-Light'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/Pru33qjShpZSmG3z6VYwnT8E0i7KZn-EPnyo3HZu7kw.woff) format('woff'); 6 | } 7 | @font-face { 8 | font-family: 'Roboto'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Roboto Regular'), local('Roboto-Regular'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/Xyjz-jNkfiYuJf8UC3Lizw.woff) format('woff'); 12 | } 13 | @font-face { 14 | font-family: 'Roboto'; 15 | font-style: normal; 16 | font-weight: 500; 17 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/oOeFwZNlrTefzLYmlVV1UD8E0i7KZn-EPnyo3HZu7kw.woff) format('woff'); 18 | } 19 | @font-face { 20 | font-family: 'Roboto'; 21 | font-style: normal; 22 | font-weight: 700; 23 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/97uahxiqZRoncBaCEI3aWz8E0i7KZn-EPnyo3HZu7kw.woff) format('woff'); 24 | } 25 | @font-face { 26 | font-family: 'Roboto'; 27 | font-style: italic; 28 | font-weight: 300; 29 | src: local('Roboto Light Italic'), local('Roboto-LightItalic'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/7m8l7TlFO-S3VkhHuR0at9Ih4imgI8P11RFo6YPCPC0.woff) format('woff'); 30 | } 31 | @font-face { 32 | font-family: 'Roboto'; 33 | font-style: italic; 34 | font-weight: 400; 35 | src: local('Roboto Italic'), local('Roboto-Italic'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/dFWsweFqlD8ExfyN7Gh_GPesZW2xOQ-xsNqO47m55DA.woff) format('woff'); 36 | } 37 | @font-face { 38 | font-family: 'Roboto'; 39 | font-style: italic; 40 | font-weight: 500; 41 | src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/OLffGBTaF0XFOW1gnuHF0dIh4imgI8P11RFo6YPCPC0.woff) format('woff'); 42 | } 43 | @font-face { 44 | font-family: 'Roboto'; 45 | font-style: italic; 46 | font-weight: 700; 47 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), url(https://themes.googleusercontent.com/static/fonts/roboto/v9/t6Nd4cfPRhZP44Q5QAjcC9Ih4imgI8P11RFo6YPCPC0.woff) format('woff'); 48 | } 49 | 50 | html { 51 | overflow-y: scroll; 52 | overflow-x: hidden; 53 | } 54 | 55 | body { 56 | font-family: 'Roboto'; 57 | } 58 | 59 | #ng-view { 60 | position: relative; 61 | padding-top: 50px; 62 | } 63 | 64 | #nav-login { 65 | margin-left:30px; 66 | } 67 | 68 | .appTitle { 69 | line-height:50px; 70 | } 71 | 72 | .view { 73 | 74 | } 75 | 76 | .white { 77 | color: white; 78 | } 79 | 80 | .white:hover{ 81 | color: white; 82 | } 83 | 84 | .card { 85 | background-color:#fff; 86 | border: 1px solid #d4d4d4; 87 | height:100px; 88 | margin-bottom: 20px; 89 | position: relative; 90 | } 91 | 92 | .cardHeader { 93 | background-color:#027FF4; 94 | font-size:12pt; 95 | color:white; 96 | padding:5px; 97 | width:100%; 98 | } 99 | 100 | .cardClose { 101 | color: white; 102 | font-weight:bold; 103 | margin-right:5px; 104 | } 105 | 106 | .cardContainer { 107 | width:85%; 108 | min-height: 400px; 109 | } 110 | 111 | .cardBody { 112 | padding-left: 5px; 113 | } 114 | 115 | .cardBodyLeft { 116 | margin-top: -5px; 117 | } 118 | 119 | .cardBodyRight { 120 | margin-left: 20px; 121 | margin-top: 2px; 122 | } 123 | 124 | .cardBodyContent { 125 | width: 100px; 126 | } 127 | 128 | .cardImage { 129 | height:50px;width:50px;margin-top:10px; 130 | } 131 | 132 | .lastRow { 133 | background-color: #ccc; 134 | } 135 | 136 | .indent { 137 | margin-left:5px; 138 | } 139 | 140 | .expensesTable { 141 | width:85%; 142 | } 143 | 144 | .expenses th, .employees th { 145 | width:20%; 146 | cursor: pointer; 147 | } 148 | 149 | .expensesTable tr:first-child th, .employeesTable tr:first-child th { 150 | background-color: #027FF4; 151 | color: #fff; 152 | font-weight: bold; 153 | } 154 | 155 | .gridContainer td { 156 | vertical-align: middle; 157 | } 158 | 159 | 160 | #submitEmployee { 161 | margin-left:50px; 162 | margin-top: -8px; 163 | } 164 | 165 | footer { 166 | margin-top:10px; 167 | font-weight: bold; 168 | } 169 | 170 | .employeeEdit input[type='text'], 171 | .employeeEdit input[type='number'], 172 | .employeeEdit input[type='email'], 173 | .employeeEdit select, 174 | .login input[type='text'], 175 | .login input[type='email'], 176 | .login input[type='password'] { 177 | width:250px; 178 | } 179 | 180 | .statusRow { 181 | height:50px; 182 | } 183 | 184 | /* Bootstrap overrides */ 185 | 186 | /* Added to fix Bootstrap 3 issue with Angular UI Bootstrap modal dialog*/ 187 | .modal { 188 | display: block; 189 | overflow: hidden; 190 | overflow-y: hidden; 191 | } 192 | 193 | .pagination > .active > a, 194 | .pagination > .active > span, 195 | .pagination > .active > a:hover, 196 | .pagination > .active > span:hover, 197 | .pagination > .active > a:focus, 198 | .pagination > .active > span:focus { 199 | background-color: #027FF4; 200 | border-color: #027FF4; 201 | } 202 | 203 | .cardContainer .col-md-3 { 204 | padding-left: 0px; 205 | } 206 | 207 | .gridContainer div { 208 | padding-left: 0px; 209 | } 210 | 211 | .navbar-brand { 212 | float:none; 213 | } 214 | 215 | a.navbar-brand { 216 | color: #fff; 217 | } 218 | 219 | .navbar-inner { 220 | padding-left: 0px; 221 | -webkit-border-radius: 0px; 222 | border-radius: 0px; 223 | -webkit-box-shadow: none; 224 | -moz-box-shadow: none; 225 | box-shadow: none; 226 | background-color: #027FF4; 227 | background-image: none; 228 | } 229 | 230 | .navbar-inner.toolbar { 231 | background-color: #fafafa; 232 | } 233 | 234 | .navbar-inner.footer { 235 | background-color: #fafafa; 236 | -webkit-box-shadow: none; 237 | -moz-box-shadow: none; 238 | box-shadow: none; 239 | height:50px; 240 | } 241 | 242 | .navbar .nav > .active > a, .navbar .nav > .active > a:hover, .navbar .nav > .active > a:focus { 243 | background-color: #efefef; 244 | -webkit-box-shadow: none; 245 | box-shadow: none; 246 | color: #808080; 247 | } 248 | 249 | .navbar .nav li.toolbaritem a:hover, .navbar .nav li a:hover { 250 | color: #E03930; 251 | } 252 | 253 | .navbar .nav > li { 254 | cursor:pointer; 255 | } 256 | 257 | .navbar .nav > li > a { 258 | color: white; 259 | font-weight:bold; 260 | -webkit-text-shadow: none; 261 | text-shadow: none; 262 | height:30px; 263 | padding-top: 6px; 264 | padding-bottom: 0px; 265 | } 266 | 267 | .navbar .nav > li.toolbaritem > a { 268 | color: black; 269 | font-weight:bold; 270 | -webkit-text-shadow: none; 271 | text-shadow: none; 272 | } 273 | 274 | 275 | .navbar-fixed-top .navbar-inner, 276 | .navbar-static-top .navbar-inner { 277 | -webkit-box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 278 | -moz-box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 279 | box-shadow: 0 1px 00px rgba(0, 0, 0, 0); 280 | } 281 | 282 | .nav.navBarPadding { 283 | margin-left:25px; 284 | margin-top: 10px; 285 | } 286 | 287 | .navbarText { 288 | font-weight:bold; 289 | } 290 | 291 | .navbar .brand { 292 | margin-top: 2px; 293 | color: #fff; 294 | -webkit-text-shadow: none; 295 | text-shadow: none; 296 | } 297 | 298 | .navbar-toggle { 299 | border: 1px solid white; 300 | } 301 | 302 | .navbar-toggle .icon-bar { 303 | background-color: white; 304 | } 305 | 306 | .close { 307 | font-size:15pt; 308 | opacity: 1.0; 309 | } 310 | 311 | .modal-backdrop, .modal-backdrop.fade.in { 312 | opacity: 0.5; 313 | filter: alpha(opacity=50); 314 | } 315 | 316 | .btn.active { 317 | background-color: #f7f7f7; 318 | } 319 | 320 | /* ng-cloak */ 321 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng.ng-cloak { 322 | display: none; 323 | } 324 | 325 | .novalidate { 326 | border-left: none !important; 327 | } 328 | 329 | input.ng-invalid, select.ng-invalid, input.ng-invalid-required, select.ng-invalid.required { 330 | border-left: 5px solid #E03930; 331 | } 332 | 333 | input.ng-valid, select.ng-valid, input.ng-valid-required, select.ng-valid.required { 334 | border-left: 5px solid #57A83F; 335 | } 336 | 337 | .errorMessage { 338 | position:absolute; 339 | padding: 5px; 340 | background-color: #E03930; 341 | color:white; 342 | top:0px; 343 | left:270px; 344 | } 345 | 346 | .editIcon { 347 | margin-top: 2px; 348 | } 349 | 350 | .pagination li { 351 | cursor: pointer; 352 | } 353 | 354 | /* wcOverlay directive CSS styles */ 355 | .overlayContainer { display: none;} 356 | .overlayBackground { top:0px; left:0px; padding-left:100px;position:absolute; z-index:1000;height:100%;width:100%;background-color:#808080;opacity:0.3;} 357 | .overlayContent { position:absolute; border: 1px solid #000; background-color:#fff; font-weight: bold;height: 100px;width: 300px;z-index:1000;text-align:center;} 358 | 359 | 360 | @media screen and (max-width: 640px) { 361 | 362 | navbar-toggle { 363 | display: none; 364 | } 365 | 366 | #ng-view { 367 | padding-top: 90px; 368 | } 369 | 370 | .navbar-brand { 371 | font-size: 12pt; 372 | } 373 | 374 | .nav.navBarPadding { 375 | margin-left: -10px; 376 | } 377 | 378 | .nav > li > a { 379 | padding-right: 5px; 380 | padding-left: 5px; 381 | } 382 | 383 | } -------------------------------------------------------------------------------- /ExpenseManager/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using ExpenseManager.SharePointHelpers; 2 | using ExpenseManager.Utils; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Security.Claims; 8 | using System.Threading.Tasks; 9 | using System.Web; 10 | using System.Web.Mvc; 11 | 12 | namespace ExpenseManager.Controllers 13 | { 14 | [Authorize] 15 | public class HomeController : Controller 16 | { 17 | [AllowAnonymous] 18 | public ActionResult Index() 19 | { 20 | ViewBag.Title = "Employee Expenses"; 21 | return View(); 22 | } 23 | 24 | //Will trigger AD authentication due to Authorize attribute on the controller 25 | public ActionResult Login() 26 | { 27 | return new RedirectResult("/index.html"); 28 | } 29 | 30 | public ActionResult RefreshToken(string returnUrl) 31 | { 32 | SharePointAuth.RefreshSession(); 33 | return new RedirectResult(Server.UrlDecode(returnUrl)); 34 | } 35 | 36 | //Quick and dirty way to test that auth is working properly to AD and SharePoint 37 | public async Task Title() 38 | { 39 | var token = await SharePointAuth.GetAccessToken(SettingsHelper.SharePointDomainUri); 40 | WebClient wc = new WebClient(); 41 | wc.Headers.Add("Method", "GET"); 42 | wc.Headers.Add("Accept", "application/json;odata=verbose"); 43 | wc.Headers.Add("Authorization", "Bearer " + token); 44 | var title = await wc.DownloadStringTaskAsync(SettingsHelper.SharePointApiServiceUri + "web/title"); 45 | ViewBag.Title = title; 46 | return View(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /ExpenseManager/ExpenseManager.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectFiles 5 | 600 6 | True 7 | False 8 | True 9 | 10 | False 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | SpecificPage 19 | True 20 | False 21 | False 22 | False 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | True 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ExpenseManager/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="ExpenseManager.MvcApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /ExpenseManager/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Web.Optimization; 7 | using System.Web.Routing; 8 | 9 | namespace ExpenseManager 10 | { 11 | public class MvcApplication : System.Web.HttpApplication 12 | { 13 | protected void Application_Start() 14 | { 15 | AreaRegistration.RegisterAllAreas(); 16 | RouteConfig.RegisterRoutes(RouteTable.Routes); 17 | FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 18 | BundleConfig.RegisterBundles(BundleTable.Bundles); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ExpenseManager/Handlers/WebProxy.ashx: -------------------------------------------------------------------------------- 1 | <%@ WebHandler Language="C#" CodeBehind="WebProxy.ashx.cs" Class="ExpenseManager.Handlers.WebProxy" %> 2 | -------------------------------------------------------------------------------- /ExpenseManager/Handlers/WebProxy.ashx.cs: -------------------------------------------------------------------------------- 1 | using ExpenseManager.SharePointHelpers; 2 | using ExpenseManager.Utils; 3 | using System; 4 | using System.Configuration; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | using System.Web; 10 | using System.Web.SessionState; 11 | 12 | namespace ExpenseManager.Handlers 13 | { 14 | public class WebProxy : HttpTaskAsyncHandler, IRequiresSessionState 15 | { 16 | public override async Task ProcessRequestAsync(HttpContext context) 17 | { 18 | try 19 | { 20 | //Get SharePoint Access Token 21 | var token = await SharePointAuth.GetAccessToken(SettingsHelper.SharePointDomainUri); 22 | 23 | if (!String.IsNullOrEmpty(token)) 24 | { 25 | Debug.WriteLine("Bearer token found: " + token); 26 | HttpWebRequest request = CreateRequest(context, token); 27 | CreateResponse(context, request); 28 | } 29 | else 30 | { 31 | Debug.WriteLine("No bearer token found in WebProxy.ashx.cs"); 32 | //context.Response.StatusCode = (int)HttpStatusCode.Redirect; 33 | context.Response.End(); 34 | } 35 | } 36 | catch (Exception exp) 37 | { 38 | Debug.WriteLine("Error in WebProxy ProcessRequest: " + exp.Message); 39 | } 40 | } 41 | 42 | private HttpWebRequest CreateRequest(HttpContext context, string token) 43 | { 44 | var url = context.Server.UrlDecode(context.Request["url"]); 45 | var contextInfoRequest = context.Request.Headers["ContextInfoRequest"]; 46 | var requestDigest = context.Request.Headers["X-RequestDigest"]; 47 | var ifMatch = context.Request.Headers["If-Match"]; 48 | var restMethod = context.Request.HttpMethod.ToUpper(); 49 | var accept = context.Request.Headers["Accept"]; 50 | var xHttpMethod = context.Request.Headers["X-HTTP-Method"]; 51 | var contentTypeVerbs = new string[] { "PUT", "POST", "MERGE" }; 52 | 53 | Debug.WriteLine(url); 54 | 55 | HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url); 56 | request.Accept = "application/json;odata=verbose"; 57 | request.ContentType = "application/json;odata=verbose"; 58 | request.Method = restMethod; 59 | request.Headers.Add("Authorization", "Bearer " + token); 60 | 61 | if (contextInfoRequest == "true") request.ContentLength = 0; 62 | if (requestDigest != null) request.Headers.Add("X-RequestDigest", requestDigest); 63 | if (ifMatch != null) request.Headers.Add("If-Match", ifMatch); 64 | if (xHttpMethod != null) request.Headers.Add("X-HTTP-Method", xHttpMethod); 65 | 66 | if (Array.IndexOf(contentTypeVerbs, restMethod) >= 0) 67 | { 68 | var bodyStream = context.Request.GetBufferedInputStream(); 69 | var dataStream = request.GetRequestStream(); 70 | bodyStream.CopyTo(dataStream); 71 | } 72 | 73 | return request; 74 | } 75 | 76 | private void CreateResponse(HttpContext context, HttpWebRequest request) 77 | { 78 | HttpWebResponse response = (HttpWebResponse)request.GetResponse(); 79 | using (var stream = response.GetResponseStream()) 80 | { 81 | var ms = new MemoryStream(); 82 | stream.CopyTo(ms); 83 | ms.Seek(0, SeekOrigin.Begin); 84 | if (ms.Length > 0) 85 | { 86 | context.Response.ContentType = "application/json"; 87 | ms.CopyTo(context.Response.OutputStream); 88 | context.Response.Flush(); 89 | } 90 | } 91 | } 92 | 93 | public bool IsReusable 94 | { 95 | get { return false; } 96 | } 97 | 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ExpenseManager/Models/PerWebUserCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Web; 6 | 7 | namespace ExpenseManager.Models 8 | { 9 | public class PerWebUserCache 10 | { 11 | [Key] 12 | public int EntryId { get; set; } 13 | public string webUserUniqueId { get; set; } 14 | public byte[] cacheBits { get; set; } 15 | public DateTime LastWrite { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /ExpenseManager/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ExpenseManager")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ExpenseManager")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("0196de6a-ce36-44d9-b302-7e9f81ca64ba")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /ExpenseManager/Scripts/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /ExpenseManager/SharePointHelpers/SharePointAuth.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 2 | using Microsoft.Office365.Discovery; 3 | using Microsoft.Office365.SharePoint.CoreServices; 4 | using Microsoft.Owin.Security; 5 | using Microsoft.Owin.Security.Cookies; 6 | using Microsoft.Owin.Security.OpenIdConnect; 7 | using ExpenseManager.Utils; 8 | using System; 9 | using System.Net; 10 | using System.Security.Claims; 11 | using System.Threading.Tasks; 12 | using System.Web; 13 | 14 | namespace ExpenseManager.SharePointHelpers 15 | { 16 | public static class SharePointAuth 17 | { 18 | private static DiscoveryClient _DiscoveryClient; 19 | 20 | public static AuthenticationContext GetAuthContext() 21 | { 22 | var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value; 23 | //var tokenCache = new NaiveSessionCache(signInUserId); 24 | var tokenCache = new EFADALTokenCache(signInUserId); 25 | var authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority, tokenCache); 26 | return authContext; 27 | } 28 | 29 | public static async Task GetAccessToken(string resource) 30 | { 31 | var userObjectId = ClaimsPrincipal.Current.FindFirst(SettingsHelper.ClaimsObjectIdentifier).Value; 32 | var clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret); 33 | var userIdentifier = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId); 34 | var authContext = GetAuthContext(); 35 | 36 | var authResult = await authContext.AcquireTokenSilentAsync(resource, clientCredential, userIdentifier); 37 | return authResult.AccessToken; 38 | } 39 | 40 | public static async Task GetDiscoveryClient(string capability) 41 | { 42 | var userObjectId = ClaimsPrincipal.Current.FindFirst(SettingsHelper.ClaimsObjectIdentifier).Value; 43 | var clientCredential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret); 44 | var userIdentifier = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId); 45 | var authContext = GetAuthContext(); 46 | 47 | _DiscoveryClient = new DiscoveryClient(new Uri(SettingsHelper.O365DiscoveryServiceEndpoint), 48 | async () => 49 | { 50 | return await GetAccessToken(SettingsHelper.O365DiscoveryResourceId); 51 | }); 52 | 53 | var dcr = await _DiscoveryClient.DiscoverCapabilityAsync(capability); 54 | 55 | return _DiscoveryClient; 56 | } 57 | 58 | //public static async Task GetSharePointClient() 59 | //{ 60 | // if (_DiscoveryClient == null) 61 | // { 62 | // _DiscoveryClient = await GetDiscoveryClient(); 63 | // } 64 | 65 | // return new SharePointClient(new Uri(SettingsHelper.SharePointApiServiceUri), async () => 66 | // { 67 | // return await GetAccessToken(SettingsHelper.SharePointDomainUri); 68 | // }); 69 | //} 70 | 71 | public static void SignIn() 72 | { 73 | if (!HttpContext.Current.Request.IsAuthenticated) 74 | { 75 | HttpContext.Current.GetOwinContext().Authentication.Challenge( 76 | new AuthenticationProperties { RedirectUri = "/" }, 77 | OpenIdConnectAuthenticationDefaults.AuthenticationType); 78 | } 79 | } 80 | 81 | public static void Signout() 82 | { 83 | string usrObjectId = ClaimsPrincipal.Current.FindFirst(SettingsHelper.ClaimsObjectIdentifier).Value; 84 | AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority, new NaiveSessionCache(usrObjectId)); 85 | authContext.TokenCache.Clear(); 86 | 87 | HttpContext.Current.GetOwinContext().Authentication.SignOut( 88 | OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType); 89 | } 90 | 91 | public static void RefreshSession() 92 | { 93 | string strRedirectController = HttpContext.Current.Request.QueryString["redirect"]; 94 | 95 | HttpContext.Current.GetOwinContext().Authentication.Challenge( 96 | new AuthenticationProperties { RedirectUri = String.Format("/{0}", strRedirectController) }, 97 | OpenIdConnectAuthenticationDefaults.AuthenticationType); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /ExpenseManager/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Owin; 2 | using Owin; 3 | 4 | [assembly: OwinStartup(typeof(ExpenseManager.Startup))] 5 | namespace ExpenseManager 6 | { 7 | public partial class Startup 8 | { 9 | public void Configuration(IAppBuilder app) 10 | { 11 | ConfigureAuth(app); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /ExpenseManager/Utils/EFADALContext.cs: -------------------------------------------------------------------------------- 1 | using ExpenseManager.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data.Entity; 5 | using System.Data.Entity.ModelConfiguration.Conventions; 6 | using System.Linq; 7 | using System.Web; 8 | 9 | namespace ExpenseManager.Utils 10 | { 11 | public class EFADALContext : DbContext 12 | { 13 | public EFADALContext() : base("EFADALContext") 14 | { 15 | } 16 | 17 | public DbSet PerUserCacheList { get; set; } 18 | 19 | protected override void OnModelCreating(DbModelBuilder modelBuilder) 20 | { 21 | modelBuilder.Conventions.Remove(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /ExpenseManager/Utils/EFADALTokenCache.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 2 | using ExpenseManager.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data.Entity; 6 | using System.Linq; 7 | using System.Web; 8 | 9 | namespace ExpenseManager.Utils 10 | { 11 | public class EFADALTokenCache : TokenCache 12 | { 13 | private EFADALContext _Context = new EFADALContext(); 14 | string User; 15 | PerWebUserCache Cache; 16 | 17 | // constructor 18 | public EFADALTokenCache(string user) 19 | { 20 | // associate the cache to the current user of the web app 21 | User = user; 22 | 23 | this.AfterAccess = AfterAccessNotification; 24 | this.BeforeAccess = BeforeAccessNotification; 25 | this.BeforeWrite = BeforeWriteNotification; 26 | 27 | // look up the entry in the DB 28 | Cache = _Context.PerUserCacheList.FirstOrDefault(c => c.webUserUniqueId == User); 29 | // place the entry in memory 30 | this.Deserialize((Cache == null) ? null : Cache.cacheBits); 31 | } 32 | 33 | // clean up the DB 34 | public override void Clear() 35 | { 36 | base.Clear(); 37 | foreach (var cacheEntry in _Context.PerUserCacheList) 38 | _Context.PerUserCacheList.Remove(cacheEntry); 39 | _Context.SaveChanges(); 40 | } 41 | 42 | // Notification raised before ADAL accesses the cache. 43 | // This is your chance to update the in-memory copy from the DB, if the in-memory version is stale 44 | void BeforeAccessNotification(TokenCacheNotificationArgs args) 45 | { 46 | if (Cache == null) 47 | { 48 | // first time access 49 | Cache = _Context.PerUserCacheList.FirstOrDefault(c => c.webUserUniqueId == User); 50 | } 51 | else 52 | { // retrieve last write from the DB 53 | var status = from e in _Context.PerUserCacheList 54 | where (e.webUserUniqueId == User) 55 | select new 56 | { 57 | LastWrite = e.LastWrite 58 | }; 59 | // if the in-memory copy is older than the persistent copy 60 | if (status.First().LastWrite > Cache.LastWrite) 61 | //// read from from storage, update in-memory copy 62 | { 63 | Cache = _Context.PerUserCacheList.FirstOrDefault(c => c.webUserUniqueId == User); 64 | } 65 | } 66 | 67 | 68 | this.Deserialize((Cache == null) ? null : Cache.cacheBits); 69 | } 70 | // Notification raised after ADAL accessed the cache. 71 | // If the HasStateChanged flag is set, ADAL changed the content of the cache 72 | void AfterAccessNotification(TokenCacheNotificationArgs args) 73 | { 74 | // if state changed 75 | if (this.HasStateChanged) 76 | { 77 | Cache = new PerWebUserCache 78 | { 79 | webUserUniqueId = User, 80 | cacheBits = this.Serialize(), 81 | LastWrite = DateTime.Now 82 | }; 83 | //// update the DB and the lastwrite 84 | _Context.Entry(Cache).State = Cache.EntryId == 0 ? EntityState.Added : EntityState.Modified; 85 | _Context.SaveChanges(); 86 | this.HasStateChanged = false; 87 | } 88 | } 89 | void BeforeWriteNotification(TokenCacheNotificationArgs args) 90 | { 91 | // if you want to ensure that no concurrent write take place, use this notification to place a lock on the entry 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /ExpenseManager/Utils/NaiveSessionCache.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Web; 7 | 8 | namespace ExpenseManager.Utils 9 | { 10 | 11 | public class NaiveSessionCache : TokenCache 12 | { 13 | private static readonly object FileLock = new object(); 14 | string UserObjectId = string.Empty; 15 | string CacheId = string.Empty; 16 | public NaiveSessionCache(string userId) 17 | { 18 | UserObjectId = userId; 19 | CacheId = UserObjectId + "_TokenCache"; 20 | 21 | this.AfterAccess = AfterAccessNotification; 22 | this.BeforeAccess = BeforeAccessNotification; 23 | Load(); 24 | } 25 | 26 | public void Load() 27 | { 28 | lock (FileLock) 29 | { 30 | this.Deserialize((byte[])HttpContext.Current.Session[CacheId]); 31 | } 32 | } 33 | 34 | public void Persist() 35 | { 36 | lock (FileLock) 37 | { 38 | // reflect changes in the persistent store 39 | HttpContext.Current.Session[CacheId] = this.Serialize(); 40 | // once the write operation took place, restore the HasStateChanged bit to false 41 | this.HasStateChanged = false; 42 | } 43 | } 44 | 45 | // Empties the persistent store. 46 | public override void Clear() 47 | { 48 | base.Clear(); 49 | System.Web.HttpContext.Current.Session.Remove(CacheId); 50 | } 51 | 52 | public override void DeleteItem(TokenCacheItem item) 53 | { 54 | base.DeleteItem(item); 55 | Persist(); 56 | } 57 | 58 | // Triggered right before ADAL needs to access the cache. 59 | // Reload the cache from the persistent store in case it changed since the last access. 60 | void BeforeAccessNotification(TokenCacheNotificationArgs args) 61 | { 62 | Load(); 63 | } 64 | 65 | // Triggered right after ADAL accessed the cache. 66 | void AfterAccessNotification(TokenCacheNotificationArgs args) 67 | { 68 | // if the access operation resulted in a cache update 69 | if (this.HasStateChanged) 70 | { 71 | Persist(); 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /ExpenseManager/Utils/SettingsHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Linq; 5 | using System.Web; 6 | 7 | namespace ExpenseManager.Utils 8 | { 9 | internal class SettingsHelper 10 | { 11 | static string _AzureADUri = "https://login.windows.net"; 12 | static string _GraphResourceId = "https://graph.windows.net"; 13 | static string _O365DiscoveryServiceEndpoint = "https://api.office.com/discovery/v1.0/me/"; 14 | static string _O365DiscoveryResourceId = "https://api.office.com/discovery/"; 15 | static string _ClaimsObjectIdentifier = "http://schemas.microsoft.com/identity/claims/objectidentifier"; 16 | static string _ContactsCapability = "Contacts"; 17 | 18 | public static string Tenant 19 | { 20 | get { return ConfigurationManager.AppSettings["ida:Tenant"]; } 21 | } 22 | 23 | public static string TenantId 24 | { 25 | get { return ConfigurationManager.AppSettings["ida:TenantID"]; } 26 | } 27 | 28 | public static string ClientId 29 | { 30 | get { return ConfigurationManager.AppSettings["ida:ClientID"]; } 31 | } 32 | 33 | public static string ClientSecret 34 | { 35 | get { return ConfigurationManager.AppSettings["ida:Password"]; } 36 | } 37 | 38 | public static string O365DiscoveryResourceId 39 | { 40 | get { return _O365DiscoveryResourceId; } 41 | } 42 | 43 | public static string O365DiscoveryServiceEndpoint 44 | { 45 | get { return _O365DiscoveryServiceEndpoint; } 46 | } 47 | 48 | public static string GraphResourceId 49 | { 50 | get { return _GraphResourceId; } 51 | } 52 | 53 | public static string AzureADAuthority 54 | { 55 | get { return string.Format(_AzureADUri + "/{0}/", TenantId); } 56 | } 57 | 58 | public static string ClaimsObjectIdentifier 59 | { 60 | get { return _ClaimsObjectIdentifier; } 61 | } 62 | 63 | public static string ContactsCapability 64 | { 65 | get { return _ContactsCapability; } 66 | } 67 | 68 | public static string SharePointDomainUri 69 | { 70 | get { return String.Format(ConfigurationManager.AppSettings["SharePointDomainUri"], Tenant); } 71 | } 72 | 73 | public static string SharePointApiServiceUri 74 | { 75 | get { return String.Format(ConfigurationManager.AppSettings["SharePointApiServiceUri"], Tenant); } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /ExpenseManager/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 |  44 | 45 |
46 |
47 | Login 48 |
49 | 60 | -------------------------------------------------------------------------------- /ExpenseManager/Views/Home/Title.cshtml: -------------------------------------------------------------------------------- 1 | 

@ViewBag.Title

2 |
3 | 4 | 5 | 32 | 33 | -------------------------------------------------------------------------------- /ExpenseManager/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Web.Mvc.HandleErrorInfo 2 | @{ 3 | Layout = null; 4 | } 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 |
14 |

Error.

15 |

An error occurred while processing your request.

16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /ExpenseManager/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewBag.Title 7 | @Styles.Render("~/Content/css") 8 | @Scripts.Render("~/bundles/modernizr") 9 | 10 | 11 | @RenderBody() 12 | 13 | -------------------------------------------------------------------------------- /ExpenseManager/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "~/Views/Shared/_Layout.cshtml"; 3 | } -------------------------------------------------------------------------------- /ExpenseManager/Views/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /ExpenseManager/Web.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /ExpenseManager/Web.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /ExpenseManager/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/animations/listAnimations.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var wcAnimations = function () { 4 | var duration = 0.5; 5 | return { 6 | enter: function (element, done) { 7 | var random = Math.random() * 100; 8 | TweenMax.set(element, { opacity: 0, left: random + 'px' }); 9 | 10 | var random2 = Math.random(); 11 | TweenMax.to(element, duration, { opacity: 1, left: '0px', ease: Back.easeInOut, delay: random2, onComplete: done }); 12 | }, 13 | leave: function (element, done) { 14 | TweenMax.to(element, duration, { opacity: 0, left: '-50px', onComplete: done }); 15 | } 16 | }; 17 | }; 18 | 19 | angular.module('expenseApp').animation('.card-animation', wcAnimations); 20 | 21 | }()); 22 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var app = angular.module('expenseApp', 4 | ['ngRoute', 'ngAnimate', 'wc.directives', 'ui.bootstrap']); 5 | 6 | app.config(['$routeProvider', function ($routeProvider) { 7 | var viewBase = '/app/expenseApp/views/'; 8 | 9 | $routeProvider 10 | .when('/employees', { 11 | controller: 'EmployeesController', 12 | templateUrl: viewBase + 'employees/employees.html', 13 | controllerAs: 'vm' 14 | }) 15 | .when('/employeeExpenses/:employeeId', { 16 | controller: 'EmployeeExpensesController', 17 | templateUrl: viewBase + 'employees/employeeExpenses.html', 18 | controllerAs: 'vm' 19 | }) 20 | .when('/employeeEdit/:employeeId', { 21 | controller: 'EmployeeEditController', 22 | templateUrl: viewBase + 'employees/employeeEdit.html', 23 | controllerAs: 'vm' 24 | }) 25 | .when('/expenses', { 26 | controller: 'ExpensesController', 27 | templateUrl: viewBase + 'expenses/expenses.html', 28 | controllerAs: 'vm' 29 | }) 30 | .when('/about', { 31 | controller: 'AboutController', 32 | templateUrl: viewBase + 'about.html' 33 | }) 34 | .otherwise({ redirectTo: '/employees' }); 35 | }]); 36 | 37 | }()); 38 | 39 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/controllers/aboutController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var AboutController = function ($scope) { 4 | 5 | }; 6 | 7 | AboutController.$inject = ['$scope']; 8 | 9 | angular.module('expenseApp').controller('AboutController', AboutController); 10 | 11 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/controllers/employees/employeeEditController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var EmployeeEditController = function ($scope, $location, $routeParams, $timeout, config, dataService, modalService) { 4 | var vm = this; 5 | 6 | var employeeId = ($routeParams.employeeId) ? parseInt($routeParams.employeeId) : 0, 7 | timer, 8 | onRouteChangeOff; 9 | 10 | vm.employee = {}; 11 | vm.employee.id = employeeId; 12 | vm.states = []; 13 | vm.title = (employeeId > 0) ? 'Edit' : 'Add'; 14 | vm.buttonText = (employeeId > 0) ? 'Update' : 'Add'; 15 | vm.updateStatus = false; 16 | vm.errorMessage = ''; 17 | 18 | vm.isStateSelected = function (employeeStateId, stateId) { 19 | return employeeStateId === stateId; 20 | }; 21 | 22 | vm.saveEmployee = function () { 23 | if ($scope.editForm.$valid) { 24 | if (!vm.employee.id) { 25 | dataService.insertEmployee(vm.employee).then(function (insertedEmployee) { 26 | vm.employee = insertedEmployee; 27 | processSuccess(); 28 | }, 29 | processError); 30 | } 31 | else { 32 | dataService.updateEmployee(vm.employee).then(processSuccess, processError); 33 | } 34 | } 35 | }; 36 | 37 | vm.deleteEmployee = function () { 38 | var empName = vm.employee.firstName + ' ' + vm.employee.lastName; 39 | var modalOptions = { 40 | closeButtonText: 'Cancel', 41 | actionButtonText: 'Delete Employee', 42 | headerText: 'Delete ' + empName + '?', 43 | bodyText: 'Are you sure you want to delete this employee?' 44 | }; 45 | 46 | modalService.showModal({}, modalOptions).then(function (result) { 47 | if (result === 'ok') { 48 | dataService.deleteEmployee(vm.employee).then(function () { 49 | onRouteChangeOff(); //Stop listening for location changes 50 | $location.path('/employees'); 51 | }, processError); 52 | } 53 | }); 54 | }; 55 | 56 | function init() { 57 | 58 | getStates().then(function () { 59 | if (employeeId > 0) { 60 | dataService.getEmployee(employeeId).then(function (employee) { 61 | vm.employee = employee; 62 | }, processError); 63 | } 64 | }); 65 | 66 | 67 | 68 | //Make sure they're warned if they made a change but didn't save it 69 | //Call to $on returns a "deregistration" function that can be called to 70 | //remove the listener (see routeChange() for an example of using it) 71 | onRouteChangeOff = $scope.$on('$locationChangeStart', routeChange); 72 | } 73 | 74 | init(); 75 | 76 | function routeChange(event, newUrl) { 77 | //Navigate to newUrl if the form isn't dirty 78 | if (!vm.editForm || !vm.editForm.$dirty) return; 79 | 80 | var modalOptions = { 81 | closeButtonText: 'Cancel', 82 | actionButtonText: 'Ignore Changes', 83 | headerText: 'Unsaved Changes', 84 | bodyText: 'You have unsaved changes. Leave the page?' 85 | }; 86 | 87 | modalService.showModal({}, modalOptions).then(function (result) { 88 | if (result === 'ok') { 89 | onRouteChangeOff(); //Stop listening for location changes 90 | $location.path($location.url(newUrl).hash()); //Go to page they're interested in 91 | } 92 | }); 93 | 94 | //prevent navigation by default since we'll handle it 95 | //once the user selects a dialog option 96 | event.preventDefault(); 97 | return; 98 | } 99 | 100 | function getStates() { 101 | return dataService.getStates().then(function (states) { 102 | vm.states = states; 103 | }, processError); 104 | } 105 | 106 | function processSuccess() { 107 | $scope.editForm.$dirty = false; 108 | vm.updateStatus = true; 109 | vm.title = 'Edit'; 110 | vm.buttonText = 'Update'; 111 | startTimer(); 112 | } 113 | 114 | function processError(error) { 115 | vm.errorMessage = error.message; 116 | startTimer(); 117 | } 118 | 119 | function startTimer() { 120 | timer = $timeout(function () { 121 | $timeout.cancel(timer); 122 | vm.errorMessage = ''; 123 | vm.updateStatus = false; 124 | }, 3000); 125 | } 126 | }; 127 | 128 | EmployeeEditController.$inject = ['$scope', '$location', '$routeParams', 129 | '$timeout', 'config', 'dataService', 'modalService']; 130 | 131 | angular.module('expenseApp').controller('EmployeeEditController', EmployeeEditController); 132 | 133 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/controllers/employees/employeeExpensesController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var EmployeeExpensesController = function ($scope, $routeParams, $window, dataService) { 4 | var vm = this; 5 | //Grab employeeId off of the route 6 | var employeeId = ($routeParams.employeeId) ? parseInt($routeParams.employeeId) : 0; 7 | 8 | vm.employee = {}; 9 | vm.expensesTotal = 0.00; 10 | 11 | init(); 12 | 13 | function init() { 14 | if (employeeId > 0) { 15 | dataService.getEmployeeExpenses(employeeId) 16 | .then(function (employee) { 17 | vm.employee = employee; 18 | $scope.$broadcast('employee', employee); 19 | }, function (error) { 20 | $window.alert("Sorry, an error occurred: " + error.message); 21 | }); 22 | } 23 | } 24 | }; 25 | 26 | EmployeeExpensesController.$inject = ['$scope', '$routeParams', '$window', 'dataService']; 27 | 28 | angular.module('expenseApp').controller('EmployeeExpensesController', EmployeeExpensesController); 29 | 30 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/controllers/employees/employeesController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var EmployeesController = function ($location, $filter, $window, $timeout, dataService, modalService) { 4 | var vm = this; 5 | 6 | vm.employees = []; 7 | vm.filteredEmployees = []; 8 | vm.pagedEmployees = []; 9 | vm.filteredCount = 0; 10 | vm.orderby = 'lastName'; 11 | vm.reverse = false; 12 | vm.searchText = null; 13 | vm.cardAnimationClass = 'card-animation'; 14 | 15 | //paging 16 | vm.totalRecords = 0; 17 | vm.pageSize = 10; 18 | vm.currentPage = 1; 19 | vm.numRecordsDisplaying; 20 | 21 | vm.pageChanged = function (page) { 22 | vm.currentPage = page; 23 | pageRecords(); 24 | }; 25 | 26 | vm.deleteEmployee = function (id) { 27 | 28 | var emp = getEmployeeById(id); 29 | var empName = emp.firstName + ' ' + emp.lastName; 30 | 31 | var modalOptions = { 32 | closeButtonText: 'Cancel', 33 | actionButtonText: 'Delete Employee', 34 | headerText: 'Delete ' + empName + '?', 35 | bodyText: 'Are you sure you want to delete this employee?' 36 | }; 37 | 38 | modalService.showModal({}, modalOptions).then(function (result) { 39 | if (result === 'ok') { 40 | dataService.deleteEmployee(emp).then(function () { 41 | for (var i = 0; i < vm.employees.length; i++) { 42 | if (vm.employees[i].id == id) { 43 | vm.employees.splice(i, 1); 44 | break; 45 | } 46 | } 47 | filterEmployees(vm.searchText); 48 | }, function (error) { 49 | $window.alert('Error deleting employee: ' + error.message); 50 | }); 51 | } 52 | }); 53 | }; 54 | 55 | vm.DisplayModeEnum = { 56 | Card: 0, 57 | List: 1 58 | }; 59 | 60 | vm.changeDisplayMode = function (displayMode) { 61 | switch (displayMode) { 62 | case vm.DisplayModeEnum.Card: 63 | vm.listDisplayModeEnabled = false; 64 | break; 65 | case vm.DisplayModeEnum.List: 66 | vm.listDisplayModeEnabled = true; 67 | break; 68 | } 69 | }; 70 | 71 | vm.navigate = function (url) { 72 | $location.path(url); 73 | }; 74 | 75 | vm.setOrder = function (orderby) { 76 | if (orderby === vm.orderby) { 77 | vm.reverse = !vm.reverse; 78 | } 79 | vm.orderby = orderby; 80 | }; 81 | 82 | vm.searchTextChanged = function () { 83 | filterEmployees(); 84 | }; 85 | 86 | function init() { 87 | getEmployeesSummary(); 88 | } 89 | 90 | function getEmployeesSummary() { 91 | dataService.getEmployeesSummary(vm.currentPage - 1, vm.pageSize) 92 | .then(function (data) { 93 | vm.totalRecords = data.totalRecords; 94 | vm.employees = data.results; 95 | filterEmployees(''); //Trigger initial filter 96 | 97 | $timeout(function() { 98 | vm.cardAnimationClass = ''; //Turn off animation 99 | }, 1000); 100 | 101 | }, function (error) { 102 | $window.alert('Sorry, an error occurred: ' + error.data.message); 103 | }); 104 | } 105 | 106 | function filterEmployees() { 107 | vm.filteredEmployees = $filter("nameCityStateFilter")(vm.employees, vm.searchText); 108 | vm.filteredCount = vm.filteredEmployees.length; 109 | 110 | //Factor in paging 111 | vm.currentPage = 1; 112 | vm.totalRecords = vm.filteredCount; 113 | pageRecords(); 114 | } 115 | 116 | function pageRecords() { 117 | var useFiltered = vm.searchText && vm.searchText.length > 0, 118 | pageStart = (vm.currentPage - 1) * vm.pageSize, 119 | pageEnd = pageStart + vm.pageSize; 120 | 121 | if (useFiltered) { 122 | if (pageEnd > vm.filteredCount) pageEnd = vm.filteredCount; 123 | } 124 | else { 125 | if (pageEnd > vm.employees.length) pageEnd = vm.employees.length; 126 | vm.totalRecords = vm.employees.length; 127 | } 128 | 129 | vm.pagedEmployees = (useFiltered) ? vm.filteredEmployees.slice(pageStart, pageEnd) : vm.employees.slice(pageStart, pageEnd); 130 | vm.numRecordsDisplaying = vm.pagedEmployees.length; 131 | vm.pagingInfo = { 132 | currentPage: vm.currentPage, 133 | totalRecords: vm.totalRecords, 134 | pageStart: pageStart, 135 | pageEnd: pageEnd, 136 | pagedEmployeeLength: vm.pagedEmployees.length, 137 | numRecordsDisplaying: vm.numRecordsDisplaying 138 | }; 139 | } 140 | 141 | function getEmployeeById(id) { 142 | for (var i = 0; i < vm.employees.length; i++) { 143 | var emp = vm.employees[i]; 144 | if (emp.id === id) { 145 | return emp; 146 | } 147 | } 148 | return null; 149 | } 150 | 151 | init(); 152 | }; 153 | 154 | EmployeesController.$inject = ['$location', '$filter', '$window', '$timeout', 'dataService', 'modalService']; 155 | 156 | angular.module('expenseApp').controller('EmployeesController', EmployeesController); 157 | 158 | }()); 159 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/controllers/expenses/expenseChildController.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var ExpenseChildController = function ($scope) { 4 | var vm = this; 5 | 6 | vm.employee = null; 7 | vm.orderby = 'product'; 8 | vm.reverse = false; 9 | vm.expensesTotal = 0.00; 10 | 11 | init(); 12 | 13 | vm.setOrder = function (orderby) { 14 | if (orderby === vm.orderby) { 15 | vm.reverse = !vm.reverse; 16 | } 17 | vm.orderby = orderby; 18 | }; 19 | 20 | function init() { 21 | //See if parent $scope has an employee that's inherited (ExpensesController) 22 | if ($scope.employee) { 23 | vm.employee = $scope.employee; 24 | updateTotal($scope.employee); 25 | //Employee not available yet so listen for availability (EmployeeExpensesController) 26 | } else { 27 | $scope.$on('employee', function (event, employee) { 28 | vm.employee = employee; 29 | updateTotal(employee); 30 | }) 31 | } 32 | } 33 | 34 | function updateTotal(employee) { 35 | if (employee && employee.expenses) { 36 | var total = 0.00; 37 | for (var i = 0; i < employee.expenses.length; i++) { 38 | var order = employee.expenses[i]; 39 | total += order.orderTotal; 40 | } 41 | vm.expensesTotal = total; 42 | } 43 | } 44 | }; 45 | 46 | ExpenseChildController.$inject = ['$scope']; 47 | 48 | angular.module('expenseApp').controller('ExpenseChildController', ExpenseChildController); 49 | 50 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/controllers/expenses/expensesController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var ExpensesController = function ($filter, $window, dataService) { 4 | var vm = this; 5 | 6 | vm.employees = []; 7 | vm.filteredEmployees = []; 8 | vm.pagedEmployees = []; 9 | vm.filteredCount = 0; 10 | vm.searchText = null; 11 | 12 | //paging 13 | vm.totalRecords = 0; 14 | vm.pageSize = 5; 15 | vm.currentPage = 1; 16 | vm.numRecordsDisplaying; 17 | 18 | vm.pageChanged = function (page) { 19 | vm.currentPage = page; 20 | pageRecords(); 21 | }; 22 | 23 | vm.searchTextChanged = function () { 24 | filterEmployeesExpenses(vm.searchText); 25 | }; 26 | 27 | function init() { 28 | getEmployeesAndExpenses(); 29 | } 30 | 31 | function filterEmployeesExpenses(filterText) { 32 | vm.filteredEmployees = $filter("nameExpenseFilter")(vm.employees, filterText); 33 | vm.filteredCount = vm.filteredEmployees.length; 34 | 35 | //Factor in paging 36 | vm.currentPage = 1; 37 | vm.totalRecords = vm.filteredCount; 38 | pageRecords(); 39 | } 40 | 41 | function pageRecords() { 42 | var useFiltered = vm.searchText && vm.searchText.length > 0, 43 | pageStart = (vm.currentPage - 1) * vm.pageSize, 44 | pageEnd = pageStart + vm.pageSize; 45 | 46 | if (useFiltered) { 47 | if (pageEnd > vm.filteredCount) pageEnd = vm.filteredCount; 48 | } 49 | else { 50 | if (pageEnd > vm.employees.length) pageEnd = vm.employees.length; 51 | vm.totalRecords = vm.employees.length; 52 | } 53 | 54 | vm.pagedEmployees = (useFiltered) ? vm.filteredEmployees.slice(pageStart, pageEnd) : vm.employees.slice(pageStart, pageEnd); 55 | vm.numRecordsDisplaying = vm.pagedEmployees.length; 56 | } 57 | 58 | function getEmployeesAndExpenses() { 59 | dataService.getEmployeesAndExpenses() 60 | .then(function (employees) { 61 | vm.totalRecords = employees.length; 62 | vm.employees = employees; 63 | filterEmployeesExpenses(''); 64 | }, function (error) { 65 | $window.alert(error.message); 66 | }); 67 | } 68 | 69 | init(); 70 | 71 | }; 72 | 73 | ExpensesController.$inject = ['$filter', '$window', 'dataService']; 74 | 75 | angular.module('expenseApp').controller('ExpensesController', ExpensesController); 76 | 77 | }()); 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/controllers/navbarController.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var NavbarController = function () { 4 | var vm = this; 5 | vm.isCollapsed = false; 6 | vm.appTitle = 'Expense Management'; 7 | }; 8 | 9 | NavbarController.$inject = ['$scope']; 10 | 11 | angular.module('expenseApp').controller('NavbarController', NavbarController); 12 | 13 | }()); 14 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/directives/lazyLoader.js: -------------------------------------------------------------------------------- 1 | // 2 | (function () { 3 | // use this directive to delay assigning user input to the underlying 4 | // view model field until focus leaves the dom element. Useful for 5 | // delaying breeze validation until user enters all data (like when 6 | // entering dates) 7 | var lazyLoad = function () { 8 | return { 9 | restrict: 'A', //E = element, A = attribute, C = class, M = comment 10 | transclude: true, 11 | scope: { 12 | name: '@' 13 | }, 14 | link: function (scope, element, attrs) { 15 | element.bind('blur', function (e) { 16 | scope.$parent.$apply(function () { 17 | scope.$parent[scope.name] = element.val(); 18 | }); 19 | }); 20 | } 21 | }; 22 | }; 23 | 24 | angular.module('customersApp').directive('lazyLoad', lazyLoad); 25 | 26 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/directives/wcUnique.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var wcUniqueDirective = function ($parse, dataService) { 4 | return { 5 | restrict: 'A', 6 | require: 'ngModel', 7 | link: function (scope, element, attrs, ngModel) { 8 | element.bind('blur', function (e) { 9 | if (!ngModel || !element.val()) return; 10 | var keyProperty = $parse(attrs.wcUnique)(); 11 | var currentValue = element.val(); 12 | dataService.checkUniqueValue(keyProperty.key, keyProperty.property, currentValue) 13 | .then(function (unique) { 14 | //Ensure value that being checked hasn't changed 15 | //since the Ajax call was made 16 | if (currentValue === element.val()) { 17 | ngModel.$setValidity('unique', unique); 18 | } 19 | }, function () { 20 | //Probably want a more robust way to handle an error 21 | //For this demo we'll set unique to true though 22 | ngModel.$setValidity('unique', true); 23 | }); 24 | }); 25 | } 26 | }; 27 | }; 28 | 29 | wcUniqueDirective.$inject = ['$parse', 'dataService']; 30 | 31 | angular.module('expenseApp').directive('wcUnique', wcUniqueDirective); 32 | 33 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/filters/nameCityStateFilter.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var nameCityStateFilter = function () { 4 | 5 | return function (employees, filterValue) { 6 | if (!filterValue) return employees; 7 | 8 | var matches = []; 9 | filterValue = filterValue.toLowerCase(); 10 | for (var i = 0; i < employees.length; i++) { 11 | var emp = employees[i]; 12 | if (emp.firstName.toLowerCase().indexOf(filterValue) > -1 || 13 | emp.lastName.toLowerCase().indexOf(filterValue) > -1 || 14 | emp.city.toLowerCase().indexOf(filterValue) > -1 || 15 | emp.state.toLowerCase().indexOf(filterValue) > -1) { 16 | 17 | matches.push(emp); 18 | } 19 | } 20 | return matches; 21 | }; 22 | }; 23 | 24 | angular.module('expenseApp').filter('nameCityStateFilter', nameCityStateFilter); 25 | 26 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/filters/nameExpenseFilter.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var nameExpenseFilter = function () { 4 | 5 | function matchesExpense(employee, filterValue) { 6 | if (employee.expenses) { 7 | for (var i = 0; i < employee.expenses.length; i++) { 8 | if (employee.expenses[i].title.toLowerCase().indexOf(filterValue) > -1) { 9 | return true; 10 | } 11 | } 12 | } 13 | return false; 14 | } 15 | 16 | return function (employees, filterValue) { 17 | if (!filterValue || !employees) return employees; 18 | 19 | var matches = []; 20 | filterValue = filterValue.toLowerCase(); 21 | for (var i = 0; i < employees.length; i++) { 22 | var emp = employees[i]; 23 | if (emp.firstName.toLowerCase().indexOf(filterValue) > -1 || 24 | emp.lastName.toLowerCase().indexOf(filterValue) > -1 || 25 | matchesExpense(emp, filterValue)) { 26 | 27 | matches.push(emp); 28 | } 29 | } 30 | return matches; 31 | }; 32 | }; 33 | 34 | angular.module('expenseApp').filter('nameExpenseFilter', nameExpenseFilter); 35 | 36 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/partials/dialog.html: -------------------------------------------------------------------------------- 1 |  4 | 7 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/partials/modal.html: -------------------------------------------------------------------------------- 1 |  13 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/services/config.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var value = { 4 | dataService: 'employeesSharePointService' 5 | }; 6 | 7 | angular.module('expenseApp').value('config', value); 8 | 9 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/services/dataService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var dataService = function (config, $injector) { 4 | var service = $injector.get(config.dataService); 5 | return service; 6 | }; 7 | 8 | dataService.$inject = ['config', '$injector']; 9 | 10 | angular.module('expenseApp').factory('dataService', dataService); 11 | 12 | }()); 13 | 14 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/services/dialogService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var dialogService = function ($dialog) { 4 | 5 | var dialogDefaults = { 6 | backdrop: true, 7 | keyboard: true, 8 | backdropClick: true, 9 | dialogFade: true, 10 | templateUrl: '/app/partials/dialog.html' 11 | }; 12 | 13 | var dialogOptions = { 14 | closeButtonText: 'Close', 15 | actionButtonText: 'OK', 16 | headerText: 'Proceed?', 17 | bodyText: 'Perform this action?' 18 | }; 19 | 20 | this.showModalDialog = function (customDialogDefaults, customDialogOptions) { 21 | if (!customDialogDefaults) customDialogDefaults = {}; 22 | customDialogDefaults.backdropClick = false; 23 | this.showDialog(customDialogDefaults, customDialogOptions); 24 | }; 25 | 26 | this.showDialog = function (customDialogDefaults, customDialogOptions) { 27 | //Create temp objects to work with since we're in a singleton service 28 | var tempDialogDefaults = {}; 29 | var tempDialogOptions = {}; 30 | 31 | //Map angular-ui dialog custom defaults to dialog defaults defined in this service 32 | angular.extend(tempDialogDefaults, dialogDefaults, customDialogDefaults); 33 | 34 | //Map dialog.html $scope custom properties to defaults defined in this service 35 | angular.extend(tempDialogOptions, dialogOptions, customDialogOptions); 36 | 37 | if (!tempDialogDefaults.controller) { 38 | tempDialogDefaults.controller = function ($scope, dialog) { 39 | $scope.dialogOptions = tempDialogOptions; 40 | $scope.dialogOptions.close = function (result) { 41 | dialog.close(result); 42 | }; 43 | $scope.dialogOptions.callback = function () { 44 | dialog.close(); 45 | customDialogOptions.callback(); 46 | }; 47 | }; 48 | } 49 | 50 | var d = $dialog.dialog(tempDialogDefaults); 51 | d.open(); 52 | }; 53 | 54 | this.showMessage = function (title, message, buttons) { 55 | var defaultButtons = [{ result: 'ok', label: 'OK', cssClass: 'btn-primary' }]; 56 | var msgBox = new $dialog.dialog({ 57 | dialogFade: true, 58 | templateUrl: 'template/dialog/message.html', 59 | controller: 'MessageBoxController', 60 | resolve: 61 | { 62 | model: function () { 63 | return { 64 | title: title, 65 | message: message, 66 | buttons: buttons == null ? defaultButtons : buttons 67 | }; 68 | } 69 | } 70 | }); 71 | return msgBox.open(); 72 | }; 73 | }; 74 | 75 | angular.module('expenseApp').service('dialogService', ['$dialog', dialogService]); 76 | 77 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/services/employeesSharePointService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var employeesFactory = function ($http, $q, $window, $location, $timeout) { 4 | var serviceBase = '/Handlers/WebProxy.ashx?url=', 5 | refreshUrlBase = '/home/refreshtoken?returnUrl=', 6 | baseSPUrl = expenseManager.baseSPUrl, 7 | baseSPListsUrl = baseSPUrl + 'web/lists/'; 8 | factory = { 9 | itemCount: 0, 10 | expenses: null 11 | }, 12 | requestDigest = null; 13 | 14 | factory.getEmployeesAndExpenses = function (pageIndex, pageSize) { 15 | 16 | //This will get all expenses and the employee (nested) but won't group expenses by employee 17 | //return $http.get(serviceBase + encodeURIComponent(baseSPListsUrl + "getByTitle('Expenses')/items?$select=Amount,Created,ExpenseCategory,Title,Employee/ID,Employee/FirstName,Employee/LastName&$expand=Employee")) 18 | // .then(function (data) { 19 | // var employees = data.d.results; 20 | // }); 21 | 22 | var deferred = $q.defer(); 23 | var empsPromise = $http.get(serviceBase + encodeURIComponent(baseSPListsUrl + 24 | "getByTitle('Employees')/items?$select=ID,FirstName,LastName&$orderby=LastName,FirstName")); 25 | var expensesPromise = $http.get(serviceBase + encodeURIComponent(baseSPListsUrl + 26 | "getByTitle('Expenses')/items?$select=Amount,Created,ExpenseCategory,Title,Employee/Id&$expand=Employee/Id")); 27 | 28 | //Currently the SharePoint REST API doesn't make grabbing the employees & expenses 29 | //all at once so we're grabbing them individually 30 | $q.all([empsPromise, expensesPromise]) 31 | .then(function (results) { 32 | var employees = (results[0].data.d) ? caseProps(results[0].data.d.results, propStyleEnum.camelCase) : []; //Get employees data 33 | var expenses = (results[1].data.d) ? caseProps(results[1].data.d.results, propStyleEnum.camelCase) : []; //Get expenses data 34 | 35 | mapEmployeeToExpenses(employees, expenses); 36 | 37 | deferred.resolve(employees); 38 | }, 39 | function (error) { 40 | if (error.status === 302) { 41 | deferred.resolve(null); 42 | //Potential infinite loop here - haven't dealt with that possibility yet 43 | $window.location.href = getRedirectUrl(); 44 | } 45 | }); 46 | 47 | return deferred.promise; //Return promise to caller 48 | }; 49 | 50 | factory.getEmployeesSummary = function (pageIndex, pageSize) { 51 | var url = serviceBase + encodeURIComponent(baseSPListsUrl + "getByTitle('Employees')/items?$select=ID,FirstName,LastName,Address,City,State,Zip,Email,Gender&$orderby=LastName,FirstName"); 52 | return getPagedResource(url, pageIndex, pageSize); 53 | }; 54 | 55 | factory.getStates = function () { 56 | var url = serviceBase + encodeURIComponent(baseSPListsUrl + "getByTitle('States')/items?$select=Title&$orderby=Title"); 57 | return $http.get(url).then( 58 | function (result) { 59 | return caseProps(result.data.d.results, propStyleEnum.camelCase); 60 | }); 61 | }; 62 | 63 | factory.getEmployee = function (id) { 64 | var url = serviceBase + encodeURIComponent(baseSPListsUrl + "getByTitle('Employees')/items(" + id + ")?$select=ID,FirstName,LastName,Address,City,State,Zip,Email,Gender"); 65 | return $http.get(url).then(function (result) { 66 | var cust = caseProps(result.data.d, propStyleEnum.camelCase); 67 | cust.zip = parseInt(cust.zip); 68 | return cust; 69 | }, 70 | function (error) { 71 | if (error.status === 302) { 72 | //Potential infinite loop here - haven't dealt with that possibility yet 73 | $window.location.href = getRedirectUrl(); 74 | } 75 | }); 76 | }; 77 | 78 | factory.checkUniqueValue = function (id, property, value) { 79 | if (!id) id = 0; 80 | return $http.get(serviceBase + 'checkUnique/' + id + '?property=' + property + '&value=' + escape(value)).then( 81 | function (results) { 82 | return results.data.status; 83 | }); 84 | }; 85 | 86 | factory.getEmployeeExpenses = function (id) { 87 | 88 | var deferred = $q.defer(); 89 | var empPromise = factory.getEmployee(id); 90 | var expensesPromise = $http.get(serviceBase + encodeURIComponent(baseSPListsUrl + "getByTitle('Expenses')/items?$filter=Employee eq " + id + "&$select=Amount,Created,ExpenseCategory,Title")); 91 | 92 | $q.all([empPromise, expensesPromise]) 93 | .then(function (results) { 94 | var employee = results[0]; //Get customer data 95 | employee.expenses = caseProps(results[1].data.d.results, propStyleEnum.camelCase); //Get expenses data 96 | 97 | calculateExpensesTotal(employee); 98 | 99 | deferred.resolve(employee); 100 | }, 101 | function (error) { 102 | if (error.status === 302) { 103 | deferred.resolve(null); 104 | //Potential infinite loop here - haven't dealt with that possibility yet 105 | $window.location.href = getRedirectUrl(); 106 | } 107 | }); 108 | 109 | return deferred.promise; //Return promise to caller 110 | 111 | }, 112 | 113 | factory.insertEmployee = function (employee) { 114 | 115 | employee = caseProps(employee, propStyleEnum.pascalCase); 116 | employee.Title = employee.FirstName + ' ' + employee.LastName; 117 | employee.Zip = employee.Zip.toString(); //Zip is a string in SharePoint 118 | employee.__metadata = { type: 'SP.Data.EmployeesListItem' }; 119 | 120 | var options = { 121 | url: serviceBase + encodeURIComponent(baseSPListsUrl + "getByTitle('Employees')/items"), 122 | method: 'POST', 123 | data: JSON.stringify(employee), 124 | headers: { 125 | 'Accept': 'application/json;odata=verbose', 126 | 'Content-Type': 'application/json;odata=verbose' 127 | //'X-RequestDigest': requestDigest 128 | }, 129 | }; 130 | 131 | return $http(options).then(function (result) { 132 | var cust = caseProps(result.data.d, propStyleEnum.camelCase); 133 | cust.zip = parseInt(cust.zip); //SharePoint Zip field is a string so convert to int 134 | return cust; 135 | }, 136 | function (error) { 137 | $window.alert(error.message); 138 | return error; 139 | }); 140 | }; 141 | 142 | factory.newEmployee = function () { 143 | return $q.when({ }); 144 | }; 145 | 146 | factory.updateEmployee = function (employee) { 147 | 148 | employee = caseProps(employee, propStyleEnum.pascalCase); 149 | employee.Title = employee.FirstName + ' ' + employee.LastName; 150 | employee.Zip = employee.Zip.toString(); //Zip is a string in SharePoint 151 | 152 | var options = { 153 | url: serviceBase + encodeURIComponent(employee.__metadata.uri), 154 | method: 'MERGE', 155 | data: JSON.stringify(employee), 156 | headers: { 157 | 'Accept': 'application/json;odata=verbose', 158 | 'Content-Type': 'application/json;odata=verbose', 159 | 'If-Match': employee.__metadata.etag 160 | //'X-RequestDigest': requestDigest 161 | } 162 | }; 163 | 164 | return $http(options); 165 | 166 | }; 167 | 168 | factory.deleteEmployee = function (employee) { 169 | 170 | var options = { 171 | url: serviceBase + encodeURIComponent(employee.__metadata.uri), 172 | method: 'DELETE', 173 | headers: { 174 | 'Accept': 'application/json;odata=verbose', 175 | 'If-Match': employee.__metadata.etag 176 | //'X-RequestDigest': requestDigest 177 | } 178 | }; 179 | 180 | return $http(options).then(function (status) { 181 | return status.data; 182 | }, 183 | function (error) { 184 | $window.alert(error.message); 185 | return error; 186 | }); 187 | }; 188 | 189 | function getRequestDigest() { 190 | 191 | var options = { 192 | url: serviceBase + encodeURIComponent(baseSPUrl + 'contextinfo'), 193 | method: 'POST', 194 | headers: { 195 | //Following will be set in HTTP Handler but listed here for completeness 196 | 'Accept': 'application/json;odata=verbose', 197 | 'ContextInfoRequest': true 198 | } 199 | }; 200 | 201 | $http(options).success(function (data) { 202 | if (data && data.d) requestDigest = data.d.GetContextWebInformation.FormDigestValue; 203 | }); 204 | } 205 | 206 | //getRequestDigest(); 207 | 208 | function getRedirectUrl() { 209 | var port = ($location.port()) ? ':' + $location.port() : ''; 210 | var link = $location.protocol() + '://' + $location.host() + port + 211 | refreshUrlBase + encodeURIComponent($location.absUrl()); 212 | return link; 213 | } 214 | 215 | function getPagedResource(baseResource, pageIndex, pageSize) { 216 | var url = baseResource; 217 | //url; += (arguments.length == 3) ? buildPagingUri(pageIndex, pageSize) : ''; 218 | 219 | //Server-side paging not currently implemented due to lack of proper paging support 220 | //in SharePoint OData/REST api. 221 | var deferred = $q.defer(); 222 | var countPromise = $http.get(serviceBase + encodeURIComponent(baseSPListsUrl + "getByTitle('Employees')/itemcount")); 223 | var empPromise = $http.get(url); 224 | 225 | $q.all([countPromise, empPromise]) 226 | .then(function (results) { 227 | var custCount = (results[0].data.d) ? results[0].data.d.ItemCount : 0; //Get countPromise data 228 | var custs = (results[1].data.d) ? caseProps(results[1].data.d.results, propStyleEnum.camelCase) : []; //Get empPromise data 229 | 230 | //extendEmployees(custs); 231 | var custData = { 232 | totalRecords: custCount, 233 | results: custs 234 | }; 235 | 236 | deferred.resolve(custData); 237 | }, 238 | function (error) { 239 | if (error.status === 302) { 240 | deferred.resolve(null); 241 | //Potential infinite loop here - haven't dealt with that possibility yet 242 | $window.location.href = getRedirectUrl(); 243 | } 244 | }); 245 | 246 | return deferred.promise; //Return promise to caller 247 | } 248 | 249 | function buildPagingUri(pageIndex, pageSize) { 250 | var uri = '&$skip=' + (pageIndex * pageSize) + '&$top=' + pageSize; 251 | return uri; 252 | } 253 | 254 | function mapEmployeeToExpenses(employees, expenses) { 255 | if (employees && expenses) { 256 | for (var i = 0; i < employees.length; i++) { 257 | var employee = employees[i]; 258 | var employeeExpenses = []; 259 | for (var j = 0; j < expenses.length; j++) { 260 | var expense = expenses[j]; 261 | if (expense.employee.Id === employee.id) { //Case of "Id" is correct for this instance 262 | employeeExpenses.push(expense); 263 | } 264 | } 265 | employee.expenses = employeeExpenses; 266 | calculateExpensesTotal(employee); 267 | } 268 | } 269 | } 270 | 271 | function extendEmployees(employees) { 272 | var employeesLen = employees.length; 273 | //Iterate through employees 274 | for (var i = 0; i < employeesLen; i++) { 275 | var employee = employees[i]; 276 | calculateExpensesTotal(employee); 277 | } 278 | } 279 | 280 | function calculateExpensesTotal(employee) { 281 | var expensesLen = employee.expenses.length; 282 | employee.expensesTotal = 0; 283 | //Iterate through expenses 284 | for (var j = 0; j < expensesLen; j++) { 285 | employee.expensesTotal += employee.expenses[j].amount; 286 | } 287 | } 288 | 289 | var propStyleEnum = { 290 | camelCase: 'camel', 291 | pascalCase: 'pascal' 292 | }; 293 | 294 | function caseProps(obj, propStyle) { 295 | 296 | function caseProp(str) { 297 | if (!str) return str; 298 | 299 | //Camel Case Option 300 | if (!propStyle || propStyle === propStyleEnum.camelCase) { 301 | return str.charAt(0).toLowerCase() + str.slice(1); 302 | } 303 | //Pascal Case Option 304 | else { 305 | //SharePoint-specific fields to worry about 306 | if (str !== '__metadata') { 307 | return str.charAt(0).toUpperCase() + str.slice(1); 308 | } 309 | return str; 310 | } 311 | } 312 | 313 | function iterate(obj) { 314 | var newObj = {}; 315 | for (prop in obj) { 316 | newObj[caseProp(prop)] = obj[prop]; 317 | } 318 | return newObj; 319 | } 320 | 321 | if (Array.isArray(obj)) { 322 | var newArray = []; 323 | for (var i = 0; i < obj.length; i++) { 324 | newArray.push(iterate(obj[i])); 325 | } 326 | return newArray; 327 | } 328 | else { 329 | return iterate(obj); 330 | } 331 | 332 | } 333 | 334 | function getItemTypeForListName(listName) { 335 | return "SP.Data." + listName.charAt(0).toUpperCase() + listName.slice(1) + "ListItem"; 336 | } 337 | 338 | return factory; 339 | }; 340 | 341 | employeesFactory.$inject = ['$http', '$q', '$window', '$location', '$timeout']; 342 | 343 | angular.module('expenseApp').factory('employeesSharePointService', employeesFactory); 344 | 345 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/services/httpInterceptors.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | angular.module('expenseApp') 4 | .config(['$httpProvider', function ($httpProvider) { 5 | 6 | var httpInterceptor401 = function ($q, $rootScope) { 7 | 8 | var success = function (response) { 9 | return response; 10 | }; 11 | 12 | var error = function (res) { 13 | if (res.status === 401) { 14 | //Raise event so listener (navbarController) can act on it 15 | $rootScope.$broadcast('redirectToLogin', null); 16 | return $q.reject(res); 17 | } 18 | return $q.reject(res); 19 | }; 20 | 21 | return function (promise) { 22 | return promise.then(success, error); 23 | }; 24 | 25 | }; 26 | 27 | httpInterceptor401.$inject = ['$q', '$rootScope']; 28 | 29 | $httpProvider.interceptors.push(httpInterceptor401); 30 | 31 | }]); 32 | 33 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/services/modalService.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var modalService = function ($modal) { 4 | 5 | var modalDefaults = { 6 | backdrop: true, 7 | keyboard: true, 8 | modalFade: true, 9 | templateUrl: '/app/expenseApp/partials/modal.html' 10 | }; 11 | 12 | var modalOptions = { 13 | closeButtonText: 'Close', 14 | actionButtonText: 'OK', 15 | headerText: 'Proceed?', 16 | bodyText: 'Perform this action?' 17 | }; 18 | 19 | this.showModal = function (customModalDefaults, customModalOptions) { 20 | if (!customModalDefaults) customModalDefaults = {}; 21 | customModalDefaults.backdrop = 'static'; 22 | return this.show(customModalDefaults, customModalOptions); 23 | }; 24 | 25 | this.show = function (customModalDefaults, customModalOptions) { 26 | //Create temp objects to work with since we're in a singleton service 27 | var tempModalDefaults = {}; 28 | var tempModalOptions = {}; 29 | 30 | //Map angular-ui modal custom defaults to modal defaults defined in this service 31 | angular.extend(tempModalDefaults, modalDefaults, customModalDefaults); 32 | 33 | //Map modal.html $scope custom properties to defaults defined in this service 34 | angular.extend(tempModalOptions, modalOptions, customModalOptions); 35 | 36 | if (!tempModalDefaults.controller) { 37 | tempModalDefaults.controller = function ($scope, $modalInstance) { 38 | $scope.modalOptions = tempModalOptions; 39 | $scope.modalOptions.ok = function (result) { 40 | $modalInstance.close('ok'); 41 | }; 42 | $scope.modalOptions.close = function (result) { 43 | $modalInstance.close('cancel'); 44 | }; 45 | }; 46 | 47 | tempModalDefaults.controller.$inject = ['$scope', '$modalInstance']; 48 | } 49 | 50 | return $modal.open(tempModalDefaults).result; 51 | }; 52 | }; 53 | 54 | angular.module('expenseApp').service('modalService', ['$modal', modalService]); 55 | 56 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/services/routeResolver.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var routeResolver = function () { 4 | 5 | this.$get = function () { 6 | return this; 7 | }; 8 | 9 | this.routeConfig = function () { 10 | var viewsDirectory = '/app/views/', 11 | controllersDirectory = '/app/controllers/', 12 | 13 | setBaseDirectories = function (viewsDir, controllersDir) { 14 | viewsDirectory = viewsDir; 15 | controllersDirectory = controllersDir; 16 | }, 17 | 18 | getViewsDirectory = function () { 19 | return viewsDirectory; 20 | }, 21 | 22 | getControllersDirectory = function () { 23 | return controllersDirectory; 24 | }; 25 | 26 | return { 27 | setBaseDirectories: setBaseDirectories, 28 | getControllersDirectory: getControllersDirectory, 29 | getViewsDirectory: getViewsDirectory 30 | }; 31 | }(); 32 | 33 | this.route = function (routeConfig) { 34 | 35 | var resolve = function (baseName, path) { 36 | if (!path) path = ''; 37 | 38 | var routeDef = {}; 39 | routeDef.templateUrl = routeConfig.getViewsDirectory() + path + baseName + '.html'; 40 | routeDef.controller = baseName + 'Controller'; 41 | routeDef.resolve = { 42 | load: ['$q', '$rootScope', function ($q, $rootScope) { 43 | var dependencies = [routeConfig.getControllersDirectory() + path + baseName + 'Controller.js']; 44 | return resolveDependencies($q, $rootScope, dependencies); 45 | }] 46 | }; 47 | 48 | return routeDef; 49 | }, 50 | 51 | resolveDependencies = function ($q, $rootScope, dependencies) { 52 | var defer = $q.defer(); 53 | require(dependencies, function () { 54 | defer.resolve(); 55 | $rootScope.$apply(); 56 | }); 57 | 58 | return defer.promise; 59 | }; 60 | 61 | return { 62 | resolve: resolve 63 | }; 64 | }(this.routeConfig); 65 | 66 | }; 67 | 68 | var servicesApp = angular.module('routeResolverServices', []); 69 | 70 | //Must be a provider since it will be injected into module.config() 71 | servicesApp.provider('routeResolver', routeResolver); 72 | 73 | }()); 74 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/views/about.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |

About

5 |
6 |
7 |
8 |
9 |
Created by:
10 | 11 | 12 |
13 |
14 |
15 |
Blog:
16 | 17 | 18 |
19 |
20 | 24 |
25 | 29 |
30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/views/employees/employeeEdit.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |

{{vm.title}} Employee

5 |
6 |
7 |
8 |
9 |
10 |

{{ vm.employee.firstName + ' ' + vm.employee.lastName }} (View Expenses)

11 |
12 |
13 |
14 |
15 |
16 | First Name: 17 |
18 |
19 | 20 | 21 | First name is required 22 | 23 |
24 |
25 |
26 |
27 |
28 | Last Name: 29 |
30 |
31 | 32 | 33 | Last name is required 34 | 35 |
36 |
37 |
38 |
39 |
40 | Gender: 41 |
42 |
43 |
44 | 50 |
51 |
52 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | Email: 66 |
67 |
68 | 69 | 73 | 74 | Email already in use 75 | 76 |
77 |
78 |
79 |
80 |
81 | Address: 82 |
83 |
84 | 85 | 86 | Address is required 87 | 88 |
89 |
90 |
91 |
92 |
93 | City: 94 |
95 |
96 | 97 | 98 | City is required 99 | 100 |
101 |
102 |
103 |
104 |
105 | State: 106 |
107 |
108 | 113 | 114 | 2 character state is required 115 | 116 |
117 |
118 |
119 |
120 |
121 | Zip: 122 |
123 |
124 | 125 | 126 | Zip is required 127 | 128 |
129 |
130 |
131 |
132 |
133 | 135 |    136 | 137 |
138 |
139 |
140 |
141 |
142 |
143 |   Employee updated! 144 |
145 |
146 |
147 |
148 |
149 |   Error: {{ vm.errorMessage }} 150 |
151 |
152 |
153 |
154 |
155 |
156 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/views/employees/employeeExpenses.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |

  Employee Expenses

5 |
6 |
7 |
8 |
9 |
10 |

Expenses for {{ vm.employee.firstName + ' ' + vm.employee.lastName }}

11 | {{vm.employee.address}} 12 |
13 | {{vm.employee.city}}, {{vm.employee.state}} 14 |

15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/views/employees/employees.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |

Employees

5 |
6 |
7 |
8 | 26 |
27 |
28 |
29 |
30 |
31 |

No employees found

32 |
33 |
34 | 35 |
36 |
38 |
39 | 40 | 41 |
42 |
43 |
44 | 45 |
46 |
47 |
{{employee.city}}, {{employee.state}}
48 | 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 83 | 84 | 85 |
 NameLocationOrders 
Employee Image{{employee.firstName + ' ' + employee.lastName}}{{employee.city}}, {{employee.state}} 78 | 79 | View Expenses 80 | 81 |
86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 |
100 |
Showing {{ vm.numRecordsDisplaying }} of {{ vm.totalRecords}} total employees
101 | Filtering by "{{ vm.searchText }}" 102 |
103 |
104 |


105 | 106 |
107 |
108 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/views/expenses/expenses.html: -------------------------------------------------------------------------------- 1 | 
2 |
3 |
4 |

  All Employee Expenses

5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
29 |
Showing {{ vm.numRecordsDisplaying }} of {{ vm.totalRecords}} total employees
30 |
Filtering by "{{ vm.searchText }}"
31 |
32 |
33 |
34 |
35 | 42 |
43 |
44 |

No employees found

45 |
46 |
47 |
48 |
49 |
56 |
Showing {{ vm.numRecordsDisplaying }} of {{ vm.totalRecords}} total employees
57 | Filtering by "{{ vm.searchText }}" 58 |
59 |
60 |
61 |
62 |


63 |
64 | 65 | -------------------------------------------------------------------------------- /ExpenseManager/app/expenseApp/views/expenses/expensesTable.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 20 | 23 | 26 | 29 | 30 | 31 | 34 | 37 | 38 | 39 |
ExpenseCreatedCategoryAmount
13 |
No expenses found
14 |
18 | {{ expense.title }} 19 | 21 | {{ expense.created | date:'MM/dd/yyyy' }} 22 | 24 | {{ expense.expenseCategory }} 25 | 27 | {{ expense.amount | currency }} 28 |
32 |   33 | 35 | {{ vm.employee.expensesTotal | currency }} 36 |
-------------------------------------------------------------------------------- /ExpenseManager/app/wc.directives/directives/lazyLoader.js: -------------------------------------------------------------------------------- 1 | // 2 | (function () { 3 | // use this directive to delay assigning user input to the underlying 4 | // view model field until focus leaves the dom element. Useful for 5 | // delaying breeze validation until user enters all data (like when 6 | // entering dates) 7 | var lazyLoad = function () { 8 | return { 9 | restrict: 'A', //E = element, A = attribute, C = class, M = comment 10 | transclude: true, 11 | scope: { 12 | name: '@' 13 | }, 14 | link: function (scope, element, attrs) { 15 | element.bind('blur', function (e) { 16 | scope.$parent.$apply(function () { 17 | scope.$parent[scope.name] = element.val(); 18 | }); 19 | }); 20 | } 21 | }; 22 | }; 23 | 24 | angular.module('customersApp').directive('lazyLoad', lazyLoad); 25 | 26 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/wc.directives/directives/menuHighlighter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Thanks to Karl-Gustav for creating the autoActive directive 3 | to simplify highlighting
  • elements in a menu based on the path 4 | View the original version of the autoActive directive at 5 | https://github.com/Karl-Gustav/autoActive 6 | 7 | This version renames the directive and does some minor code restructuring and changes. 8 | */ 9 | 10 | (function () { 11 | 12 | var menuHighlighter = function ($location) { 13 | return { 14 | restrict: 'A', 15 | scope: { 16 | highlightClassName: '@' 17 | }, 18 | link: function (scope, element) { 19 | function setActive() { 20 | var path = $location.path(); 21 | var className = scope.highlightClassName || 'active'; 22 | 23 | if (path) { 24 | angular.forEach(element.find('li'), function (li) { 25 | var anchor = li.querySelector('a'); 26 | //Get href from href attribute or data-href in cases where href isn't used (such as login) 27 | var href = (anchor && anchor.href) ? anchor.href : 28 | anchor.getAttribute('data-href').replace('#', ''); 29 | //Get value after hash 30 | var trimmedHref = href.substr(href.indexOf('#/') + 1, href.length); 31 | //Convert path to same length as trimmedHref 32 | var basePath = path.substr(0, trimmedHref.length); 33 | 34 | //See if trimmedHref and basePath match. If so, then highlight that item 35 | if (trimmedHref === basePath) { 36 | angular.element(li).addClass(className); 37 | } else { 38 | angular.element(li).removeClass(className); 39 | } 40 | }); 41 | } 42 | } 43 | 44 | setActive(); 45 | 46 | //Monitor location changes 47 | scope.$on('$locationChangeSuccess', setActive); 48 | } 49 | } 50 | } 51 | 52 | menuHighlighter.$inject = ['$location']; 53 | 54 | angular.module('wc.directives').directive('menuHighlighter', menuHighlighter); 55 | 56 | }()); -------------------------------------------------------------------------------- /ExpenseManager/app/wc.directives/directives/wcOverlay.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var wcOverlayDirective = function ($q, $timeout, $window, httpInterceptor) { 4 | return { 5 | restrict: 'EA', 6 | transclude: true, 7 | scope: { 8 | wcOverlayDelay: "@" 9 | }, 10 | template: '
    ' + 11 | '
    ' + 12 | '
    ' + 13 | '
    ' + 14 | '
    ', 15 | link: function (scope, element, attrs) { 16 | var overlayContainer = null, 17 | timerPromise = null, 18 | timerPromiseHide = null, 19 | queue = []; 20 | 21 | init(); 22 | 23 | function init() { 24 | wireUpHttpInterceptor(); 25 | if (window.jQuery) wirejQueryInterceptor(); 26 | overlayContainer = element[0].firstChild; //Get to template 27 | } 28 | 29 | //Hook into httpInterceptor factory request/response/responseError functions 30 | function wireUpHttpInterceptor() { 31 | 32 | httpInterceptor.request = function (config) { 33 | processRequest(); 34 | return config || $q.when(config); 35 | }; 36 | 37 | httpInterceptor.response = function (response) { 38 | processResponse(); 39 | return response || $q.when(response); 40 | }; 41 | 42 | httpInterceptor.responseError = function (rejection) { 43 | processResponse(); 44 | return $q.reject(rejection); 45 | }; 46 | } 47 | 48 | //Monitor jQuery Ajax calls in case it's used in an app 49 | function wirejQueryInterceptor() { 50 | $(document).ajaxStart(function () { 51 | processRequest(); 52 | }); 53 | 54 | $(document).ajaxComplete(function () { 55 | processResponse(); 56 | }); 57 | 58 | $(document).ajaxError(function () { 59 | processResponse(); 60 | }); 61 | } 62 | 63 | function processRequest() { 64 | queue.push({}); 65 | if (queue.length == 1) { 66 | timerPromise = $timeout(function () { 67 | if (queue.length) showOverlay(); 68 | }, scope.wcOverlayDelay ? scope.wcOverlayDelay : 500); //Delay showing for 500 millis to avoid flicker 69 | } 70 | } 71 | 72 | function processResponse() { 73 | queue.pop(); 74 | if (queue.length == 0) { 75 | //Since we don't know if another XHR request will be made, pause before 76 | //hiding the overlay. If another XHR request comes in then the overlay 77 | //will stay visible which prevents a flicker 78 | timerPromiseHide = $timeout(function () { 79 | //Make sure queue is still 0 since a new XHR request may have come in 80 | //while timer was running 81 | if (queue.length == 0) { 82 | hideOverlay(); 83 | if (timerPromiseHide) $timeout.cancel(timerPromiseHide); 84 | } 85 | }, scope.wcOverlayDelay ? scope.wcOverlayDelay : 500); 86 | } 87 | } 88 | 89 | function showOverlay() { 90 | var w = 0; 91 | var h = 0; 92 | if (!$window.innerWidth) { 93 | if (!(document.documentElement.clientWidth == 0)) { 94 | w = document.documentElement.clientWidth; 95 | h = document.documentElement.clientHeight; 96 | } 97 | else { 98 | w = document.body.clientWidth; 99 | h = document.body.clientHeight; 100 | } 101 | } 102 | else { 103 | w = $window.innerWidth; 104 | h = $window.innerHeight; 105 | } 106 | var content = document.getElementById('overlay-content'); 107 | var contentWidth = parseInt(getComputedStyle(content, 'width').replace('px', '')); 108 | var contentHeight = parseInt(getComputedStyle(content, 'height').replace('px', '')); 109 | 110 | content.style.top = h / 2 - contentHeight / 2 + 'px'; 111 | content.style.left = w / 2 - contentWidth / 2 + 'px'; 112 | 113 | overlayContainer.style.display = 'block'; 114 | } 115 | 116 | function hideOverlay() { 117 | if (timerPromise) $timeout.cancel(timerPromise); 118 | overlayContainer.style.display = 'none'; 119 | } 120 | 121 | var getComputedStyle = function () { 122 | var func = null; 123 | if (document.defaultView && document.defaultView.getComputedStyle) { 124 | func = document.defaultView.getComputedStyle; 125 | } else if (typeof (document.body.currentStyle) !== "undefined") { 126 | func = function (element, anything) { 127 | return element["currentStyle"]; 128 | }; 129 | } 130 | 131 | return function (element, style) { 132 | return func(element, null)[style]; 133 | }; 134 | }(); 135 | } 136 | }; 137 | }; 138 | 139 | var wcDirectivesApp = angular.module('wc.directives', []); 140 | 141 | //Empty factory to hook into $httpProvider.interceptors 142 | //Directive will hookup request, response, and responseError interceptors 143 | wcDirectivesApp.factory('httpInterceptor', function () { 144 | return {}; 145 | }); 146 | 147 | //Hook httpInterceptor factory into the $httpProvider interceptors so that we can monitor XHR calls 148 | wcDirectivesApp.config(['$httpProvider', function ($httpProvider) { 149 | $httpProvider.interceptors.push('httpInterceptor'); 150 | }]); 151 | 152 | //Directive that uses the httpInterceptor factory above to monitor XHR calls 153 | //When a call is made it displays an overlay and a content area 154 | //No attempt has been made at this point to test on older browsers 155 | wcDirectivesApp.directive('wcOverlay', ['$q', '$timeout', '$window', 'httpInterceptor', wcOverlayDirective]); 156 | 157 | }()); -------------------------------------------------------------------------------- /ExpenseManager/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /ExpenseManager/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /ExpenseManager/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpenseManager/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /ExpenseManager/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Expense Manager 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 38 | 39 |
    40 |
    41 |
    42 | 43 | 58 | 59 |
    60 |
      Loading 61 |
    62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /ExpenseManager/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ExpensesTrackerSiteTemplate.wsp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/SP-AngularJS-ExpenseManager-Code-Sample/03730772d91132b4078ebd7efb074e9feada9e0a/ExpensesTrackerSiteTemplate.wsp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | products: 4 | - office-sp 5 | - office-365 6 | languages: 7 | - javascript 8 | urlFragment: expense-manager-angular 9 | description: "This application is a stand-alone AngularJS application that performs CRUD operations against SharePoint/Office 365." 10 | extensions: 11 | contentType: samples 12 | technologies: 13 | - Azure AD 14 | createdDate: 6/19/2014 8:30:16 AM 15 | --- 16 | 17 | # Expense Manager with AngularJS, SharePoint/Office 365 and Microsoft Azure Active Directory 18 | 19 | If you're new to AngularJS check out the [AngularJS in 60-ish Minutes](http://weblogs.asp.net/dwahlin/video-tutorial-angularjs-fundamentals-in-60-ish-minutes) video tutorial or download the [free eBook](http://weblogs.asp.net/dwahlin/angularjs-in-60-ish-minutes-the-ebook). Also check out [The AngularJS Magazine](http://flip.it/bdyUX) for up-to-date information on using AngularJS to build Single Page Applications (SPAs). 20 | 21 | A presentation on all samples can be found in the [presentation folder](presentation) within this repository. 22 | 23 | ![PowerPoint slide titled Expenses Manager Code Sample with AngularJS and Office 365 APIs by Jeremy Thake Technical Product Manager Microsoft Corporation](ExpenseManager/Content/images/readmeImages/channel9scrnsht.png) 24 | 25 | An on-demand web cast recorded by Jeremy Thake can be found on [Channel 9](http://channel9.msdn.com/Blogs/Office-365-Dev/Getting-started-with-the-Expense-Tracker-AngularJS-Office-365-API-Code-Sample). 26 | 27 | This application is a stand-alone AngularJS application that performs CRUD operations against SharePoint/Office 365. Authentication relies on Microsoft Azure Active Directory. 28 | This application demonstrates: 29 | 30 | * Consuming data provided by SharePoint/Office 365 RESTful APIs 31 | * Authentication against Microsoft Azure Active Directory 32 | * A custom "middle-man" proxy that allows cross-domain calls to be made to SharePoint/Office 365 33 | * A complete application with read-only and editable data 34 | * Using AngularJS with $http in a factory to access a backend RESTful service 35 | * Techniques for showing multiple views of data (card view and list view) 36 | * Custom filters for filtering customer and product data 37 | * A custom directive to ensure unique values in a form for email 38 | * A custom directive that intercepts $http and jQuery XHR requests (in case either are used) and displays a loading dialog 39 | * A custom directive that handles highlighting menu items automatically based upon the path navigated to by the user 40 | * Form validation using AngularJS 41 | 42 | 43 | ![Application page for Expense Management employees list view](ExpenseManager/Content/images/readmeImages/screenshot.png) 44 | 45 | ## Prerequisites 46 | 47 | * Azure subscription (trial will work) 48 | * Office 365 tenant 49 | * SharePoint site collection in your Office 365 tenant 50 | 51 | ## Office 365 and SharePoint Setup 52 | 53 | Following are the steps to upload the ExpensesTrackerSiteTemplate.wsp template into an existing Office 365/SharePoint site collection solution folder. Then create a Site instance within that site collection based on that site template called "Expense Tracker Site Template". This will create an Expenses site with 3 lists for employees, expenses, and states. 54 | 55 | 1. Go To the Admin Screen 56 | 57 | ![Active Directory](ExpenseManager/Content/images/readmeImages/1-GoToAdminScreen.png) 58 | 59 | 1. Create a new Site Collection 60 | 61 | ![Active Directory](ExpenseManager/Content/images/readmeImages/2-CreateNewSiteCollection.png) 62 | 63 | 1. Fill in New Site Collection Form, select template later under the custom tab for the template of the top level site. 64 | 65 | ![Active Directory](ExpenseManager/Content/images/readmeImages/3-FillInSiteCollectionFormChooseTemplateLater.png) 66 | 67 | 1. Click on the Solution Gallery Link. 68 | 69 | ![Active Directory](ExpenseManager/Content/images/readmeImages/4-clickonsolutiongallery.png) 70 | 71 | 1. Click the Upload Solution icon in the ribbon. 72 | 73 | ![Active Directory](ExpenseManager/Content/images/readmeImages/5-clickUploadSolution.png) 74 | 75 | 1. Browse to the .wsp file included in the package. (in this case it is the expenses.wsp) and upload the solution. 76 | 77 | ![Active Directory](ExpenseManager/Content/images/readmeImages/6-browsetosolutionfromgithubfolder.png) 78 | 1. Activate the Solution. 79 | 80 | ![Active Directory](ExpenseManager/Content/images/readmeImages/7-ActivateSolution.png) 81 | 1. Click the Browse tab to get back to the home page. 82 | 83 | ![Active Directory](ExpenseManager/Content/images/readmeImages/8-clickbrowse.png) 84 | 85 | 1. Click the Custom tab. 86 | 87 | ![Active Directory](ExpenseManager/Content/images/readmeImages/9-clickcustomtab.png) 88 | 89 | 1. Select the Expenses (the one you uploaded and activated) solution and click OK. 90 | 91 | ![Active Directory](ExpenseManager/Content/images/readmeImages/10-selectexpensesandclickok.png) 92 | 93 | 1. Set the default SharePoint Security Groups. Sometimes they line up perfectly, sometimes you might have to line them up with the drop down menu. 94 | 95 | ![Active Directory](ExpenseManager/Content/images/readmeImages/11-setdefaultgroups.png) 96 | 97 | 1. Browse to the Home Page. 98 | 99 | ![Active Directory](ExpenseManager/Content/images/readmeImages/12-HomePageTopLevelSiteWithLists.png) 100 | 101 | 1. If needed click on Site Contents to see the lists. 102 | 103 | 104 | ## Azure and Application Setup 105 | To get the application running you'll need to do the following: 106 | 107 | 1. Login to your Azure Management Portal and select Active Directory from the left menu. 108 | 109 | ![Active Directory](ExpenseManager/Content/images/readmeImages/ManagementServicesMenuItem.png) 110 | 111 | 1. Click on the directory you'd like to use (Default Directory will work fine) 112 | 113 | ![Active Directory](ExpenseManager/Content/images/readmeImages/DefaultDirectory.png) 114 | 115 | 1. Click the "Add an application you're developing" link 116 | 1. Give the application a name of Expense Manager: 117 | 118 | ![Active Directory](ExpenseManager/Content/images/readmeImages/AddApplication.png) 119 | 120 | 1. Click the arrow to go to the next screen and enter the following for the information in the screen. Substitute your Office 365 Tenant ID for YOUR_TENANT: 121 | 122 | ![Active Directory](ExpenseManager/Content/images/readmeImages/ApplicationProperties.png) 123 | 124 | 1. Press the Complete button in the wizard to create the application. 125 | 1. Click the CONFIGURE link at the top of the Expense Manager application screen. 126 | 1. Scroll to the "keys" section and select **1 year** from the dropdown. 127 | 1. Note the Client ID and key value that are displayed. You'll need to update the application's web.config file with these values in a moment. 128 | 129 | ![Active Directory](ExpenseManager/Content/images/readmeImages/ClientID.png) 130 | 131 | 1. Scroll down to the "permissions to other applications" section of the screen. 132 | 1. In the first dropdown in the Microsoft Azure Active Directory column select Office 365 SharePoint Online and make the selections shown next: 133 | 134 | ![Active Directory](ExpenseManager/Content/images/readmeImages/Permissions.png) 135 | 136 | 1. Click the Select application drop and add the following permission for Microsoft Azure Active Directory (see the first entry in the image below): 137 | 138 | ![Active Directory](ExpenseManager/Content/images/readmeImages/ADPermissions.png) 139 | 140 | 1. Click the Save icon at the bottom of the interface. 141 | 1. Open the Expense Manager's .sln file in Visual Studio 2013 or higher (click Download Zip in Github and extract the project if you haven't already) 142 | 1. Open web.config and replace Tenant, TenantID, ClientID and Password values with the values displayed in the Azure Directory screen shown earlier: 143 | 144 | ```html 145 | 146 | 147 | 148 | 149 | ``` 150 | 151 | 1. Open index.html from the root of the project and scroll to the bottom. 152 | 1. Locate the expenseManager.baseSPUrl variable and update YOUR_TENANT with your Office 365 tenant ID. 153 | 1. Press F5 to build and run the application. 154 | 1. You should be taken to a login screen where you can login using your Office 365 credentials. 155 | 156 | 157 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 158 | --------------------------------------------------------------------------------