├── .vscode └── settings.json ├── .gitignore ├── splitIt-app ├── src │ ├── assets │ │ └── .gitkeep │ ├── app │ │ ├── app.component.css │ │ ├── group │ │ │ ├── group.component.css │ │ │ ├── member │ │ │ │ └── add-member │ │ │ │ │ ├── add-member.component.css │ │ │ │ │ ├── add-member.component.spec.ts │ │ │ │ │ ├── add-member.component.html │ │ │ │ │ └── add-member.component.ts │ │ │ ├── balance │ │ │ │ └── list-balance │ │ │ │ │ ├── list-balance.component.css │ │ │ │ │ ├── list-balance.component.spec.ts │ │ │ │ │ ├── list-balance.component.ts │ │ │ │ │ └── list-balance.component.html │ │ │ ├── expense │ │ │ │ ├── add-expense │ │ │ │ │ ├── add-expense.component.css │ │ │ │ │ ├── add-expense.component.spec.ts │ │ │ │ │ ├── add-expense.component.html │ │ │ │ │ └── add-expense.component.ts │ │ │ │ └── list-expense │ │ │ │ │ ├── list-expense.component.css │ │ │ │ │ ├── expense-details │ │ │ │ │ ├── expense-details.component.css │ │ │ │ │ ├── expense-details.component.spec.ts │ │ │ │ │ ├── expense-details.component.ts │ │ │ │ │ └── expense-details.component.html │ │ │ │ │ ├── list-expense.component.spec.ts │ │ │ │ │ ├── list-expense.component.html │ │ │ │ │ └── list-expense.component.ts │ │ │ ├── group.module.ts │ │ │ ├── group.component.spec.ts │ │ │ ├── group-routing.module.ts │ │ │ ├── group.component.ts │ │ │ └── group.component.html │ │ ├── navbar │ │ │ ├── navbar.component.css │ │ │ ├── navbar.component.spec.ts │ │ │ ├── navbar.component.html │ │ │ └── navbar.component.ts │ │ ├── create-group │ │ │ ├── create-group.component.css │ │ │ ├── create-group.component.spec.ts │ │ │ ├── create-group.component.html │ │ │ └── create-group.component.ts │ │ ├── reset-password │ │ │ ├── reset-password.component.css │ │ │ ├── reset-password.component.spec.ts │ │ │ ├── reset-password.component.ts │ │ │ └── reset-password.component.html │ │ ├── loading-spinner │ │ │ ├── loading-spinner.component.css │ │ │ ├── loading-spinner.component.ts │ │ │ ├── loading-spinner.component.spec.ts │ │ │ └── loading-spinner.component.html │ │ ├── home │ │ │ ├── home.component.css │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.html │ │ │ └── home.component.ts │ │ ├── models │ │ │ ├── user.model.ts │ │ │ └── group.model.ts │ │ ├── expense.model.ts │ │ ├── abs.pipe.spec.ts │ │ ├── login │ │ │ ├── login.component.css │ │ │ ├── login.component.spec.ts │ │ │ ├── login.component.ts │ │ │ └── login.component.html │ │ ├── register │ │ │ ├── register.component.css │ │ │ ├── register.component.spec.ts │ │ │ ├── register.component.ts │ │ │ └── register.component.html │ │ ├── abs.pipe.ts │ │ ├── expense-filter.pipe.spec.ts │ │ ├── calculator │ │ │ ├── calculator.component.css │ │ │ ├── calculator.component.spec.ts │ │ │ ├── calculator.component.html │ │ │ └── calculator.component.ts │ │ ├── auth.service.spec.ts │ │ ├── group.service.spec.ts │ │ ├── users.service.spec.ts │ │ ├── expense.service.spec.ts │ │ ├── invitation.service.spec.ts │ │ ├── auth.guard.ts │ │ ├── reset-password.service.spec.ts │ │ ├── auth.guard.spec.ts │ │ ├── app.component.html │ │ ├── reset-password.service.ts │ │ ├── users.service.ts │ │ ├── app.component.ts │ │ ├── app-routing.module.ts │ │ ├── invitation.service.ts │ │ ├── app.component.spec.ts │ │ ├── auth.service.ts │ │ ├── expense.service.ts │ │ ├── expense-filter.pipe.ts │ │ ├── group.service.ts │ │ └── app.module.ts │ ├── favicon.ico │ ├── main.ts │ ├── styles.css │ └── index.html ├── postcss.config.js ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json └── angular.json ├── gitSnaps ├── Group.png ├── Home.png ├── Login.png ├── Expense.png ├── Group_1.png ├── Register.png ├── Create_Group.png ├── Group_Balance.png ├── Member_Added.png ├── Member_Splits.png ├── Home_Group_List.png ├── Settle_Balance.png ├── Settled_Balance.png ├── UnequalSplit_1.png ├── UnequalSplit_2.png ├── Expense_Category.png ├── Expense_Multiples.png ├── Group_Balance_For2.png ├── Inivitation_Sent.png ├── Login_Validations.png ├── Member_Validations.png ├── Inivitation_Received.png ├── Login_Validations_Ok.png ├── Register_Validations.png ├── Expense_Multiple_User.png ├── Member_Settled_Balance.png ├── Register_Validations_Ok.png ├── Group_Balance_Multiple_User.png ├── Participants_based_Expense_1.png ├── Participants_based_Expense_2.png ├── Participants_based_Expense_3.png ├── Participants_based_Expense_4.png ├── Settle_Balance_Confirmation.png └── Shares_Percentages_Calculator.png ├── server ├── models │ ├── otp.js │ ├── user.js │ ├── invitation.js │ ├── group.js │ └── expense.js ├── package.json ├── routes │ ├── users.js │ ├── groups.js │ ├── invitations.js │ └── expenses.js └── server.js ├── LICENSE ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /splitIt-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/group.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/navbar/navbar.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/create-group/create-group.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/reset-password/reset-password.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/member/add-member/add-member.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/loading-spinner/loading-spinner.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/balance/list-balance/list-balance.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/add-expense/add-expense.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/list-expense.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gitSnaps/Group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Group.png -------------------------------------------------------------------------------- /gitSnaps/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Home.png -------------------------------------------------------------------------------- /gitSnaps/Login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Login.png -------------------------------------------------------------------------------- /splitIt-app/src/app/home/home.component.css: -------------------------------------------------------------------------------- 1 | .minWidth{ 2 | min-width: 300px; 3 | } -------------------------------------------------------------------------------- /gitSnaps/Expense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Expense.png -------------------------------------------------------------------------------- /gitSnaps/Group_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Group_1.png -------------------------------------------------------------------------------- /gitSnaps/Register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Register.png -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/expense-details/expense-details.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gitSnaps/Create_Group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Create_Group.png -------------------------------------------------------------------------------- /gitSnaps/Group_Balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Group_Balance.png -------------------------------------------------------------------------------- /gitSnaps/Member_Added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Member_Added.png -------------------------------------------------------------------------------- /gitSnaps/Member_Splits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Member_Splits.png -------------------------------------------------------------------------------- /gitSnaps/Home_Group_List.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Home_Group_List.png -------------------------------------------------------------------------------- /gitSnaps/Settle_Balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Settle_Balance.png -------------------------------------------------------------------------------- /gitSnaps/Settled_Balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Settled_Balance.png -------------------------------------------------------------------------------- /gitSnaps/UnequalSplit_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/UnequalSplit_1.png -------------------------------------------------------------------------------- /gitSnaps/UnequalSplit_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/UnequalSplit_2.png -------------------------------------------------------------------------------- /splitIt-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/splitIt-app/src/favicon.ico -------------------------------------------------------------------------------- /gitSnaps/Expense_Category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Expense_Category.png -------------------------------------------------------------------------------- /gitSnaps/Expense_Multiples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Expense_Multiples.png -------------------------------------------------------------------------------- /gitSnaps/Group_Balance_For2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Group_Balance_For2.png -------------------------------------------------------------------------------- /gitSnaps/Inivitation_Sent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Inivitation_Sent.png -------------------------------------------------------------------------------- /gitSnaps/Login_Validations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Login_Validations.png -------------------------------------------------------------------------------- /gitSnaps/Member_Validations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Member_Validations.png -------------------------------------------------------------------------------- /gitSnaps/Inivitation_Received.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Inivitation_Received.png -------------------------------------------------------------------------------- /gitSnaps/Login_Validations_Ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Login_Validations_Ok.png -------------------------------------------------------------------------------- /gitSnaps/Register_Validations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Register_Validations.png -------------------------------------------------------------------------------- /gitSnaps/Expense_Multiple_User.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Expense_Multiple_User.png -------------------------------------------------------------------------------- /gitSnaps/Member_Settled_Balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Member_Settled_Balance.png -------------------------------------------------------------------------------- /gitSnaps/Register_Validations_Ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Register_Validations_Ok.png -------------------------------------------------------------------------------- /gitSnaps/Group_Balance_Multiple_User.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Group_Balance_Multiple_User.png -------------------------------------------------------------------------------- /gitSnaps/Participants_based_Expense_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Participants_based_Expense_1.png -------------------------------------------------------------------------------- /gitSnaps/Participants_based_Expense_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Participants_based_Expense_2.png -------------------------------------------------------------------------------- /gitSnaps/Participants_based_Expense_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Participants_based_Expense_3.png -------------------------------------------------------------------------------- /gitSnaps/Participants_based_Expense_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Participants_based_Expense_4.png -------------------------------------------------------------------------------- /gitSnaps/Settle_Balance_Confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Settle_Balance_Confirmation.png -------------------------------------------------------------------------------- /gitSnaps/Shares_Percentages_Calculator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihir09/SplitIt/HEAD/gitSnaps/Shares_Percentages_Calculator.png -------------------------------------------------------------------------------- /splitIt-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /splitIt-app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /splitIt-app/src/app/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | _id: string; 3 | username: string; 4 | email: string; 5 | password: string; 6 | groups: string[]; 7 | } -------------------------------------------------------------------------------- /splitIt-app/src/app/models/group.model.ts: -------------------------------------------------------------------------------- 1 | export interface Group { 2 | balancesWithNames: any; 3 | balance: any; 4 | _id: string; 5 | name: string; 6 | members: string[]; 7 | } -------------------------------------------------------------------------------- /splitIt-app/src/app/expense.model.ts: -------------------------------------------------------------------------------- 1 | export interface Expense { 2 | expenseName: string; 3 | payer: string; 4 | expenseDate: Date; 5 | description?: string; 6 | amount: number; 7 | } -------------------------------------------------------------------------------- /splitIt-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{html,js}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | 10 | -------------------------------------------------------------------------------- /splitIt-app/src/app/abs.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbsPipe } from './abs.pipe'; 2 | 3 | describe('AbsPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new AbsPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /splitIt-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /splitIt-app/src/app/login/login.component.css: -------------------------------------------------------------------------------- 1 | .error-message{ 2 | color: red; 3 | } 4 | 5 | .ng-valid[required], .ng-valid.required { 6 | border-left: 5px solid #42A948; 7 | } 8 | 9 | .ng-invalid:not(form) { 10 | border-left: 5px solid #a94442; 11 | } -------------------------------------------------------------------------------- /splitIt-app/src/app/register/register.component.css: -------------------------------------------------------------------------------- 1 | .error-message{ 2 | color: red; 3 | } 4 | 5 | .ng-valid[required], .ng-valid.required { 6 | border-left: 5px solid #42A948; 7 | } 8 | 9 | .ng-invalid:not(form) { 10 | border-left: 5px solid #a94442; 11 | } -------------------------------------------------------------------------------- /splitIt-app/src/app/abs.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'abs' 5 | }) 6 | export class AbsPipe implements PipeTransform { 7 | 8 | transform(value: number): number { 9 | return Math.abs(value); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /splitIt-app/src/app/expense-filter.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ExpenseFilterPipe } from './expense-filter.pipe'; 2 | 3 | describe('ExpenseFilterPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new ExpenseFilterPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /splitIt-app/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | html, body { height: 100%; } 6 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; background-color: rgb(243 244 246);} 7 | -------------------------------------------------------------------------------- /splitIt-app/src/app/loading-spinner/loading-spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loading-spinner', 5 | templateUrl: './loading-spinner.component.html', 6 | styleUrls: ['./loading-spinner.component.css'] 7 | }) 8 | export class LoadingSpinnerComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /splitIt-app/src/app/calculator/calculator.component.css: -------------------------------------------------------------------------------- 1 | .calculator { 2 | @apply w-48 mx-auto border rounded p-4 shadow-md; 3 | } 4 | 5 | .display { 6 | @apply text-3xl mb-2; 7 | } 8 | 9 | .buttons { 10 | @apply grid grid-cols-4 gap-2; 11 | } 12 | 13 | button { 14 | @apply p-2 text-lg bg-gray-200 hover:bg-gray-300 rounded; 15 | } -------------------------------------------------------------------------------- /splitIt-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": ["gtag.js"] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /server/models/otp.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // OTP schema 4 | const Schema = mongoose.Schema; 5 | const otpSchema = new Schema({ 6 | email: { type: String, required: true }, 7 | otp: { type: String, required: true }, 8 | expiration: { type: Date, required: true } 9 | }); 10 | 11 | module.exports = mongoose.model('OTP', otpSchema); -------------------------------------------------------------------------------- /splitIt-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /splitIt-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /splitIt-app/src/app/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { GroupService } from './group.service'; 4 | 5 | describe('GroupService', () => { 6 | let service: GroupService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(GroupService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /splitIt-app/src/app/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UsersService } from './users.service'; 4 | 5 | describe('UsersService', () => { 6 | let service: UsersService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UsersService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /splitIt-app/src/app/expense.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ExpenseService } from './expense.service'; 4 | 5 | describe('ExpenseService', () => { 6 | let service: ExpenseService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ExpenseService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "node server.js" 4 | }, 5 | "dependencies": { 6 | "@sendgrid/mail": "^8.1.1", 7 | "bcrypt": "^5.1.1", 8 | "body-parser": "^1.20.2", 9 | "cors": "^2.8.5", 10 | "dotenv": "^16.3.1", 11 | "express": "^4.18.2", 12 | "jsonwebtoken": "^9.0.2", 13 | "mongoose": "^7.5.3" 14 | }, 15 | "devDependencies": { 16 | "@types/cors": "^2.8.17", 17 | "@types/express": "^4.17.21" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /splitIt-app/src/app/invitation.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { InvitationService } from './invitation.service'; 4 | 5 | describe('InvitationService', () => { 6 | let service: InvitationService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(InvitationService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /splitIt-app/src/app/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivateFn, Router } from '@angular/router'; 2 | import { AuthService } from './auth.service'; 3 | import { inject } from '@angular/core'; 4 | 5 | export const authGuard: CanActivateFn = (route, state) => { 6 | const authService = inject(AuthService); 7 | const router = inject(Router); 8 | if(authService.isAuthenticated()){ 9 | return true; 10 | } 11 | else{ 12 | router.navigate(['/login']); 13 | return false; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /splitIt-app/src/app/reset-password.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ResetPasswordService } from './reset-password.service'; 4 | 5 | describe('ResetPasswordService', () => { 6 | let service: ResetPasswordService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ResetPasswordService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/group.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { GroupRoutingModule } from './group-routing.module'; 4 | import { DatePipe } from '@angular/common'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | ], 10 | imports: [ 11 | CommonModule, 12 | GroupRoutingModule, 13 | MatDialogModule, 14 | ], 15 | providers: [DatePipe], 16 | }) 17 | export class GroupModule { } 18 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // User schema 4 | const Schema = mongoose.Schema; 5 | const userSchema = new Schema({ 6 | username: String, 7 | email: { type: String, required: true, unique: true }, 8 | password: String, 9 | groups: [{ 10 | groupId: { type: mongoose.Schema.Types.ObjectId, ref: 'Group' }, 11 | groupName: String 12 | }], 13 | invitations: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Invitation' }] 14 | }); 15 | 16 | module.exports = mongoose.model('User', userSchema); -------------------------------------------------------------------------------- /splitIt-app/src/app/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { CanActivateFn } from '@angular/router'; 3 | 4 | import { authGuard } from './auth.guard'; 5 | 6 | describe('authGuard', () => { 7 | const executeGuard: CanActivateFn = (...guardParameters) => 8 | TestBed.runInInjectionContext(() => authGuard(...guardParameters)); 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({}); 12 | }); 13 | 14 | it('should be created', () => { 15 | expect(executeGuard).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /splitIt-app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /splitIt-app/src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [HomeComponent] 12 | }); 13 | fixture = TestBed.createComponent(HomeComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /server/models/invitation.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // Invitation schema 4 | const Schema = mongoose.Schema; 5 | const invitationSchema = Schema({ 6 | senderId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, 7 | recipientId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, 8 | groupName: { type: String, required: true }, 9 | status: { type: String, enum: ['pending', 'accepted', 'declined'], default: 'pending' }, 10 | groupId: { type: mongoose.Schema.Types.ObjectId, ref: 'groupId', required: true }, 11 | }, { timestamps: true }); 12 | 13 | module.exports = mongoose.model('Invitation', invitationSchema); -------------------------------------------------------------------------------- /splitIt-app/src/app/group/group.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GroupComponent } from './group.component'; 4 | 5 | describe('GroupComponent', () => { 6 | let component: GroupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [GroupComponent] 12 | }); 13 | fixture = TestBed.createComponent(GroupComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [LoginComponent] 12 | }); 13 | fixture = TestBed.createComponent(LoginComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NavbarComponent } from './navbar.component'; 4 | 5 | describe('NavbarComponent', () => { 6 | let component: NavbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [NavbarComponent] 12 | }); 13 | fixture = TestBed.createComponent(NavbarComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 26 | 27 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [RegisterComponent] 12 | }); 13 | fixture = TestBed.createComponent(RegisterComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/calculator/calculator.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CalculatorComponent } from './calculator.component'; 4 | 5 | describe('CalculatorComponent', () => { 6 | let component: CalculatorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [CalculatorComponent] 12 | }); 13 | fixture = TestBed.createComponent(CalculatorComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/reset-password.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class ResetPasswordService { 9 | private apiUrl = 'http://localhost:3000'; 10 | 11 | constructor(private http: HttpClient) { } 12 | 13 | sendResetOTP(email: string): Observable { 14 | return this.http.post(`${this.apiUrl}/api/reset-password`, { email }); 15 | } 16 | 17 | resetPassword(data: { email: string, otp: string, newPassword: string }): Observable { 18 | return this.http.post(`${this.apiUrl}/api/reset-password/verify`, data); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/member/add-member/add-member.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddMemberComponent } from './add-member.component'; 4 | 5 | describe('AddMemberComponent', () => { 6 | let component: AddMemberComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [AddMemberComponent] 12 | }); 13 | fixture = TestBed.createComponent(AddMemberComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/create-group/create-group.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CreateGroupComponent } from './create-group.component'; 4 | 5 | describe('CreateGroupComponent', () => { 6 | let component: CreateGroupComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [CreateGroupComponent] 12 | }); 13 | fixture = TestBed.createComponent(CreateGroupComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/add-expense/add-expense.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AddExpenseComponent } from './add-expense.component'; 4 | 5 | describe('AddExpenseComponent', () => { 6 | let component: AddExpenseComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [AddExpenseComponent] 12 | }); 13 | fixture = TestBed.createComponent(AddExpenseComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/balance/list-balance/list-balance.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListBalanceComponent } from './list-balance.component'; 4 | 5 | describe('ListBalanceComponent', () => { 6 | let component: ListBalanceComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ListBalanceComponent] 12 | }); 13 | fixture = TestBed.createComponent(ListBalanceComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/list-expense.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListExpenseComponent } from './list-expense.component'; 4 | 5 | describe('ListExpenseComponent', () => { 6 | let component: ListExpenseComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ListExpenseComponent] 12 | }); 13 | fixture = TestBed.createComponent(ListExpenseComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /splitIt-app/src/app/reset-password/reset-password.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ResetPasswordComponent } from './reset-password.component'; 4 | 5 | describe('ResetPasswordComponent', () => { 6 | let component: ResetPasswordComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ResetPasswordComponent] 12 | }); 13 | fixture = TestBed.createComponent(ResetPasswordComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Split It

5 |
6 |
7 | {{_userDetails.name}} 8 | 9 | 10 | 11 |
12 |
13 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/loading-spinner/loading-spinner.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadingSpinnerComponent } from './loading-spinner.component'; 4 | 5 | describe('LoadingSpinnerComponent', () => { 6 | let component: LoadingSpinnerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [LoadingSpinnerComponent] 12 | }); 13 | fixture = TestBed.createComponent(LoadingSpinnerComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/expense-details/expense-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExpenseDetailsComponent } from './expense-details.component'; 4 | 5 | describe('ExpenseDetailsComponent', () => { 6 | let component: ExpenseDetailsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ExpenseDetailsComponent] 12 | }); 13 | fixture = TestBed.createComponent(ExpenseDetailsComponent); 14 | component = fixture.componentInstance; 15 | fixture.detectChanges(); 16 | }); 17 | 18 | it('should create', () => { 19 | expect(component).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /server/models/group.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // Group schema 4 | const Schema = mongoose.Schema; 5 | const groupSchema = new Schema({ 6 | name: { type: String, required: true, unique: true }, 7 | members: [ 8 | { 9 | memberId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}, 10 | memberBalance: Number 11 | } 12 | ], 13 | expenses: [ 14 | { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: 'Expense', 17 | }, 18 | ], 19 | balance: [ 20 | { 21 | from: { type: mongoose.Schema.Types.ObjectId, ref: 'Member' }, 22 | to: { type: mongoose.Schema.Types.ObjectId, ref: 'Member' }, 23 | balance: Number 24 | } 25 | ] 26 | }); 27 | 28 | module.exports = mongoose.model('Group', groupSchema); -------------------------------------------------------------------------------- /splitIt-app/src/app/create-group/create-group.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Create Group

3 |
4 |
5 | 6 | 8 |
9 |
10 | {{ errorMessage }} 11 |
12 | 16 |
17 |
-------------------------------------------------------------------------------- /server/models/expense.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const expenseSchema = new mongoose.Schema({ 4 | expenseName: { 5 | type: String, 6 | required: true, 7 | }, 8 | payer: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: 'User', 11 | required: true, 12 | }, 13 | expenseDate: { 14 | type: Date, 15 | required: false, 16 | }, 17 | description: { 18 | type: String, 19 | required: false, 20 | }, 21 | amount: { 22 | type: Number, 23 | required: true, 24 | min: 0, 25 | }, 26 | groupId: { 27 | type: String, 28 | required: false, 29 | }, 30 | payerName: String, 31 | participants: { 32 | type: Map, 33 | of: Number, 34 | }, 35 | splitType: String, 36 | category: { 37 | type: String, 38 | required: false, 39 | }, 40 | }); 41 | 42 | module.exports = mongoose.model('Expense', expenseSchema); 43 | -------------------------------------------------------------------------------- /splitIt-app/src/app/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class UsersService { 9 | 10 | private baseUrl = 'http://localhost:3000/api/users'; 11 | 12 | constructor(private http: HttpClient) { } 13 | 14 | getUserGroups(userEmail: string): Observable { 15 | return this.http.get(`${this.baseUrl}/${userEmail}/groups`); 16 | } 17 | 18 | getUserDetails(userId: string): Observable { 19 | return this.http.get(`${this.baseUrl}/${userId}`); 20 | } 21 | 22 | getUserDetailsByEmail(userEmail: string): Observable { 23 | return this.http.get(`${this.baseUrl}/email/${userEmail}`); 24 | } 25 | 26 | getUserInvitations(userEmail: string): Observable { 27 | return this.http.get(`${this.baseUrl}/invitations/${userEmail}`); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /splitIt-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SplitIt 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /splitIt-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, AfterViewInit, HostListener, ViewChild } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent implements AfterViewInit { 9 | @ViewChild('scrollToTopButton') scrollToTopButton!: ElementRef; 10 | 11 | ngAfterViewInit() { 12 | this.scrollToTopButton.nativeElement.style.display = 'none'; 13 | } 14 | 15 | title = 'splitIt-app'; 16 | 17 | scrollToTop() { 18 | window.scrollTo({ top: 0, behavior: 'smooth' }); 19 | } 20 | 21 | @HostListener('window:scroll', []) 22 | onWindowScroll() { 23 | const scrollPosition = window.scrollY; 24 | 25 | if (scrollPosition >= 100) { 26 | this.scrollToTopButton.nativeElement.style.display = 'block' 27 | } else { 28 | this.scrollToTopButton.nativeElement.style.display = 'none' 29 | } 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /splitIt-app/src/app/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthService } from '../auth.service'; 3 | import { UsersService } from '../users.service'; 4 | 5 | @Component({ 6 | selector: 'app-navbar', 7 | templateUrl: './navbar.component.html', 8 | styleUrls: ['./navbar.component.css'] 9 | }) 10 | export class NavbarComponent { 11 | _user: string | void = ''; 12 | _userDetails: any; 13 | 14 | constructor(public authService: AuthService, public usersService: UsersService) { 15 | this._user = this.authService.getCurrentUser()! 16 | 17 | if(!this._user){ 18 | this.authService.logout() 19 | } 20 | this.usersService.getUserDetailsByEmail(this._user!).subscribe({ 21 | next: (res) => { 22 | this._userDetails = res; 23 | }, 24 | error: (error) => { 25 | console.error('Error fetching user details:', error); 26 | } 27 | }); 28 | 29 | } 30 | 31 | onLogout() { 32 | this.authService.logout(); 33 | } 34 | } -------------------------------------------------------------------------------- /splitIt-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /splitIt-app/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { LoginComponent } from './login/login.component'; 4 | import { RegisterComponent } from './register/register.component'; 5 | import { HomeComponent } from './home/home.component'; 6 | import { authGuard } from './auth.guard'; 7 | import { ResetPasswordComponent } from './reset-password/reset-password.component'; 8 | 9 | const routes: Routes = [ 10 | { path: 'login', component: LoginComponent }, 11 | { path: 'register', component: RegisterComponent }, 12 | { path: 'reset-password', component: ResetPasswordComponent }, 13 | { 14 | path: 'group/:groupId', 15 | loadChildren: () => import('./group/group.module').then(m => m.GroupModule), 16 | canActivate: [authGuard], 17 | }, 18 | { path: '', component: HomeComponent, canActivate: [authGuard]}, 19 | ]; 20 | 21 | @NgModule({ 22 | imports: [RouterModule.forRoot(routes)], 23 | exports: [RouterModule] 24 | }) 25 | export class AppRoutingModule { } 26 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/group-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { GroupComponent } from './group.component'; 4 | import { AddExpenseComponent } from './expense/add-expense/add-expense.component'; 5 | import { ListExpenseComponent } from './expense/list-expense/list-expense.component'; 6 | import { AddMemberComponent } from './member/add-member/add-member.component'; 7 | import { ListBalanceComponent } from './balance/list-balance/list-balance.component'; 8 | 9 | const routes: Routes = [ 10 | { 11 | path: '', 12 | component: GroupComponent, 13 | children: [ 14 | { path: 'add-expense', component: AddExpenseComponent }, 15 | { path: 'list-expense', component: ListExpenseComponent }, 16 | { path: 'add-member', component: AddMemberComponent }, 17 | { path: 'list-balance', component: ListBalanceComponent } 18 | ] 19 | }, 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class GroupRoutingModule { } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mihir Jagdishbhai Patel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software with some restrictions. You are allowed to fork, modify, and merge the Software, but copying, distributing, and/or selling/publishing copies of the software is strictly prohibited, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, 17 | OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. 19 | -------------------------------------------------------------------------------- /splitIt-app/src/app/invitation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class InvitationService { 9 | private apiUrl = 'http://localhost:3000/api/invitations'; 10 | 11 | constructor(private http: HttpClient) { } 12 | 13 | inviteNewUser(senderEmail: string, userEmail: string): Observable { 14 | return this.http.post(`${this.apiUrl}/invite`, { senderEmail, userEmail }); 15 | } 16 | 17 | sendInvitation(senderEmail: string, userEmail: string, groupId: string): Observable { 18 | return this.http.post(`${this.apiUrl}/send`, { senderEmail, userEmail, groupId }); 19 | } 20 | 21 | acceptInvitation(invitationId: string, userEmail: string): Observable { 22 | return this.http.post(`${this.apiUrl}/accept`, { invitationId, userEmail }); 23 | } 24 | 25 | declineInvitation(invitationId: string, userEmail: string): Observable { 26 | return this.http.post(`${this.apiUrl}/decline`, { invitationId, userEmail }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /splitIt-app/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /splitIt-app/src/app/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthService } from '../auth.service'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-register', 7 | templateUrl: './register.component.html', 8 | styleUrls: ['./register.component.css'] 9 | }) 10 | export class RegisterComponent { 11 | formData = { username: '', email: '', password: '' }; 12 | errorMessage: string ='' 13 | constructor(private authService: AuthService, private router: Router) {} 14 | 15 | onSubmit() { 16 | this.authService.register(this.formData).subscribe({ 17 | next : (response: any) => { 18 | localStorage.setItem('token', response.token) 19 | localStorage.setItem('userEmail', this.formData.email) 20 | this.errorMessage=''; 21 | this.router.navigate(['/']) 22 | // console.log('Registration successful', response); 23 | }, 24 | error :(error) => { 25 | this.errorMessage = error.error.message; 26 | console.error('Registration failed:', error.error.message); 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /splitIt-app/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(() => TestBed.configureTestingModule({ 7 | imports: [RouterTestingModule], 8 | declarations: [AppComponent] 9 | })); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have as title 'splitIt-app'`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('splitIt-app'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector('.content span')?.textContent).toContain('splitIt-app app is running!'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /splitIt-app/README.md: -------------------------------------------------------------------------------- 1 | # SplitItApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /splitIt-app/src/app/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AuthService { 9 | private baseUrl = 'http://localhost:3000/api'; 10 | 11 | constructor(private http: HttpClient, private router: Router) { 12 | window.addEventListener('onload', () => { 13 | localStorage.removeItem('token'); 14 | }); 15 | } 16 | 17 | register(user: any) { 18 | return this.http.post(`${this.baseUrl}/register`, user); 19 | } 20 | 21 | login(credentials: any) { 22 | return this.http.post(`${this.baseUrl}/login`, credentials); 23 | } 24 | 25 | isAuthenticated() { 26 | return !!localStorage.getItem('token') 27 | } 28 | 29 | logout() { 30 | localStorage.removeItem('token') 31 | localStorage.removeItem('userEmail') 32 | this.router.navigate(['/login']) 33 | } 34 | 35 | getCurrentUser(){ 36 | const userEmail = localStorage.getItem('userEmail'); 37 | if (userEmail){ 38 | return userEmail; 39 | } 40 | return this.logout(); 41 | } 42 | 43 | getToken() { 44 | return localStorage.getItem('token'); 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /splitIt-app/src/app/expense.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, Subject, tap } from 'rxjs'; 4 | import { GroupService } from './group.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class ExpenseService { 10 | private expensesUrl = 'http://localhost:3000/api/expenses'; 11 | private groupUrl = 'http://localhost:3000/api/groups'; 12 | 13 | constructor(private http: HttpClient, private groupService: GroupService) { } 14 | 15 | addExpense(expenseData: any): Observable { 16 | return this.http.post(`${this.expensesUrl}`, expenseData) 17 | } 18 | 19 | getExpensesOfGroup(groupId: string): Observable { 20 | return this.http.get(`${this.groupUrl}/${groupId}/expenses`); 21 | } 22 | 23 | deleteExpense(expenseId: string): Observable { 24 | const url = `${this.expensesUrl}/${expenseId}`; 25 | return this.http.delete(url); 26 | } 27 | 28 | editExpense(expenseId: string, expenseData: any, oldExpenseData: any): Observable { 29 | const editUrl = `${this.expensesUrl}/${expenseId}`; 30 | const combinedData = {expenseData:expenseData, oldExpenseData:oldExpenseData} 31 | return this.http.put(editUrl, combinedData); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /splitIt-app/src/app/group/group.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { GroupService } from '../group.service'; 3 | import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-group', 7 | templateUrl: './group.component.html', 8 | styleUrls: ['./group.component.css'] 9 | }) 10 | export class GroupComponent implements OnInit { 11 | groupId: string = ''; 12 | groupName: string = ''; 13 | 14 | constructor( 15 | private route: ActivatedRoute, 16 | private groupService: GroupService, 17 | private router: Router 18 | ) { } 19 | 20 | ngOnInit() { 21 | this.route.params.subscribe(res => this.groupId = res['groupId']); 22 | this.groupService.getGroupDetails(this.groupId).subscribe(response => this.groupName = response.name) 23 | } 24 | 25 | handleGroupName(groupName: string) { 26 | this.groupName = groupName 27 | } 28 | 29 | navigate(event: any): void { 30 | const routeName = (event.target as HTMLInputElement).value 31 | this.router.navigate(['group', this.groupId, routeName], { queryParams: { groupId: this.groupId } }); 32 | } 33 | 34 | @ViewChild(RouterOutlet, { static: true }) routerOutlet!: RouterOutlet; 35 | 36 | routerOutletHasContent(): boolean { 37 | return this.routerOutlet && this.routerOutlet.isActivated; 38 | } 39 | } -------------------------------------------------------------------------------- /splitIt-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "split-it-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.2.0", 14 | "@angular/cdk": "^16.2.11", 15 | "@angular/common": "^16.2.0", 16 | "@angular/compiler": "^16.2.0", 17 | "@angular/core": "^16.2.0", 18 | "@angular/forms": "^16.2.0", 19 | "@angular/material": "^16.2.11", 20 | "@angular/platform-browser": "^16.2.0", 21 | "@angular/platform-browser-dynamic": "^16.2.0", 22 | "@angular/router": "^16.2.0", 23 | "autoprefixer": "^10.4.16", 24 | "postcss": "^8.4.31", 25 | "rxjs": "~7.8.0", 26 | "tslib": "^2.3.0", 27 | "zone.js": "~0.13.0" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "^16.2.0", 31 | "@angular/cli": "~16.2.0", 32 | "@angular/compiler-cli": "^16.2.0", 33 | "@types/gtag.js": "^0.0.18", 34 | "@types/jasmine": "~4.3.0", 35 | "jasmine-core": "~4.6.0", 36 | "karma": "~6.4.0", 37 | "karma-chrome-launcher": "~3.2.0", 38 | "karma-coverage": "~2.2.0", 39 | "karma-jasmine": "~5.1.0", 40 | "karma-jasmine-html-reporter": "~2.1.0", 41 | "tailwindcss": "^3.3.3", 42 | "typescript": "~5.1.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /splitIt-app/src/app/expense-filter.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'expenseFilter' 5 | }) 6 | export class ExpenseFilterPipe implements PipeTransform { 7 | 8 | transform(items: any[], searchText: string, startDate: Date, endDate: Date): any[] { 9 | console.log(startDate, endDate) 10 | if (!items) { 11 | return items; 12 | } 13 | searchText = searchText.toLowerCase() 14 | return items.filter(item => { 15 | const isExpenseNameMatch = item.expenseName.toLowerCase().includes(searchText); 16 | const isPayerNameMatch = item.payerName.toLowerCase().includes(searchText); 17 | const isAmountMatch = item.amount.toString().includes(searchText); 18 | var dateCheck = false; 19 | var isDateInRange = false 20 | if (startDate && endDate) { 21 | isDateInRange = this.isDateInRange(item.expenseDate, startDate, endDate); 22 | dateCheck = true 23 | } 24 | 25 | return (isExpenseNameMatch || isPayerNameMatch || isAmountMatch) && (dateCheck? isDateInRange : true); 26 | }); 27 | } 28 | 29 | isDateInRange(date: Date, startDate: Date, endDate: Date): boolean { 30 | console.log(date, startDate, endDate) 31 | const isoDateString = date 32 | date = new Date(isoDateString); 33 | 34 | console.log(date); 35 | return (!startDate || date >= startDate) && (!endDate || date <= endDate); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /splitIt-app/src/app/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthService } from '../auth.service'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-login', 7 | templateUrl: './login.component.html', 8 | styleUrls: ['./login.component.css'] 9 | }) 10 | export class LoginComponent { 11 | formData = { email: '', password:'' }; 12 | errorMessage: string = ''; 13 | errorType: string = ''; 14 | constructor(private authService: AuthService, private router: Router) {} 15 | 16 | onSubmit() { 17 | this.authService.login(this.formData).subscribe({ 18 | next : (response: any)=> { 19 | localStorage.setItem('token', response.token) 20 | localStorage.setItem('userEmail', this.formData.email) 21 | this.errorMessage=''; 22 | this.router.navigate(['/']) 23 | // console.log('Login successful') 24 | }, 25 | error: (error) => { 26 | if (error.error.type && error.error.type=='incorrect_password'){ 27 | this.errorType = error.error.type; 28 | } 29 | let suggestion = ''; 30 | if (error.error.suggestion) 31 | { 32 | suggestion = error.error.suggestion 33 | } 34 | 35 | this.errorMessage = error.error.message + ' ' + suggestion; 36 | 37 | console.error('Login failed:', error.error.message); 38 | } 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /splitIt-app/src/app/calculator/calculator.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ display }}
3 |
{{operator}}
4 |
{{lastOperation}}
5 |
enter value
6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/expense-details/expense-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject} from '@angular/core'; 2 | import { GroupService } from 'src/app/group.service'; 3 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 4 | 5 | @Component({ 6 | selector: 'app-expense-details', 7 | templateUrl: './expense-details.component.html', 8 | styleUrls: ['./expense-details.component.css'], 9 | }) 10 | export class ExpenseDetailsComponent { 11 | expense: any; 12 | members: any = []; 13 | participants: any[] = []; 14 | participantsArray: any[] = []; 15 | 16 | constructor(private groupService: GroupService, 17 | public dialogRef: MatDialogRef, 18 | @Inject(MAT_DIALOG_DATA) public data: any 19 | ) { 20 | this.expense = data.expense; 21 | this.groupService.getMembers(this.expense.groupId).subscribe({ 22 | next: (response) => { 23 | this.members = response; 24 | this.participants = this.expense.participants 25 | this.updateParticipantsDetails(); 26 | }, 27 | error: (error) => { 28 | console.log(error); 29 | }, 30 | }); 31 | } 32 | 33 | 34 | private updateParticipantsDetails(): void { 35 | Object.entries(this.participants).forEach(([participantId, contributionAmount]: [string, any]): void => { 36 | const participant = this.members.find((member: any) => member.id === participantId); 37 | const participantName = participant ? participant.name : 'Unknown'; 38 | this.participantsArray.push({ name: participantName, amount: contributionAmount || 0 }); 39 | }); 40 | } 41 | 42 | onCloseClick(): void { 43 | this.dialogRef.close(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/expense-details/expense-details.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 8 |

Expense Details

9 |
10 |

Expense Name: {{ expense.expenseName }}

11 |

Expense Date: {{ expense.expenseDate | date }}

12 |

Payer Name: {{ expense.payerName }}

13 |

Amount Paid: {{ expense.amount }}

14 | 15 |
16 |

Participants:

17 |
    18 |
  • 19 | {{ participant.name }}'s share is {{ participant.amount }} 20 |
  • 21 |
22 |
23 | 24 |

Split Type: {{ expense.splitType }}

25 |

Expense Category: {{ expense.category }}

26 | 27 |

Expense Description: {{ expense.description }}

28 |
29 | 30 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/loading-spinner/loading-spinner.component.html: -------------------------------------------------------------------------------- 1 |
3 |

Simplify your group expenses with SplitIt! 📊 4 | Track balances, 💸 manage expenses, and keep everything in check. 5 | Choose an option to get started.🚀

6 |
7 | 16 | Loading... 17 |
18 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/create-group/create-group.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { GroupService } from '../group.service'; 4 | import { AuthService } from '../auth.service'; 5 | import { Router } from '@angular/router'; 6 | 7 | @Component({ 8 | selector: 'app-create-group', 9 | templateUrl: './create-group.component.html', 10 | styleUrls: ['./create-group.component.css'], 11 | }) 12 | export class CreateGroupComponent implements OnInit { 13 | createForm: FormGroup; 14 | errorMessage: string = ''; 15 | 16 | constructor(private fb: FormBuilder, private groupService: GroupService, private authService: AuthService, 17 | private router: Router) { 18 | this.createForm = this.fb.group({ 19 | name: ['', Validators.required], 20 | }); 21 | } 22 | 23 | ngOnInit(): void {} 24 | 25 | onCreateGroup() { 26 | if (this.createForm.valid) { 27 | const groupData = { 28 | ...this.createForm.value 29 | }; 30 | this.groupService.createGroup(groupData).subscribe( 31 | (group) => { 32 | // console.log('Group created successfully:'); 33 | this.groupService.addUserToGroup(group._id, this.authService.getCurrentUser()!).subscribe( 34 | (response) => { 35 | // console.log('User added to the group successfully'); 36 | this.router.navigate(['group',group._id ]) 37 | }, 38 | (error) => { 39 | console.error('Error adding user to group:', error.error.message); 40 | } 41 | ); 42 | this.createForm.reset(); 43 | this.errorMessage = ''; 44 | }, 45 | (error) => { 46 | this.errorMessage = error.error.message; 47 | console.error('Error creating group:', error); 48 | } 49 | ); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to SplitIt 2 | 3 | Welcome to SplitIt! We appreciate your interest in contributing. By contributing to this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). Please take a moment to review it before proceeding. 4 | 5 | ## How to Contribute 6 | 7 | 1. **Fork the Repository:** 8 | - Fork the repository to your GitHub account. 9 | 10 | 2. **Clone the Repository:** 11 | - Clone the forked repository to your local machine. 12 | ```sh 13 | git clone https://github.com/mihir09/SplitIt.git 14 | cd SplitIt 15 | ``` 16 | 17 | 3. **Create a Branch:** 18 | - Create a new branch for your contribution. 19 | ```sh 20 | git checkout -b feature/your-feature-name 21 | ``` 22 | 23 | 4. **Make Changes:** 24 | - Implement your changes or add new features. Please ensure your code follows our coding standards. 25 | 26 | 5. **Commit Changes:** 27 | - Commit your changes with a descriptive commit message. 28 | ```sh 29 | git commit -m "Add your descriptive commit message" 30 | ``` 31 | 32 | 6. **Push Changes:** 33 | - Push your changes to your forked repository. 34 | ```sh 35 | git push origin feature/your-feature-name 36 | ``` 37 | 38 | 8. **Submit a Pull Request (PR):** 39 | - Open a pull request from your forked repository to the main repository. Provide a clear title and description for your changes. 40 | 41 | 9. **Code Review:** 42 | - Participate in the code review process by responding to feedback and making necessary changes. 43 | 44 | ## Code of Conduct 45 | 46 | Please note that this project is released with a Contributor Code of Conduct. By participating in this project, you agree to abide by its terms. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details. 47 | 48 | ## Reporting Issues 49 | 50 | If you encounter any issues or have suggestions for improvements, please open an issue on the [Issues](../../issues) page. 51 | 52 | Thank you for contributing to [Your Project Name]! 🚀 53 | -------------------------------------------------------------------------------- /splitIt-app/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

Invitations

5 |
    6 |
  • 7 |
    8 |

    Invited by {{ invitation.senderName }} to join group "{{ invitation.groupName }}"

    9 |
    10 |
    11 | 12 | 13 |
    14 |
  • 15 |
16 |
17 | 18 | 19 |
20 |

Groups

21 | 26 |
27 | 28 | 30 | 31 |
33 |
34 | 36 | 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /splitIt-app/src/app/reset-password/reset-password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { ResetPasswordService } from './../reset-password.service'; 4 | 5 | @Component({ 6 | selector: 'app-reset-password', 7 | templateUrl: './reset-password.component.html', 8 | styleUrls: ['./reset-password.component.css'] 9 | }) 10 | export class ResetPasswordComponent implements OnInit { 11 | formData = { email: '', otp: '', newPassword: '' }; 12 | errorMessage: string = ''; 13 | resetComplete = false; 14 | passwordResetSuccess = false; 15 | 16 | constructor( 17 | private route: ActivatedRoute, 18 | private resetPasswordService: ResetPasswordService 19 | ) { } 20 | 21 | ngOnInit(): void { 22 | const email = this.route.snapshot.queryParamMap.get('email'); 23 | const otp = this.route.snapshot.queryParamMap.get('otp'); 24 | if (email && otp) { 25 | this.formData.email = email; 26 | this.formData.otp = otp; 27 | this.resetComplete = true; 28 | } 29 | } 30 | 31 | sendResetOTP(): void { 32 | this.errorMessage=''; 33 | this.resetPasswordService.sendResetOTP(this.formData.email).subscribe( 34 | (response: any) => { 35 | console.log(response); 36 | this.resetComplete = true; 37 | }, 38 | (error: any) => { 39 | console.error('Error occurred:', error); 40 | this.errorMessage = error.error.message || 'An error occurred while sending the reset OTP.'; 41 | } 42 | ); 43 | } 44 | 45 | resetPassword(): void { 46 | this.errorMessage=''; 47 | if (this.resetComplete) { 48 | this.resetPasswordService.resetPassword(this.formData).subscribe( 49 | (response: any) => { 50 | console.log(response); 51 | this.passwordResetSuccess = true; 52 | }, 53 | (error: any) => { 54 | console.error('Error occurred:', error); 55 | this.errorMessage = error.error.message || 'An error occurred while resetting the password.'; 56 | } 57 | ); 58 | } 59 | else{ 60 | this.errorMessage = 'Please generate new OTP and try again.' 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/member/add-member/add-member.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Add New Member

3 |
4 |
5 | 6 | 8 |
9 |
10 | {{ errorMessage }} 11 |
12 |
13 | 18 | 19 | 20 |
21 |
22 | {{ successMessage }} 23 |
24 | 28 |
29 | 30 | 31 |
32 |

Group Members

33 | 34 |
    35 |
  • 36 |
    37 |
    38 | 39 | 41 | 42 | 43 |

    {{ member.name }}

    44 |
    45 | 46 |
    47 |

    {{ member.email }}

    48 |
    49 |
    50 |
  • 51 |
52 |
53 | 54 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/calculator/calculator.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-calculator', 5 | templateUrl: './calculator.component.html', 6 | styleUrls: ['./calculator.component.css'] 7 | }) 8 | export class CalculatorComponent { 9 | display = '0'; 10 | currentInput = ''; 11 | operator = ''; 12 | previousValue = ''; 13 | lastOperation = ''; 14 | 15 | onDigitClick(digit: string): void { 16 | if (this.display === '0') { 17 | this.display = digit; 18 | } else { 19 | if (this.operator === '=') { 20 | this.display = digit; 21 | this.operator = ''; 22 | } 23 | else { 24 | this.display += digit; 25 | } 26 | } 27 | } 28 | 29 | onOperatorClick(operator: string): void { 30 | if (this.operator !== '') { 31 | this.calculate(); 32 | } 33 | this.operator = operator; 34 | 35 | this.previousValue = this.display; 36 | this.currentInput = ''; 37 | this.display = '0'; 38 | this.lastOperation = ''; 39 | } 40 | 41 | onEqualClick(): void { 42 | this.calculate(); 43 | this.operator = '='; 44 | } 45 | 46 | onClearClick(): void { 47 | this.display = '0'; 48 | this.currentInput = ''; 49 | this.operator = ''; 50 | 51 | console.log(this.operator) 52 | this.previousValue = ''; 53 | } 54 | 55 | onDoubleZeroClick() { 56 | this.display += '00'; 57 | } 58 | 59 | onClearEntryClick() { 60 | if (this.display.length > 1) { 61 | this.display = this.display.slice(0, -1); 62 | } 63 | else{ 64 | this.display = '0'; 65 | } 66 | } 67 | 68 | private calculate(): void { 69 | const currentValue = parseFloat(this.display); 70 | const previousValue = parseFloat(this.previousValue); 71 | switch (this.operator) { 72 | case '+': 73 | this.display = (previousValue + currentValue).toFixed(2).toString(); 74 | break; 75 | case '-': 76 | this.display = (previousValue - currentValue).toFixed(2).toString(); 77 | break; 78 | case '*': 79 | this.display = (previousValue * currentValue).toFixed(2).toString(); 80 | break; 81 | case '/': 82 | this.display = (previousValue / currentValue).toFixed(2).toString(); 83 | break; 84 | case '%': 85 | this.display = (previousValue * (currentValue/100)).toFixed(2).toString(); 86 | break; 87 | default: 88 | break; 89 | } 90 | this.lastOperation = this.previousValue + " " + this.operator + " " + currentValue.toString() + " = " + this.display; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /splitIt-app/src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 | 7 | 11 |
12 |
Email is required.
13 |
Please enter a valid email address.
14 |
15 |
16 | 17 |
18 | 19 | 22 |
23 |
Password is required.
24 |
25 |
26 | 27 |
28 | {{ errorMessage }} 29 |
30 |
31 | 32 |
33 | 34 |
35 | 37 |
38 |
39 |

New here? Register now and join us: 40 | 41 |

42 |
43 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/group/balance/list-balance/list-balance.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { AuthService } from 'src/app/auth.service'; 4 | import { GroupService } from 'src/app/group.service'; 5 | import { Group } from 'src/app/models/group.model'; 6 | import { UsersService } from 'src/app/users.service'; 7 | 8 | @Component({ 9 | selector: 'app-list-balance', 10 | templateUrl: './list-balance.component.html', 11 | styleUrls: ['./list-balance.component.css'] 12 | }) 13 | export class ListBalanceComponent { 14 | groupId: string = ''; 15 | members$!: any; 16 | groupDetails!: Group; 17 | currentUser: any; 18 | balanceWithNames!: any; 19 | settle: boolean = false; 20 | selectedBalance: any; 21 | loading: boolean = true; 22 | 23 | constructor( 24 | private groupService: GroupService, 25 | private usersService: UsersService, 26 | private route: ActivatedRoute, 27 | private authService: AuthService) { } 28 | 29 | ngOnInit() { 30 | this.route.queryParams.subscribe(params => { 31 | this.groupId = params['groupId']; 32 | this.currentUser = this.usersService.getUserDetailsByEmail(this.authService.getCurrentUser() || '').subscribe((response) => { 33 | this.currentUser = response 34 | }) 35 | this.fetchGroupDetails() 36 | }); 37 | } 38 | 39 | private fetchGroupDetails() { 40 | this.groupService.getGroupDetailsWithMembers(this.groupId).subscribe({ 41 | next: (groupDetails) => { 42 | this.groupDetails = groupDetails; 43 | this.members$ = this.groupDetails.members; 44 | this.balanceWithNames = this.groupDetails.balancesWithNames; 45 | this.members$.map((member: any) => { 46 | if (member.email == this.currentUser.email) { 47 | this.currentUser.balance = member.balance; 48 | } 49 | }) 50 | this.loading = false; 51 | 52 | }, 53 | error: (error) => { 54 | console.error('Error fetching group details', error); 55 | }, 56 | }); 57 | } 58 | 59 | 60 | confirmSettleBalance(index: number): void { 61 | const confirmed = window.confirm('Are you sure you want to settle this balance?'); 62 | if (confirmed) { 63 | this.loading = true; 64 | this.settleSelectedBalance(index); 65 | } 66 | } 67 | 68 | settleSelectedBalance(index: any) { 69 | const transactionId = this.groupDetails.balance[index]._id; 70 | this.groupService.settleBalance(this.groupId, transactionId).subscribe({ 71 | next: (response) => { 72 | console.log(response.message) 73 | this.fetchGroupDetails() 74 | }, 75 | error: (error) => { 76 | console.error('Error settling balance:', error); 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We, as contributors and maintainers, pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Being respectful of differing viewpoints and experiences 12 | - Gracefully accepting constructive criticism 13 | - Focusing on what is best for the community 14 | - Showing empathy towards other community members 15 | 16 | Examples of unacceptable behavior include: 17 | 18 | - The use of sexualized language or imagery, and unwelcome sexual attention or advances 19 | - Trolling, insulting/derogatory comments, and personal or political attacks 20 | - Public or private harassment 21 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 22 | - Other conduct which could reasonably be considered inappropriate in a professional setting 23 | 24 | ## Our Responsibilities 25 | 26 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 27 | 28 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 29 | 30 | ## Enforcement 31 | 32 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [mj7774542@gmail.com](mailto:mj7774542@gmail.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 33 | 34 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 35 | 36 | ## Attribution 37 | 38 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). 39 | 40 | For answers to common questions about this code of conduct, see [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). 41 | -------------------------------------------------------------------------------- /server/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const User = require('../models/user'); 4 | 5 | // Get all groups of a user by user Email 6 | router.get('/:userEmail/groups', async (req, res) => { 7 | try { 8 | const { userEmail } = req.params; 9 | 10 | const user = await User.findOne({ email: userEmail }).populate('groups'); 11 | 12 | if (!user) { 13 | return res.status(404).json({ message: 'User not found' }); 14 | } 15 | const groups = user.groups; 16 | 17 | return res.status(200).json(groups); 18 | } catch (error) { 19 | console.error(error); 20 | return res.status(500).json({ message: 'Internal server error' }); 21 | } 22 | }); 23 | 24 | // Get user details by user Id 25 | router.get('/:userId', async (req, res) => { 26 | try { 27 | 28 | const { userId } = req.params; 29 | const user = await User.findOne({ _id: userId }); 30 | 31 | if (!user) { 32 | return res.status(404).json({ message: 'User not found' }); 33 | } 34 | const userDetails = {name: user.username, email: user.email, id: user._id}; 35 | return res.status(200).json(userDetails); 36 | } catch (error) { 37 | console.error(error); 38 | return res.status(500).json({ message: 'Internal server error' }); 39 | } 40 | }); 41 | 42 | // Get user details by user Email 43 | router.get('/email/:userEmail', async (req, res) => { 44 | try { 45 | const { userEmail } = req.params; 46 | 47 | const user = await User.findOne({ email: userEmail }) 48 | 49 | if (!user) { 50 | return res.status(404).json({ message: 'User not found' }); 51 | } 52 | const userDetails = {name: user.username, email: user.email, id: user._id}; 53 | return res.status(200).json(userDetails); 54 | } catch (error) { 55 | console.error(error); 56 | return res.status(500).json({ message: 'Internal server error' }); 57 | } 58 | }); 59 | 60 | // Get user Invitations by user email 61 | router.get('/invitations/:userEmail', async (req, res) => { 62 | try { 63 | const { userEmail } = req.params; 64 | 65 | const user = await User.findOne({ email: userEmail }).populate('invitations') 66 | 67 | if (!user) { 68 | return res.status(404).json({ message: 'User not found' }); 69 | } 70 | const userInvitations = [] 71 | 72 | for (const invitation of user.invitations) { 73 | const sender = await User.findById(invitation.senderId); 74 | userInvitations.push({ id: invitation._id, senderName: sender.username, groupName: invitation.groupName }); 75 | } 76 | 77 | return res.status(200).json(userInvitations); 78 | } catch (error) { 79 | console.error(error); 80 | return res.status(500).json({ message: 'Internal server error' }); 81 | } 82 | }); 83 | 84 | module.exports = router; 85 | -------------------------------------------------------------------------------- /splitIt-app/src/app/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Register

4 |
5 |
6 | 7 | 10 |
11 |
Username is required.
12 |
13 |
14 | 15 |
16 | 17 | 21 |
22 |
Email is required.
23 |
Please enter a valid email address.
24 |
25 |
26 | 27 |
28 | 29 | 32 |
33 |
Password is required.
34 |
35 |
36 | 37 |
38 | {{ errorMessage }} 39 |
40 | 41 |
42 | 44 |
45 |
46 |

Already have an account? 47 | 48 |

49 |
50 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/group/balance/list-balance/list-balance.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Group Balance

4 | 5 |
6 |
    7 |
  • 9 | {{ balance.from == currentUser.name ? 'You owe' : balance.from+' owes' }} {{ balance.to == currentUser.name ? 'You' : balance.to }} 10 | ${{ balance.balance | abs | number: '1.2-2' }} 11 |
  • 12 |
13 |
14 | 18 |
19 |
20 | 21 |
22 |
    23 |
  • 26 | {{ balance.from }} owes {{ balance.to }} 27 | ${{ balance.balance | abs | number: '1.2-2' }} 28 |
  • 29 |
30 |
31 | 32 | 36 |
37 |
38 | 39 | This group is settled up. 40 | 41 |
42 | 43 |
44 |

Members Balance

45 | 46 |
    47 |
  • 48 | {{ member.name }} 52 | {{ member.balance === 0 ? 'is settled up' : member.balance >= 0 ? 'lent ' : 'owes ' }} 53 | {{ member.balance === 0 ? '' : member.balance | abs | currency }} 54 | 55 |
  • 56 |
57 |
58 | 59 |
60 | 61 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable, Subject, forkJoin, map, switchMap, tap } from 'rxjs'; 4 | import { Group } from './models/group.model'; 5 | import { AuthService } from './auth.service'; 6 | import { UsersService } from './users.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class GroupService { 12 | private groupUrl = 'http://localhost:3000/api/groups'; 13 | 14 | constructor(private http: HttpClient, private authService: AuthService, private usersService: UsersService) { } 15 | 16 | createGroup(groupData: any): Observable { 17 | groupData = { 18 | ...groupData, 19 | creatorEmailId : this.authService.getCurrentUser() 20 | } 21 | return this.http.post(`${this.groupUrl}`, groupData); 22 | } 23 | 24 | addUserToGroup(groupId: string, userEmail: string): Observable { 25 | 26 | return this.http.post(`${this.groupUrl}/${groupId}/addUserByEmail`, { 27 | userEmail 28 | }) 29 | } 30 | 31 | getGroupDetailsWithMembers(groupId: string): Observable { 32 | return this.getGroupDetails(groupId).pipe( 33 | switchMap((groupDetails: any) => 34 | this.fetchAndProcessMembers(groupDetails.members).pipe( 35 | map((members) => ({ 36 | ...groupDetails, 37 | members: members, 38 | balancesWithNames: this.mapBalancesWithNames(groupDetails.balance, members), 39 | })) 40 | ) 41 | ) 42 | ); 43 | } 44 | 45 | getGroupDetails(groupId: string): Observable{ 46 | return this.http.get(`${this.groupUrl}/${groupId}`) 47 | } 48 | 49 | private fetchAndProcessMembers(memberDetails: any[]): Observable { 50 | const memberDetailObservables = memberDetails.map((member) => 51 | this.usersService.getUserDetails(member.memberId) 52 | ); 53 | 54 | return forkJoin(memberDetailObservables).pipe( 55 | map((memberResponses) => 56 | memberResponses.map((response, index) => ({ 57 | name: response.name, 58 | id: response.id, 59 | email: response.email, 60 | balance: memberDetails[index].memberBalance, 61 | })) 62 | ) 63 | ); 64 | } 65 | 66 | private mapBalancesWithNames(balanceItems: any[], members: any[]): any[] { 67 | const memberDetailsMap = new Map(members.map((member) => [member.id, member.name])); 68 | 69 | return balanceItems.map((balanceItem: any) => ({ 70 | from: memberDetailsMap.get(balanceItem.from) || balanceItem.from, 71 | to: memberDetailsMap.get(balanceItem.to) || balanceItem.to, 72 | balance: balanceItem.balance, 73 | })); 74 | } 75 | 76 | getMembers(groupId: string): Observable { 77 | return this.getGroupDetails(groupId).pipe( 78 | switchMap((groupDetails: any) => this.fetchAndProcessMembers(groupDetails.members)) 79 | ); 80 | } 81 | 82 | settleBalance(groupId: string, transactionId: string): Observable { 83 | return this.http.post(`${this.groupUrl}/${groupId}/transaction/${transactionId}`, {}) 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /splitIt-app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "splitIt-app": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/split-it-app", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": [ 20 | "zone.js" 21 | ], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets" 26 | ], 27 | "styles": [ 28 | "@angular/material/prebuilt-themes/indigo-pink.css", 29 | "src/styles.css" 30 | ], 31 | "scripts": [] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "budgets": [ 36 | { 37 | "type": "initial", 38 | "maximumWarning": "500kb", 39 | "maximumError": "1mb" 40 | }, 41 | { 42 | "type": "anyComponentStyle", 43 | "maximumWarning": "2kb", 44 | "maximumError": "4kb" 45 | } 46 | ], 47 | "outputHashing": "all" 48 | }, 49 | "development": { 50 | "buildOptimizer": false, 51 | "optimization": false, 52 | "vendorChunk": true, 53 | "extractLicenses": false, 54 | "sourceMap": true, 55 | "namedChunks": true 56 | } 57 | }, 58 | "defaultConfiguration": "production" 59 | }, 60 | "serve": { 61 | "builder": "@angular-devkit/build-angular:dev-server", 62 | "configurations": { 63 | "production": { 64 | "browserTarget": "splitIt-app:build:production" 65 | }, 66 | "development": { 67 | "browserTarget": "splitIt-app:build:development" 68 | } 69 | }, 70 | "defaultConfiguration": "development" 71 | }, 72 | "extract-i18n": { 73 | "builder": "@angular-devkit/build-angular:extract-i18n", 74 | "options": { 75 | "browserTarget": "splitIt-app:build" 76 | } 77 | }, 78 | "test": { 79 | "builder": "@angular-devkit/build-angular:karma", 80 | "options": { 81 | "polyfills": [ 82 | "zone.js", 83 | "zone.js/testing" 84 | ], 85 | "tsConfig": "tsconfig.spec.json", 86 | "assets": [ 87 | "src/favicon.ico", 88 | "src/assets" 89 | ], 90 | "styles": [ 91 | "@angular/material/prebuilt-themes/indigo-pink.css", 92 | "src/styles.css" 93 | ], 94 | "scripts": [] 95 | } 96 | } 97 | } 98 | } 99 | }, 100 | "cli": { 101 | "analytics": "d80f8473-aa98-41d5-af19-330359fde63f" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /splitIt-app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { RegisterComponent } from './register/register.component'; 7 | import { LoginComponent } from './login/login.component'; 8 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 9 | import { HomeComponent } from './home/home.component'; 10 | import { AuthService } from './auth.service'; 11 | import { HttpClientModule } from '@angular/common/http'; 12 | import { CreateGroupComponent } from './create-group/create-group.component'; 13 | import { GroupComponent } from './group/group.component'; 14 | 15 | import { NavbarComponent } from './navbar/navbar.component'; 16 | import { ExpenseFilterPipe } from './expense-filter.pipe'; 17 | 18 | import { MatCardModule } from '@angular/material/card'; 19 | import { MatDatepickerModule } from '@angular/material/datepicker'; 20 | import { MatFormFieldModule } from '@angular/material/form-field'; 21 | import { MatInputModule } from '@angular/material/input'; 22 | import { MatNativeDateModule } from '@angular/material/core'; 23 | import { MatIconModule } from '@angular/material/icon'; 24 | import { MatPaginatorModule } from '@angular/material/paginator'; 25 | import { MatSortModule } from '@angular/material/sort'; 26 | import { MatTableModule } from '@angular/material/table'; 27 | import { MatTooltipModule } from '@angular/material/tooltip'; 28 | import { MatSelectModule } from '@angular/material/select'; 29 | 30 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 31 | import { AddExpenseComponent } from './group/expense/add-expense/add-expense.component'; 32 | import { ListExpenseComponent } from './group/expense/list-expense/list-expense.component'; 33 | import { ExpenseDetailsComponent } from './group/expense/list-expense/expense-details/expense-details.component'; 34 | 35 | import { AddMemberComponent } from './group/member/add-member/add-member.component'; 36 | 37 | import { ListBalanceComponent } from './group/balance/list-balance/list-balance.component'; 38 | import { AbsPipe } from './abs.pipe'; 39 | import { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component'; 40 | import { CalculatorComponent } from './calculator/calculator.component'; 41 | import { ResetPasswordComponent } from './reset-password/reset-password.component'; 42 | 43 | @NgModule({ 44 | declarations: [ 45 | AppComponent, 46 | RegisterComponent, 47 | LoginComponent, 48 | HomeComponent, 49 | CreateGroupComponent, 50 | GroupComponent, 51 | NavbarComponent, 52 | ExpenseFilterPipe, 53 | AddExpenseComponent, 54 | ListExpenseComponent, 55 | ExpenseDetailsComponent, 56 | AddMemberComponent, 57 | ListBalanceComponent, 58 | AbsPipe, 59 | LoadingSpinnerComponent, 60 | CalculatorComponent, 61 | ResetPasswordComponent, 62 | ], 63 | imports: [ 64 | MatCardModule, 65 | BrowserModule, 66 | AppRoutingModule, 67 | FormsModule, 68 | HttpClientModule, 69 | ReactiveFormsModule, 70 | BrowserAnimationsModule, 71 | MatDatepickerModule, 72 | MatFormFieldModule, 73 | MatInputModule, 74 | MatNativeDateModule, 75 | MatIconModule, 76 | MatPaginatorModule, 77 | MatTableModule, 78 | MatSortModule, 79 | MatTooltipModule, 80 | MatSelectModule, 81 | BrowserAnimationsModule, 82 | ], 83 | providers: [AuthService], 84 | bootstrap: [AppComponent] 85 | }) 86 | export class AppModule { } 87 | -------------------------------------------------------------------------------- /splitIt-app/src/app/reset-password/reset-password.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Reset Password

4 | 5 |
6 |
7 | 8 | 12 |
13 |
Email is required.
14 |
Please enter a valid email address.
15 |
16 |
17 | 18 |
19 | {{ errorMessage }} 20 |
21 | 22 |
23 | 25 |
26 |
27 | 28 |
29 |

An OTP has been sent to your email. Please check your inbox (and spam folder) for the OTP.

30 |

If you didn't receive the email, you can also enter the OTP manually:

31 |
32 |
33 | 34 | 37 |
38 |
OTP is required.
39 |
40 |
41 | 42 |
43 | 44 | 47 |
48 |
New password is required.
49 |
50 |
51 | 52 |
53 | 56 |
57 |
58 |
59 |
60 |
61 |

Password reset successfully. Login now.

62 |
63 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, ChangeDetectorRef } from '@angular/core'; 2 | import { UsersService } from '../users.service'; 3 | import { AuthService } from '../auth.service'; 4 | import { DOCUMENT } from '@angular/common'; 5 | import { ActivatedRoute, NavigationEnd, Router, RouterState } from '@angular/router'; 6 | import { InvitationService } from '../invitation.service'; 7 | 8 | @Component({ 9 | selector: 'app-home', 10 | templateUrl: './home.component.html', 11 | styleUrls: ['./home.component.css'] 12 | }) 13 | export class HomeComponent { 14 | groups: any[] = []; 15 | invitations: any[] = []; 16 | showCreateGroup: boolean = false; 17 | currentUser: string = ''; 18 | 19 | constructor( 20 | private usersService: UsersService, 21 | private authService: AuthService, 22 | private invitationsService: InvitationService, 23 | private router: Router, 24 | private cdr: ChangeDetectorRef, 25 | @Inject(DOCUMENT) private document: Document 26 | ) { 27 | this.loadUserGroups(); 28 | this.handleRouteEvents(); 29 | } 30 | 31 | ngOnInit(): void { 32 | this.currentUser = this.authService.getCurrentUser()! 33 | this.usersService.getUserInvitations(this.currentUser).subscribe({ 34 | next: (invitations) => { 35 | this.invitations = invitations; 36 | }, 37 | error: (error) => { 38 | console.error('Error fetching invitations:', error); 39 | } 40 | }); 41 | } 42 | 43 | loadUserGroups() { 44 | this.usersService.getUserGroups(this.authService.getCurrentUser()!).subscribe({ 45 | next : (groups) => { 46 | this.groups = groups; 47 | }, 48 | error: (error) => { 49 | console.error('Error loading user groups:', error.error.message); 50 | } 51 | }); 52 | } 53 | 54 | toggleCreateGroup() { 55 | this.showCreateGroup = !this.showCreateGroup; 56 | } 57 | 58 | handleRouteEvents() { 59 | this.router.events.subscribe(event => { 60 | if (event instanceof NavigationEnd) { 61 | const title = this.getTitle(this.router.routerState, this.router.routerState.root).join('-'); 62 | gtag('event', 'page_view', { 63 | page_title: title, 64 | page_path: event.urlAfterRedirects, 65 | page_location: this.document.location.href 66 | }) 67 | } 68 | }); 69 | } 70 | 71 | getTitle(state: RouterState, parent: ActivatedRoute): string[] { 72 | const data = []; 73 | if (parent && parent.snapshot.data && parent.snapshot.data['title']) { 74 | data.push(parent.snapshot.data['title']); 75 | } 76 | if (state && parent && parent.firstChild) { 77 | data.push(...this.getTitle(state, parent.firstChild)); 78 | } 79 | return data; 80 | } 81 | 82 | acceptInvitation(invitationId: string){ 83 | this.invitationsService.acceptInvitation(invitationId, this.currentUser).subscribe({ 84 | next : (response) => { 85 | // console.log("Accepted", response.message) 86 | this.router.navigate(['group', response.groupId ]) 87 | }, 88 | error: (error) => { 89 | console.error('Error ', error.error.message); 90 | } 91 | }); 92 | } 93 | 94 | declineInvitation(invitationId: string){ 95 | this.invitationsService.declineInvitation(invitationId, this.currentUser).subscribe({ 96 | next : (message) => { 97 | console.log("Declined", message) 98 | this.invitations = this.invitations.filter(invitation => invitation._id !== invitationId); 99 | this.usersService.getUserInvitations(this.currentUser).subscribe({ 100 | next: (invitations) => { 101 | this.invitations = invitations; 102 | }, 103 | error: (error) => { 104 | console.error('Error fetching invitations:', error); 105 | } 106 | }); 107 | }, 108 | error: (error) => { 109 | console.error('Error ', error.error.message); 110 | } 111 | }); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/list-expense.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Expenses

3 | 4 |
5 |
6 | 9 |
10 |
11 | 12 |
13 | 14 | Enter a date range 15 | 16 | 18 | 20 | 21 | 22 | 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 | 62 | 63 | 64 | 65 | 66 |
Expense Date{{ element.expenseDate | date }}Expense Name{{ element.expenseName }}Payer Name{{ element.payerName }}Amount Paid{{ element.amount }}Actions 50 |
51 | 54 | 57 | 60 |
61 |
67 | 68 |
69 | 70 | 71 |
72 | 73 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/member/add-member/add-member.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { GroupService } from '../../../group.service'; 4 | import { InvitationService } from '../../../invitation.service'; 5 | import { ActivatedRoute, Router } from '@angular/router'; 6 | import { AuthService } from 'src/app/auth.service'; 7 | 8 | @Component({ 9 | selector: 'app-add-member', 10 | templateUrl: './add-member.component.html', 11 | styleUrls: ['./add-member.component.css'] 12 | }) 13 | export class AddMemberComponent implements OnInit { 14 | createForm: FormGroup; 15 | errorMessage: string = ''; 16 | successMessage: string = ''; 17 | groupId: string = ''; 18 | members: any = []; 19 | showInviteButton: boolean = false; 20 | userEmailToInvite: string = ''; 21 | 22 | constructor(private fb: FormBuilder, 23 | private groupService: GroupService, 24 | private invitationService: InvitationService, 25 | private authService: AuthService, 26 | private route: ActivatedRoute, 27 | private router: Router) { 28 | this.createForm = this.fb.group({ 29 | email: ['', Validators.required], 30 | }); 31 | } 32 | 33 | ngOnInit(): void { 34 | this.route.queryParams.subscribe(params => { 35 | this.groupId = params['groupId']; 36 | this.errorMessage = ''; 37 | this.successMessage = ''; 38 | this.groupService.getMembers(this.groupId).subscribe({ 39 | next: (response) => { 40 | this.members = response 41 | }, 42 | error: (error) => { 43 | console.log(error) 44 | } 45 | }) 46 | }); 47 | 48 | } 49 | onAddUser() { 50 | if (this.createForm.valid) { 51 | this.errorMessage = ''; 52 | this.successMessage = ''; 53 | const userEmail = this.createForm.value.email; 54 | const senderEmail = this.authService.getCurrentUser(); 55 | this.invitationService.sendInvitation(senderEmail!, userEmail, this.groupId).subscribe({ 56 | next: (response) => { 57 | this.successMessage = 'Invitation sent successfully to ' + userEmail; 58 | this.errorMessage = ''; 59 | this.createForm.reset(); 60 | }, 61 | error: (error) => { 62 | this.showInviteButton = false; 63 | switch (error.error.type) { 64 | case 'user_not_found': 65 | this.errorMessage = error.error.message + " " + error.error.suggestion; 66 | this.showInviteButton = true; 67 | this.userEmailToInvite = userEmail; 68 | break; 69 | case 'user_already_present': 70 | this.errorMessage = error.error.message + " " + error.error.suggestion; 71 | break; 72 | case 'user_already_invited': 73 | this.errorMessage = error.error.message + " " + error.error.suggestion; 74 | break; 75 | 76 | case 'group_not_found': 77 | this.errorMessage = error.error.message + " " + error.error.suggestion; 78 | break; 79 | 80 | case 'sender_not_found': 81 | this.errorMessage = error.error.message + " You will be logged out in 10 seconds. " + error.error.suggestion; 82 | setTimeout(() => { 83 | this.authService.logout(); 84 | this.router.navigate(['/login']); 85 | }, 10000); 86 | break; 87 | default: 88 | this.errorMessage = 'An unexpected error occurred.'; 89 | } 90 | console.error('Error adding user to group:', error); 91 | } 92 | }); 93 | } 94 | } 95 | 96 | onInvite(userEmail: string) { 97 | this.errorMessage = ''; 98 | this.successMessage = ''; 99 | const senderEmail = this.authService.getCurrentUser(); 100 | this.showInviteButton = false; 101 | this.invitationService.inviteNewUser(senderEmail!, userEmail).subscribe({ 102 | next: (response) => { 103 | this.successMessage = 'Invitation sent successfully to ' + userEmail; 104 | }, 105 | error: (error) => { 106 | console.error('Error sending invitation:', error); 107 | this.errorMessage = 'Error sending invite. Please try again.'; 108 | } 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/group.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |

{{groupName}}

7 |
8 |
9 |
10 | 18 |
19 |
20 |
21 |
22 | 23 | 24 | 44 | 45 | 72 | 73 | 74 |
75 | 76 | 77 |
78 |

79 | Simplify your group expenses with SplitIt! 📊 Track balances, 💸 manage expenses, and keep everything in check. 80 | Choose an option to get started.🚀 81 |

82 |
83 |
84 |
85 | 86 |
87 |
-------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/list-expense/list-expense.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild} from '@angular/core'; 2 | import { ExpenseService } from 'src/app/expense.service'; 3 | import { MatPaginator } from '@angular/material/paginator'; 4 | import { MatSort } from '@angular/material/sort'; 5 | import { MatTableDataSource } from '@angular/material/table'; 6 | import { ActivatedRoute, Router } from '@angular/router'; 7 | import { Expense } from 'src/app/expense.model'; 8 | import { MatDialog } from '@angular/material/dialog'; 9 | import { ExpenseDetailsComponent } from './expense-details/expense-details.component'; 10 | 11 | @Component({ 12 | selector: 'app-list-expense', 13 | templateUrl: './list-expense.component.html', 14 | styleUrls: ['./list-expense.component.css'] 15 | }) 16 | export class ListExpenseComponent implements OnInit { 17 | members: any[] = []; 18 | groupId!: string; 19 | expenses: any[] = []; 20 | searchTerm: string = ''; 21 | startDate!: Date | null; 22 | endDate!: Date | null; 23 | loading: boolean = true; 24 | selectedExpense: any | undefined; 25 | 26 | @ViewChild(MatSort) sort!: MatSort; 27 | @ViewChild(MatPaginator) paginator!: MatPaginator; 28 | 29 | displayedColumns: string[] = ['expenseDate', 'expenseName', 'payerName', 'amount', 'actions']; 30 | expenseList = new MatTableDataSource([]); 31 | 32 | constructor(private expenseService: ExpenseService, 33 | private route: ActivatedRoute, 34 | private router: Router, 35 | private dialog: MatDialog) { } 36 | 37 | ngOnInit(): void { 38 | this.route.queryParams.subscribe(params => { 39 | this.groupId = params['groupId']; 40 | this.fetchExpenses(); 41 | }); 42 | } 43 | 44 | 45 | fetchExpenses() { 46 | this.expenseService.getExpensesOfGroup(this.groupId).subscribe({ 47 | next: (expenses) => { 48 | this.expenseList = new MatTableDataSource(expenses) 49 | this.expenseList.paginator = this.paginator; 50 | this.expenseList.sort = this.sort; 51 | this.expenseList.filterPredicate = this.expenseFilter(); 52 | this.loading = false; 53 | }, 54 | error: (error) => { 55 | console.error('Error fetching expenses', error); 56 | }, 57 | }); 58 | } 59 | 60 | applyFilterExpense(): void { 61 | const filterValue = this.searchTerm.toLowerCase(); 62 | const filterObj: any = {searchTerm : filterValue, startDate: this.startDate, endDate:this.endDate}; 63 | this.expenseList.filter = filterObj; 64 | } 65 | 66 | expenseFilter(): (data: any, filter: any) => boolean { 67 | const filterFunction = (data: any, filter: any): boolean => { 68 | const searchText = filter.searchTerm; 69 | const isExpenseNameMatch = data.expenseName.toLowerCase().includes(searchText); 70 | const isPayerNameMatch = data.payerName.toLowerCase().includes(searchText); 71 | const isAmountMatch = data.amount.toString().includes(searchText); 72 | var dateCheck = false; 73 | var isDateInRange = false 74 | if (filter.startDate && filter.endDate) { 75 | isDateInRange = this.isDateInRange(data.expenseDate, filter.startDate, filter.endDate); 76 | dateCheck = true 77 | } 78 | 79 | return (filter? (isExpenseNameMatch || isPayerNameMatch || isAmountMatch) : true) && (dateCheck? isDateInRange : true); 80 | }; 81 | 82 | return filterFunction; 83 | } 84 | 85 | isDateInRange(date: Date, startDate: Date, endDate: Date): boolean { 86 | const isoDateString = date 87 | date = new Date(isoDateString); 88 | return (!startDate || date >= startDate) && (!endDate || date <= endDate); 89 | } 90 | 91 | clearDateRange(){ 92 | this.startDate = null; 93 | this.endDate = null; 94 | this.applyFilterExpense(); 95 | } 96 | 97 | deleteExpense(expense: any): void { 98 | const confirmDelete = confirm('Are you sure you want to delete this expense?'); 99 | if (confirmDelete) { 100 | this.loading = true; 101 | this.expenseService.deleteExpense(expense._id).subscribe({ 102 | next: (res) => { 103 | this.fetchExpenses(); 104 | }, 105 | error: (error) => { 106 | console.error('Error deleting expense', error); 107 | }, 108 | }); 109 | } 110 | } 111 | 112 | editExpense(expense: any) { 113 | const expenseString = JSON.stringify(expense); 114 | this.router.navigate(['group', this.groupId, 'add-expense'], { 115 | queryParams: { 116 | mode: 'edit', 117 | expense: expenseString, 118 | groupId: this.groupId 119 | }, 120 | }); 121 | } 122 | 123 | showExpenseDetails(expense: Expense): void { 124 | this.selectedExpense = expense; 125 | const dialogRef = this.dialog.open(ExpenseDetailsComponent, { 126 | width: '400px', 127 | data: { expense: expense } 128 | }); 129 | 130 | dialogRef.afterClosed().subscribe(result => { 131 | // console.log('The dialog was closed'); 132 | }); 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /server/routes/groups.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | const router = express.Router(); 4 | const Group = require('../models/group'); 5 | const User = require('../models/user'); 6 | 7 | const expenseRouter = require('./expenses'); 8 | router.use('/expenses', expenseRouter); 9 | 10 | // Create a new group 11 | router.post('/', async (req, res) => { 12 | try { 13 | const { name } = req.body; 14 | 15 | const existingGroup = await Group.findOne({ name }); 16 | if (existingGroup) { 17 | return res.status(400).json({ message: 'Group name is already in use. Please choose a different name.' }); 18 | } 19 | 20 | const group = new Group({ name }); 21 | await group.save(); 22 | 23 | return res.status(201).json(group); 24 | } catch (error) { 25 | console.error(error); 26 | return res.status(500).json({ message: 'Internal server error' }); 27 | } 28 | }); 29 | 30 | // Add user to a group by email 31 | router.post('/:groupId/addUserByEmail', async (req, res) => { 32 | try { 33 | const { groupId } = req.params; 34 | const { userEmail } = req.body; 35 | 36 | const group = await Group.findById(groupId); 37 | 38 | if (!group) { 39 | return res.status(404).json({ message: 'Group not found' }); 40 | } 41 | 42 | const user = await User.findOne({ email: userEmail }); 43 | 44 | if (user) { 45 | if (!group.members.some((member) => member.memberId.equals(user._id))) { 46 | group.members.push({memberId:user._id, memberBalance: 0}); 47 | user.groups.push({groupId:group._id, groupName: group.name}); 48 | await user.save(); 49 | } 50 | else{ 51 | return res.status(404).json({ message: 'User already present in group.'}) 52 | } 53 | } else { 54 | return res.status(404).json({ message: 'User not found. Please re-check the email' }) 55 | } 56 | 57 | await group.save(); 58 | 59 | return res.status(200).json({ message: 'Users added to the group successfully' }); 60 | } catch (error) { 61 | console.error(error); 62 | return res.status(500).json({ message: 'Internal server error' }); 63 | } 64 | }); 65 | 66 | // Fetch group details 67 | router.get('/:groupId', async (req, res) => { 68 | try { 69 | const { groupId } = req.params; 70 | 71 | const group = await Group.findById(groupId); 72 | 73 | if (!group) { 74 | return res.status(404).json({ message: 'Group not found' }); 75 | } 76 | 77 | return res.status(201).json(group); 78 | } catch (error) { 79 | console.error(error); 80 | return res.status(500).json({ message: 'Internal server error' }); 81 | } 82 | }); 83 | 84 | // Fetch expenses in group 85 | router.get('/:groupId/expenses', async (req, res) => { 86 | try { 87 | const { groupId } = req.params; 88 | 89 | const group = await Group.findById(groupId).populate('expenses'); 90 | if (!group) { 91 | return res.status(404).json({ message: 'Group not found' }); 92 | } 93 | 94 | const expenses = group.expenses 95 | return res.status(201).json(expenses); 96 | } catch (error) { 97 | console.error(error); 98 | return res.status(500).json({ message: 'Internal server error' }); 99 | } 100 | }); 101 | 102 | // Settle transaction 103 | router.post('/:groupId/transaction/:transactionId', async (req, res) => { 104 | try { 105 | const { groupId, transactionId } = req.params; 106 | const group = await Group.findById(groupId) 107 | const transactionIndex = group.balance.findIndex( 108 | (transaction) => transaction._id.toString() === transactionId 109 | ); 110 | 111 | if (transactionIndex === -1) { 112 | return res.status(404).json({ message: 'Transaction not found' }); 113 | } 114 | // console.log(group.balance[transactionIndex]) 115 | 116 | 117 | // console.log('debtor', group.members.find(member => member.memberId.toString() == group.balance[transactionIndex].from)) 118 | debtor = group.members.find(member => member.memberId.toString() == group.balance[transactionIndex].from) 119 | debtor.memberBalance += group.balance[transactionIndex].balance; 120 | debtor.memberBalance = Number(debtor.memberBalance).toFixed(2); 121 | // console.log(debtor.memberBalance) 122 | 123 | // console.log('creditor', group.members.find(member => member.memberId.toString() == group.balance[transactionIndex].to)) 124 | creditor = group.members.find(member => member.memberId.toString() == group.balance[transactionIndex].to) 125 | creditor.memberBalance -= group.balance[transactionIndex].balance; 126 | creditor.memberBalance = Number(creditor.memberBalance).toFixed(2); 127 | // console.log(creditor.memberBalance) 128 | 129 | group.balance.splice(transactionIndex, 1); 130 | await group.save() 131 | 132 | return res.status(200).json({ message: 'Transaction settled' }); 133 | } catch (error) { 134 | console.error(error); 135 | return res.status(500).json({ message: 'Internal server error' }); 136 | } 137 | }); 138 | 139 | module.exports = router; 140 | -------------------------------------------------------------------------------- /server/routes/invitations.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const User = require('../models/user'); 4 | const Invitation = require('../models/invitation'); 5 | const Group = require('../models/group'); 6 | const sgMail = require('@sendgrid/mail'); 7 | const cors = require('cors'); 8 | 9 | sgMail.setApiKey(process.env.SENDGRID_API_KEY); 10 | 11 | router.use(cors()); 12 | 13 | // Function to send invitation email 14 | const sendInviteEmail = async (senderName, recipientEmail) => { 15 | const msg = { 16 | to: recipientEmail, 17 | from: 'splititmail@gmail.com', 18 | templateId: process.env.SENDGRID_TEMPLATE_ID, 19 | dynamicTemplateData: { 20 | sender_name: senderName, 21 | } 22 | }; 23 | 24 | try { 25 | await sgMail.send(msg); 26 | console.log(`Invitation email sent to ${recipientEmail}`); 27 | return true; 28 | } catch (error) { 29 | console.error(`Failed to send invitation email to ${recipientEmail}:`, error); 30 | return false; 31 | } 32 | }; 33 | 34 | // Sending invite emails 35 | router.post('/invite', async (req, res) => { 36 | try { 37 | const { senderEmail, userEmail } = req.body; 38 | const sender = await User.findOne({ email: senderEmail }) 39 | 40 | if (!sender) { 41 | return res.status(404).json({ 42 | message: 'Could not be authroized.', 43 | type: 'sender_not_found', 44 | suggestion: 'Please log in again to send invites.' 45 | }); 46 | } 47 | 48 | const emailSent = await sendInviteEmail(sender.username, userEmail); 49 | if (!emailSent) { 50 | throw new Error('Failed to send invitation email.'); 51 | } 52 | 53 | res.status(200).json({ message: 'Invitation email sent successfully.' }); 54 | } catch (error) { 55 | console.error(error); 56 | res.status(500).json({ error: 'Internal Server Error' }); 57 | } 58 | }); 59 | 60 | // Send Group Invitation 61 | router.post('/send', async (req, res) => { 62 | try { 63 | const { senderEmail, userEmail, groupId } = req.body; 64 | 65 | const group = await Group.findById(groupId); 66 | 67 | if (!group) { 68 | return res.status(404).json({ 69 | message: 'Group not found. Please select a valid group to proceed.', 70 | type: 'group_not_found', 71 | suggestion: 'Please choose a different group or create a new one.' 72 | }); 73 | } 74 | 75 | const sender = await User.findOne({ email: senderEmail }); 76 | const user = await User.findOne({ email: userEmail }).populate('invitations'); 77 | 78 | if (!sender) { 79 | return res.status(404).json({ 80 | message: 'Could not be authroized.', 81 | type: 'sender_not_found', 82 | suggestion: 'Please log in again to send invites.' 83 | }); 84 | } 85 | 86 | if (!user) { 87 | return res.status(404).json({ 88 | message: 'User not in database.', 89 | type: 'user_not_found', 90 | suggestion: 'Would you like to invite them to SplitIt?' 91 | }); 92 | } 93 | 94 | if (group.members.some((member) => member.memberId.equals(user._id))) { 95 | return res.status(409).json({ 96 | message: 'User already present in group.', 97 | type: 'user_already_present', 98 | suggestion: 'Looks like someone snuck in! Let\'s SplitIt.' 99 | }); 100 | } 101 | 102 | if (user.invitations.some(invitation => invitation.groupId.equals(groupId))) { 103 | return res.status(409).json({ 104 | message: 'User already invited to this group.', 105 | type: 'user_already_invited', 106 | suggestion: 'Looks like someone\'s quite popular or has a lot to payback.', 107 | }); 108 | } 109 | 110 | const invitation = await Invitation.create({ senderId: sender._id, recipientId: user._id, groupName: group.name, groupId }); 111 | 112 | await User.findOneAndUpdate( 113 | { _id: user._id }, 114 | { $push: { invitations: invitation._id } } 115 | ); 116 | 117 | res.status(201).json({ message: 'Invitation sent successfully', invitation }); 118 | } catch (error) { 119 | console.error(error); 120 | res.status(500).json({ error: 'Internal Server Error' }); 121 | } 122 | }); 123 | 124 | // Accept Group Invitation 125 | router.post('/accept', async (req, res) => { 126 | try { 127 | const { invitationId, userEmail } = req.body; 128 | 129 | const invitation = await Invitation.findById(invitationId); 130 | 131 | if (!invitation) { 132 | return res.status(404).json({ message: 'Invitation not found' }); 133 | } 134 | 135 | const user = await User.findOne({ email: userEmail }); 136 | 137 | if (!user) { 138 | return res.status(404).json({ message: 'User not found' }); 139 | } 140 | 141 | if (invitation.recipientId.toString() !== user._id.toString()) { 142 | console.log(invitation.recipientId, user._id) 143 | return res.status(403).json({ message: 'Invalid user for this invitation' }); 144 | } 145 | 146 | const group = await Group.findById(invitation.groupId); 147 | 148 | if (!group) { 149 | return res.status(404).json({ message: 'Group not found' }); 150 | } 151 | 152 | if (!group.members.some((member) => member.memberId.equals(user._id))) { 153 | group.members.push({ memberId: user._id, memberBalance: 0 }); 154 | user.groups.push({ groupId: group._id, groupName: group.name }); 155 | await user.save(); 156 | } else { 157 | return res.status(400).json({ message: 'User already present in group' }); 158 | } 159 | 160 | invitation.status = 'accepted'; 161 | await invitation.save(); 162 | 163 | await User.findByIdAndUpdate( 164 | { _id: invitation.recipientId }, 165 | { $pull: { invitations: invitation._id } } 166 | ); 167 | 168 | await group.save(); 169 | 170 | return res.status(200).json({ message: 'User added to the group successfully', groupId: group._id }); 171 | } catch (error) { 172 | console.error(error); 173 | return res.status(500).json({ message: 'Internal server error' }); 174 | } 175 | }); 176 | 177 | // Decline Group Invitation 178 | router.post('/decline', async (req, res) => { 179 | try { 180 | const { invitationId, userEmail } = req.body; 181 | 182 | const invitation = await Invitation.findById(invitationId); 183 | 184 | if (!invitation) { 185 | return res.status(404).json({ message: 'Invitation not found' }); 186 | } 187 | 188 | const user = await User.findOne({ email: userEmail }); 189 | 190 | if (!user) { 191 | return res.status(404).json({ message: 'User not found' }); 192 | } 193 | 194 | if (invitation.recipientId.toString() !== user._id.toString()) { 195 | return res.status(403).json({ message: 'Invalid user for this invitation' }); 196 | } 197 | 198 | invitation.status = 'declined'; 199 | await invitation.save(); 200 | 201 | await User.findByIdAndUpdate( 202 | { _id: invitation.recipientId }, 203 | { $pull: { invitations: invitation._id } } 204 | ); 205 | 206 | res.status(200).json({ message: 'Invitation declined successfully' }); 207 | } catch (error) { 208 | console.error(error); 209 | res.status(500).json({ error: 'Internal Server Error' }); 210 | } 211 | }); 212 | 213 | module.exports = router; 214 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | // Getting required modules 2 | const express = require('express'); 3 | const mongoose = require('mongoose'); 4 | const bcrypt = require('bcrypt'); 5 | const jwt = require('jsonwebtoken'); 6 | require('dotenv').config(); 7 | const cors = require('cors'); 8 | const User = require('./models/user'); 9 | const OTP = require('./models/otp'); 10 | const sgMail = require('@sendgrid/mail'); 11 | 12 | // Creating express app 13 | const app = express(); 14 | app.use(express.json()); 15 | 16 | // CORS access to angular 17 | app.use(cors()); 18 | 19 | // MongoDb database setup 20 | // mongoose.connect('mongodb://127.0.0.1:27017/SplitIt', { 21 | // useNewUrlParser: true, 22 | // useUnifiedTopology: true, 23 | // }); 24 | 25 | const uri = process.env.MONGODB_URI; 26 | 27 | mongoose.connect(uri, { 28 | useNewUrlParser: true, 29 | useUnifiedTopology: true, 30 | }); 31 | 32 | 33 | // User registration 34 | app.post('/api/register', async (req, res) => { 35 | try { 36 | const { username, email, password } = req.body; 37 | 38 | if (!username || !email || !password) { 39 | return res.status(400).json({ message: 'All fields are required' }); 40 | } 41 | 42 | const existingUser = await User.findOne({ email }); 43 | 44 | if (existingUser) { 45 | return res.status(400).json({ message: 'Email address is already in use. Please login to continue.' }); 46 | } 47 | 48 | const hashedPassword = await bcrypt.hash(password, 10); 49 | 50 | const user = new User({ username, email, password: hashedPassword }); 51 | await user.save(); 52 | 53 | const currentUser = await User.findOne({ email }); 54 | 55 | const token = jwt.sign({ userId: currentUser._id }, process.env.ACCESS_TOKEN_SECRET, { 56 | expiresIn: '1h', 57 | }); 58 | 59 | return res.status(200).json({ token: token, message: 'User registered successfully' }); 60 | } catch (error) { 61 | console.error(error); 62 | return res.status(500).json({ message: 'Internal server error' }); 63 | } 64 | }); 65 | 66 | // User login 67 | app.post('/api/login', async (req, res) => { 68 | try { 69 | const { email, password } = req.body; 70 | 71 | const user = await User.findOne({ email }); 72 | 73 | if (!user) { 74 | return res.status(404).json({ 75 | message: 'User not found. Please verify the email address.' }); 76 | } 77 | 78 | const isPasswordValid = await bcrypt.compare(password, user.password); 79 | 80 | if (!isPasswordValid) { 81 | return res.status(401).json({ 82 | type: 'incorrect_password', 83 | message: 'Incorrect password. Please try again.', 84 | suggestion: "Oops! Password slipped your mind? No biggie, happens to everyone! Just tap that reset button and let's work our magic to get you back in action" }); 85 | } 86 | 87 | const token = jwt.sign({ userId: user._id }, process.env.ACCESS_TOKEN_SECRET, { 88 | expiresIn: '1h', 89 | }); 90 | 91 | return res.status(200).json({ token: token, message: 'Successfully Logged In.' }); 92 | } catch (error) { 93 | console.error(error); 94 | return res.status(500).json({ message: 'Internal server error' }); 95 | } 96 | }); 97 | 98 | sgMail.setApiKey(process.env.SENDGRID_API_KEY); 99 | 100 | // Forgot Password 101 | app.post('/api/reset-password', async (req, res) => { 102 | try { 103 | const { email } = req.body; 104 | 105 | if (!email) { 106 | return res.status(400).json({ message: 'Please enter the email.' }); 107 | } 108 | 109 | const existingUser = await User.findOne({ email }); 110 | 111 | if (!existingUser) { 112 | return res.status(400).json({ message: 'Email not in our system. Please register to continue or check email entered is correct.' }); 113 | } 114 | 115 | const otp = Math.floor(100000 + Math.random() * 900000).toString(); 116 | 117 | const expiration = new Date(); 118 | expiration.setMinutes(expiration.getMinutes() + 10); 119 | 120 | await OTP.create({ email, otp, expiration }); 121 | const resetLink = `http://localhost:3000/reset-password?email=${email}&otp=${otp}`; 122 | const msg = { 123 | to: email, 124 | from: 'splititmail@gmail.com', 125 | subject: 'Password Reset OTP', 126 | text: `Your OTP for password reset is: ${otp}`, 127 | html: `

Your OTP for password reset is: ${otp}

128 |

Click here to reset your password with this OTP.

` 129 | }; 130 | 131 | await sgMail.send(msg); 132 | 133 | return res.status(200).json({ message: 'Reset OTP sent successfully.' }); 134 | } catch (error) { 135 | console.error(error); 136 | return res.status(500).json({ message: 'Internal server error' }); 137 | } 138 | }); 139 | 140 | // Validate OTP and reset password 141 | app.post('/api/reset-password/verify', async (req, res) => { 142 | try { 143 | const { email, otp, newPassword } = req.body; 144 | 145 | if (!email || !otp || !newPassword) { 146 | return res.status(400).json({ message: 'Please provide email, OTP, and new password.' }); 147 | } 148 | 149 | const otpData = await OTP.findOne({ email, otp, expiration: { $gt: new Date() } }); 150 | 151 | if (otpData) { 152 | const hashedPassword = await bcrypt.hash(newPassword, 10); 153 | await User.updateOne({ email }, { password: hashedPassword }); 154 | 155 | await OTP.deleteOne({ email, otp }); 156 | 157 | return res.status(200).json({ message: 'Password reset successfully.' }); 158 | } else { 159 | return res.status(400).json({ message: 'Invalid or expired OTP. Please try again.' }); 160 | } 161 | } catch (error) { 162 | console.error(error); 163 | return res.status(500).json({ message: 'Internal server error' }); 164 | } 165 | }); 166 | 167 | // Forget Password 168 | // app.post('/api/reset-password', async (req, res) => { 169 | // try { 170 | // const { email } = req.body; 171 | 172 | // if (!email) { 173 | // return res.status(400).json({ message: 'Please enter the email.' }); 174 | // } 175 | 176 | // const existingUser = await User.findOne({ email }); 177 | 178 | // if (!existingUser) { 179 | // return res.status(400).json({ message: 'Email not in our system. Please register to continue or check email entered is correct.' }); 180 | // } 181 | 182 | // const hashedPassword = await bcrypt.hash(password, 10); 183 | 184 | // const user = new User({ username, email, password: hashedPassword }); 185 | // await user.save(); 186 | 187 | // const currentUser = await User.findOne({ email }); 188 | 189 | // const token = jwt.sign({ userId: currentUser._id }, process.env.ACCESS_TOKEN_SECRET, { 190 | // expiresIn: '1h', 191 | // }); 192 | 193 | // return res.status(200).json({ token: token, message: 'Password changed successfully' }); 194 | // } catch (error) { 195 | // console.error(error); 196 | // return res.status(500).json({ message: 'Internal server error' }); 197 | // } 198 | // }); 199 | 200 | const groupsRouter = require('./routes/groups'); 201 | app.use('/api/groups', groupsRouter); 202 | 203 | const invitationsRouter = require('./routes/invitations'); 204 | app.use('/api/invitations', invitationsRouter); 205 | 206 | const usersRouter = require('./routes/users'); 207 | app.use('/api/users', usersRouter); 208 | 209 | const expensesRouter = require('./routes/expenses'); 210 | app.use('/api/expenses', expensesRouter); 211 | 212 | // Listening on port 3000 213 | const PORT = 3000; 214 | app.listen(PORT, () => { 215 | console.log(`Server is running on port ${PORT}`); 216 | }); 217 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/add-expense/add-expense.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Add Expense

3 |

Edit Expense

4 | 5 |
6 |
7 | 8 | 10 |
11 |
13 | Expense Name is required. 14 |
15 | 16 |
17 | 18 | 22 |
23 |
25 | Payer is required. 26 |
27 | 28 |
29 | 30 | 32 |
33 | 34 |
36 | Amount is required. 37 |
38 |
39 | Amount must be greater than or equal to 0. 40 |
41 |
43 | Please enter a valid amount with up to two decimal places. 44 |
45 | 46 |
47 |
48 |

Please select a split type:

49 |
50 | 55 | 61 | 67 | 73 |
74 |
75 | 76 | 77 | 78 |
79 | 80 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 97 | 98 | 99 | 102 | 103 | 104 | 105 |
106 |
107 |
108 | Select at least 1 participant other than payer. 109 |
110 |
113 | Individual participant amount must be greater than 0 and sum of participant amount must be equal to the expense 114 | amount. 115 |
116 |
119 | Individual share must be Integer and greater than 0. 120 |
121 |
124 | Individual participant percentage must be greater than 0 and sum of participant percentages must be equal to 100. 125 |
126 | 127 | 128 | 129 |
130 | 131 |
132 | 133 |
134 | 135 | 144 |
145 | 146 | 147 |
148 | 149 | 151 |
152 | 153 |
154 | 155 | 157 |
158 | 159 |
160 | 167 |
168 |
169 | 170 |
171 | 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SplitIt - Finance Management Application 2 | 3 | SplitIt is a finance management application that simplifies the process of adding bills and splitting expenses among group members. Expense management including features for adding, updating, deleting expenses, and settling balances. With advanced functionalities for splitting Equally, Unequally, by Shares and by Percentages. SplitIt emerges as an ideal tool for efficient management of shared finances within a group. 4 | 5 | ## Latest Feature: Invitation to join group 6 | 7 | ### Overview 8 | 9 | In this latest update, SplitIt introduces a new feature for sending invitations to existing users, providing them with the option to either accept or decline the invitation. 10 | 11 | ### How it Works 12 | 13 | 1. **Invitation System**: Users can send invitations to existing users to join their group. 14 | 15 | 2. **Accept or Decline Invitations**: Receiving users have the option to either accept or decline the invitation. 16 | 17 | ### Implementation in Action 18 | 19 | 1. Invitation Sent 20 | 21 | ![Invitation Sent](gitSnaps/Inivitation_Sent.png "Invitation Sent") 22 | 23 | 2. Invitation Received 24 | 25 | ![Invitation Received](gitSnaps/Inivitation_Received.png "Invitation Received") 26 | 27 | ## Features 28 | 29 | - **User Authentication**: SplitIt provides user registration and login functionalities to ensure secure access to your finance management account. 30 | 31 | - **Bill Management**: Users can easily add, edit, and delete bills. Each bill can be associated with a description, amount, and the members involved in the expense. 32 | 33 | - **Expense Splitting**: SplitIt calculates how much each member owes or is owed based on the bills and the members involved. It simplifies the process of dividing expenses evenly with a participant-based splitting mechanism for spliting exclusively among involved members, guaranteeing a tailored and accurate financial management experience without impacting others. Introducing an advanced option for expense splitting – "Unequal Splitting." This feature allows users to customize the contribution of each participant individually, providing a more personalized and flexible approach to expense sharing. Users can specify the exact amount each member should contribute to the expense, enhancing the granularity and customization of financial arrangements within the group. Additionally, features like "Shares Split" and "Percentage Split" provide alternatives to determine contribution shares and percentages instead of exact amounts, further diversifying the options for users in managing shared expenses. 34 | 35 | - **Balance Settlement**: Users can settle balances among group members, making it easy to track who owes money and who is owed. 36 | 37 | - **Validation**: SplitIt incorporates validation mechanisms to ensure that the bills and expenses are correctly input and that calculations are accurate. 38 | 39 | - **Expense Filter**: Effortlessly locate expenses by utilizing our intuitive search bar and refine results further with a selectable date range, providing a seamless and tailored filtering experience. 40 | 41 | - **Expense Pagination**: Users can take control of thier expense list with our Pagination feature, allowing to view and manage expenses at thier own pace. 42 | 43 | ## Technologies Used 44 | 45 | - **Frontend (Angular)**: The frontend of SplitIt is built using Angular, a popular web application framework. Angular provides a robust structure for building dynamic and responsive user interfaces. 46 | 47 | - **Backend (Node.js with Express)**: The backend of SplitIt is powered by Node.js, a server-side JavaScript runtime, and Express, a web application framework for Node.js. Together, they handle the server-side logic and API endpoints for the application. 48 | 49 | - **MongoDB**: SplitIt uses MongoDB, a NoSQL database, to store and manage data. MongoDB's flexibility and scalability make it a suitable choice for handling financial data. 50 | 51 | - **JWT (JSON Web Tokens)**: JWT is used for user authentication and authorization in SplitIt. It provides secure and efficient access control to the application. 52 | 53 | ## Setup Instructions 54 | 55 | ### Frontend (Angular) 56 | 57 | - Navigate to the frontend directory: 58 | ```bash 59 | cd splitIt-app 60 | - Install dependencies: 61 | ```bash 62 | npm install 63 | - Start the Angular development server: 64 | ```bash 65 | ng serve 66 | Open your browser and go to http://localhost:4200 to access the SplitIt frontend. 67 | 68 | ### Backend (Node.js with Express) 69 | 70 | - Navigate to the backend directory: 71 | ```bash 72 | cd server 73 | - Install dependencies: 74 | ```bash 75 | npm install 76 | - Add .env file and paste following content in it: 77 | ```bash 78 | ACCESS_TOKEN_SECRET='123' 79 | REFRESH_TOKEN_SECRET='123' 80 | ``` 81 | - Configure the MongoDB connection in server.js file in following code: 82 | ```bash 83 | mongoose.connect('mongodb://127.0.0.1:27017/SplitIt', { 84 | useNewUrlParser: true, 85 | useUnifiedTopology: true, 86 | }); 87 | ``` 88 | - Start server: 89 | ```bash 90 | node server.js 91 | 92 | Test the server's API by making requests to http://localhost:3000 using Postman or any other suitable endpoint testing platform. 93 | 94 | For checking out without signup user either of following details to login: 95 | 96 | Email: testuser1@gmail.com 97 | Password: testuser1 98 | 99 | or 100 | 101 | Email: testuser2@gmail.com 102 | Password: testuser2 103 | 104 | # Deployment 105 | 106 | Launched website at https://splititapp.netlify.app/ 107 | 108 | Shifted to Cyclic.sh for hosting Backend server and Netlify for Frontend. 109 | 110 | # Future Steps and Improvements 111 | 112 | SplitIt is an ongoing project, and there are several potential improvements and future steps: 113 | 114 | - ~~**Unequal Split**: Implement backend logic and frontend options to enable unequal distribution of expenses.~~ ✅ 115 | 116 | - ~~**Participants-based Split**: Implement backend logic and frontend options to select members involved in participants.~~ ✅ 117 | 118 | - ~~**Shares and Percentages Split**: Implement backend logic and frontend options to enable unequal distribution of expenses by mentioning shares or percentages of involved participants.~~ ✅ 119 | 120 | - **Notifications**: Add email or in-app notifications to keep users informed about their financial activities. 121 | 122 | - ~~**Expense Categories**: Allow users to categorize expenses for better tracking.~~ ✅ 123 | 124 | - ~~**Expense Modifications**: Allow users to modify expenses and resolve balances based on that.~~ ✅ 125 | 126 | - **Reports and Analytics**: Create visual reports and analytics for a better understanding of spending patterns. 127 | 128 | - **Logs**: Introduce activity logs to monitor all changes within the group. 129 | 130 | Please feel free to contribute to the project or provide feedback to help us make SplitIt even better! 131 | 132 | Thank you for using SplitIt! 133 | 134 | ## Snap Shots 135 | 136 | 1. Register Page 137 | 138 | ![Register](gitSnaps/Register.png "Register Page") 139 | 140 | 2. Register Validations 141 | 142 | ![Register Validations](gitSnaps/Register_Validations.png "Register Validations") 143 | 144 | 3. Register Validations Ok 145 | 146 | ![Register Validations Ok](gitSnaps/Register_Validations_Ok.png "Register Validations Ok") 147 | 148 | 4. Login Page 149 | 150 | ![Login](gitSnaps/Login.png "Login Page") 151 | 152 | 5. Login Validations 153 | 154 | ![Login Validations](gitSnaps/Login_Validations.png "Login Validations") 155 | 156 | 6. Login Validations Ok 157 | 158 | ![Login Validations Ok](gitSnaps/Login_Validations_Ok.png "Login Validations Ok") 159 | 160 | 7. Home 161 | 162 | ![Home](gitSnaps/Home.png "Home") 163 | 164 | 8. Create Group 165 | 166 | ![Create Group](gitSnaps/Create_Group.png "Create Group") 167 | 168 | 9. Group 169 | 170 | ![Group](gitSnaps/Group.png "Group") 171 | 172 | 10. Group 173 | 174 | ![Group](gitSnaps/Group_1.png "Group") 175 | 176 | 11. Member Validations 177 | 178 | ![Member Validations](gitSnaps/Member_Validations.png "Member Validations") 179 | 180 | 12. Member Added 181 | 182 | ![Member Added](gitSnaps/Member_Added.png "Member Added") 183 | 184 | 13. Expense 185 | 186 | ![Expense](gitSnaps/Expense.png "Expense") 187 | 188 | 14. Member Splits 189 | 190 | ![Member Splits](gitSnaps/Member_Splits.png "Member Splits") 191 | 192 | 15. Group Balance 193 | 194 | ![Group Balance](gitSnaps/Group_Balance.png "Group Balance") 195 | 196 | 16. Expense Multiple User 197 | 198 | ![Expense Multiple User](gitSnaps/Expense_Multiple_User.png "Expense Multiple User") 199 | 200 | 17. Group Balance Multiple User 201 | 202 | ![Group Balance Multiple User](gitSnaps/Group_Balance_Multiple_User.png "Group Balance Multiple User") 203 | 204 | 18. Settle Balance 205 | 206 | ![Settle Balance](gitSnaps/Settle_Balance.png "Settle Balance") 207 | 208 | 19. Settle Balance Confirmation 209 | 210 | ![Settle Balance Confirmation](gitSnaps/Settle_Balance_Confirmation.png "Settle Balance Confirmation") 211 | 212 | 20. Settled Balance 213 | 214 | ![Settled Balance](gitSnaps/Settled_Balance.png "Settled Balance") 215 | 216 | 21. Member Settled Balance 217 | 218 | ![Member Settled Balance](gitSnaps/Member_Settled_Balance.png "Member Settled Balance") 219 | 220 | 22. Home Group List 221 | 222 | ![Home Group List](gitSnaps/Home_Group_List.png "Home Group List") -------------------------------------------------------------------------------- /server/routes/expenses.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Expense = require('../models/expense'); 4 | const Group = require('../models/group'); 5 | const User = require('../models/user'); 6 | 7 | // Find group by ID 8 | async function findGroupById(groupId) { 9 | const group = await Group.findById(groupId); 10 | return group; 11 | } 12 | 13 | // Find user by ID 14 | async function findUserById(userId) { 15 | const user = await User.findOne({ _id: userId }); 16 | return user; 17 | } 18 | 19 | // Find expense by ID 20 | async function findExpenseById(expenseId) { 21 | const expense = await Expense.findById(expenseId); 22 | return expense; 23 | } 24 | 25 | // Create and save expense 26 | async function createExpense(expenseData) { 27 | const expense = new Expense(expenseData); 28 | await expense.save(); 29 | return expense; 30 | } 31 | 32 | // Calculate and update participant amounts 33 | async function calculateAndUpdateBalances(group, expense, operation) { 34 | const payer = group.members.find(member => member.memberId.toString() == expense.payer.toString()); 35 | const participantsList = expense.participants instanceof Map 36 | ? expense.participants 37 | : new Map(Object.entries(expense.participants)); 38 | // const rawSplitAmount = expense.amount / participantsList.size; 39 | // const splitAmount = parseFloat(rawSplitAmount.toFixed(2)); 40 | 41 | // // Update participant amounts by adding the split amount 42 | // for (const [participantId, participantAmount] of participantsList.entries()) { 43 | // participantsList.set(participantId, splitAmount); 44 | // } 45 | 46 | // Update participants balance 47 | for (const [participantId, participantAmount] of participantsList.entries()) { 48 | const member = group.members.find(member => member.memberId == participantId); 49 | 50 | if (!member) { 51 | throw new Error("Participant " + participantId + " missing in the group"); 52 | } 53 | 54 | const unroundedResult = operation === 'add' ? member.memberBalance - participantAmount : member.memberBalance + participantAmount; 55 | const roundedResult = parseFloat(unroundedResult.toFixed(2)); 56 | member.memberBalance = roundedResult; 57 | } 58 | 59 | // Update Payer balance 60 | const unroundedResult = operation === 'add' ? payer.memberBalance + expense.amount : payer.memberBalance - expense.amount; 61 | const roundedResult = parseFloat(unroundedResult.toFixed(2)); 62 | payer.memberBalance = roundedResult; 63 | expense.participants = participantsList 64 | await expense.save() 65 | return group; 66 | } 67 | 68 | // Settle debts within the group 69 | function settleGroupDebts(group, operation) { 70 | const membersDetails = JSON.parse(JSON.stringify(group.members)); 71 | const [groupBalance, debtors, creditors] = settleDebts(membersDetails); 72 | 73 | while (debtors.length > 0) { 74 | let debtor = debtors[0]; 75 | let debtorMember = group.members.find(member => member._id == debtor._id); 76 | debtorMember.memberBalance = operation === 'add' ? debtorMember.memberBalance + debtor.memberBalance : debtorMember.memberBalance - debtor.memberBalance; 77 | debtorMember.memberBalance = Number(debtorMember.memberBalance.toFixed(2)); 78 | debtors.shift(); 79 | } 80 | 81 | while (creditors.length > 0) { 82 | let creditor = creditors[0]; 83 | let creditorMember = group.members.find(member => member._id == creditor._id); 84 | creditorMember.memberBalance = operation === 'add' ? creditorMember.memberBalance - creditor.memberBalance : creditorMember.memberBalance + creditor.memberBalance; 85 | creditorMember.memberBalance = Number(creditorMember.memberBalance.toFixed(2)); 86 | creditors.shift(); 87 | } 88 | 89 | group.balance = groupBalance.map(balance => ({ ...balance })); 90 | return group; 91 | } 92 | 93 | // Settle debts logic 94 | function settleDebts(members) { 95 | const debtors = members.filter(member => member.memberBalance < 0).sort((a, b) => a.memberBalance - b.memberBalance); 96 | const creditors = members.filter(member => member.memberBalance > 0).sort((a, b) => b.memberBalance - a.memberBalance); 97 | 98 | const remainingBalance = []; 99 | 100 | while (debtors.length > 0 && creditors.length > 0) { 101 | const debtor = debtors[0]; 102 | const creditor = creditors[0]; 103 | const balance = Math.min(Math.abs(debtor.memberBalance), creditor.memberBalance); 104 | const absoluteBalance = parseFloat(balance.toFixed(2)) 105 | remainingBalance.push({ from: debtor.memberId, to: creditor.memberId, balance: absoluteBalance }); 106 | 107 | debtor.memberBalance = parseFloat((debtor.memberBalance + absoluteBalance).toFixed(2)); 108 | creditor.memberBalance = parseFloat((creditor.memberBalance - absoluteBalance).toFixed(2)); 109 | 110 | if (debtor.memberBalance === 0) debtors.shift(); 111 | if (creditor.memberBalance === 0) creditors.shift(); 112 | } 113 | 114 | return [remainingBalance, debtors, creditors]; 115 | } 116 | 117 | // Create a new expense 118 | async function handleExpenseCreation(req, res) { 119 | try { 120 | const { expenseName, payer, expenseDate, description, amount, groupId, payerName, participants, splitType, category } = req.body; 121 | 122 | // Find Group 123 | const group = await findGroupById(groupId); 124 | if (!groupId || !group) { 125 | return res.status(404).json({ message: "Group couldn't be linked" }); 126 | } 127 | 128 | // Find Payer 129 | const payerUser = await findUserById(payer); 130 | if (!payer || !payerUser) { 131 | return res.status(404).json({ message: 'Payer not found' }); 132 | } 133 | 134 | // Create and Save Expense 135 | const expenseData = { 136 | expenseName, 137 | payer, 138 | expenseDate, 139 | description, 140 | amount, 141 | groupId, 142 | payerName, 143 | participants, 144 | splitType, 145 | category 146 | }; 147 | 148 | const expense = await createExpense(expenseData); 149 | 150 | // Link Expense to Group 151 | group.expenses.push(expense._id); 152 | 153 | operation = 'add'; 154 | 155 | // Calculate and update balances 156 | calculateAndUpdateBalances(group, expense, operation); 157 | 158 | // Settle group debts 159 | settleGroupDebts(group, operation); 160 | // console.log(expense) 161 | 162 | await group.save(); 163 | 164 | return res.status(201).json(expense); 165 | } catch (error) { 166 | console.error(error); 167 | return res.status(500).json({ message: 'Internal Server Error' }); 168 | } 169 | } 170 | router.post('/', handleExpenseCreation); 171 | 172 | // Delete Expense 173 | async function handleExpenseDeletion(req, res) { 174 | try { 175 | const expenseId = req.params.expenseId; 176 | 177 | // Find Expense 178 | const expense = await findExpenseById(expenseId); 179 | if (!expense) { 180 | return res.status(404).json({ message: 'Expense not found' }); 181 | } 182 | 183 | // Find Group 184 | const group = await findGroupById(expense.groupId); 185 | if (!expense.groupId || !group) { 186 | return res.status(404).json({ message: "Expense Group couldn't be linked" }); 187 | } 188 | 189 | // Group has expense or not 190 | const groupIncludesExpense = group.expenses.includes(expenseId); 191 | if (!groupIncludesExpense) { 192 | return res.status(404).json({ message: "Expense not found in group" }); 193 | } 194 | 195 | // Find Payer 196 | const payerUser = await findUserById(expense.payer); 197 | if (!expense.payer || !payerUser) { 198 | return res.status(404).json({ message: 'Expense Payer not found' }); 199 | } 200 | 201 | const operation = 'delete'; 202 | 203 | // Calculate and update balances 204 | calculateAndUpdateBalances(group, expense, operation); 205 | 206 | // Settle group debts 207 | settleGroupDebts(group, operation); 208 | 209 | // Remove expense from group 210 | group.expenses = group.expenses.filter(expense => expense.toString() !== expenseId); 211 | 212 | await group.save(); 213 | 214 | 215 | return res.status(200).json({ message: 'Expense deleted successfully' }); 216 | } catch (error) { 217 | console.error(error); 218 | return res.status(500).json({ message: 'Internal server error' }); 219 | } 220 | }; 221 | router.delete('/:expenseId', handleExpenseDeletion); 222 | 223 | // Update Expense 224 | async function handleExpenseUpdate(req, res) { 225 | try { 226 | const expenseId = req.params.expenseId; 227 | 228 | const expense = await findExpenseById(expenseId); 229 | 230 | if (!expenseId || !expense) { 231 | return res.status(404).json({ message: 'Expense not found' }); 232 | } 233 | 234 | // Find Group 235 | const group = await findGroupById(expense.groupId); 236 | if (!expense.groupId || !group) { 237 | return res.status(404).json({ message: "Expense Group couldn't be linked" }); 238 | } 239 | 240 | // Group has expense or not 241 | const groupIncludesExpense = group.expenses.includes(expenseId); 242 | if (!groupIncludesExpense) { 243 | return res.status(404).json({ message: "Expense not found in group" }); 244 | } 245 | 246 | // Remove previous expense 247 | 248 | const oldExpense = req.body.oldExpenseData; 249 | 250 | // Find Old Expense Payer 251 | const payerUser = await findUserById(oldExpense.payer); 252 | if (!oldExpense.payer || !payerUser) { 253 | return res.status(404).json({ message: 'Previous Expense Payer not found' }); 254 | } 255 | 256 | let operation = 'delete'; 257 | 258 | // Calculate and update balances 259 | calculateAndUpdateBalances(group, expense, operation); 260 | 261 | // Settle group debts 262 | settleGroupDebts(group, operation); 263 | 264 | // Update Expense 265 | const updatedExpense = await Expense.findByIdAndUpdate( 266 | expenseId, 267 | { $set: req.body.expenseData }, 268 | { new: true } 269 | ); 270 | 271 | // Add modified expense 272 | const newExpense = req.body.expenseData; 273 | 274 | // Find New Expense Payer 275 | const newPayerUser = await findUserById(newExpense.payer); 276 | if (!newExpense.payer || !newPayerUser) { 277 | return res.status(404).json({ message: 'New Expense Payer not found' }); 278 | } 279 | 280 | operation = 'add'; 281 | 282 | // Calculate and update balances 283 | calculateAndUpdateBalances(group, updatedExpense, operation); 284 | 285 | // Settle group debts 286 | settleGroupDebts(group, operation); 287 | 288 | // Updating expense details in group 289 | const index = group.expenses.findIndex(e => e.equals(updatedExpense._id)); 290 | group.expenses[index] = updatedExpense; 291 | 292 | await group.save(); 293 | 294 | await expense.save() 295 | 296 | return res.status(200).json({ message: 'Expense updated successfully', updatedExpense: updatedExpense }); 297 | } catch (error) { 298 | console.error(error); 299 | return res.status(500).json({ message: 'Internal server error' }); 300 | } 301 | }; 302 | router.put('/:expenseId', handleExpenseUpdate) 303 | 304 | module.exports = router; 305 | -------------------------------------------------------------------------------- /splitIt-app/src/app/group/expense/add-expense/add-expense.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectorRef, HostBinding } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { Expense } from 'src/app/expense.model'; 5 | import { ExpenseService } from 'src/app/expense.service'; 6 | import { GroupService } from 'src/app/group.service'; 7 | import { catchError, finalize, of, switchMap, tap } from 'rxjs'; 8 | import { animate, state, style, transition, trigger } from '@angular/animations'; 9 | 10 | @Component({ 11 | selector: 'app-add-expense', 12 | templateUrl: './add-expense.component.html', 13 | styleUrls: ['./add-expense.component.css'], 14 | animations: [ 15 | trigger('openClose', [ 16 | state('open', style({ height: '*', display: 'block' })), 17 | state('closed', style({ height: '0', display: 'none' })), 18 | transition('closed => open', [animate('200ms ease-in')]), 19 | ]), 20 | ], 21 | }) 22 | export class AddExpenseComponent { 23 | mode: 'add' | 'edit' = 'add'; 24 | members: any[] = []; 25 | groupId!: string | null; 26 | expenses!: Expense[]; 27 | expenseForm: FormGroup; 28 | expensetoEdit: any; 29 | participants: any[] = []; 30 | participantAmounts: { [key: string]: number } = {}; 31 | participantShares: { [key: string]: number } = {}; 32 | participantPercentages: { [key: string]: number } = {}; 33 | loading = true; 34 | selectedSplitType: 'equal' | 'unequal' | 'shares' | 'percentages' = 'equal'; 35 | openClose = 'closed'; 36 | categories: { title: string, categories: string[] }[] = [ 37 | { title: 'Entertainment', categories: ['Games', 'Movies', 'Music', 'Sports'] }, 38 | { title: 'Food and drink', categories: ['Groceries', 'Dine out', 'Liquor'] }, 39 | { title: 'Home', categories: ['Rent', 'Mortgage', 'Household supplies', 'Furniture', 'Maintenance', 'Pets', 'Services', 'Electronics'] }, 40 | { title: 'Transportation', categories: ['Parking', 'Car', 'Bus/train', 'Gas/fuel', 'Taxi', 'Bicycle', 'Hotel', 'Rental Vehicle'] }, 41 | { title: 'Utilities', categories: ['Electricity', 'Heat/gas', 'Water', 'TV/Phone/Internet', 'Trash', 'Cleaning'] }, 42 | { title: 'Uncategorized', categories: ['Other'] }, 43 | ]; 44 | 45 | constructor( 46 | private fb: FormBuilder, 47 | private expenseService: ExpenseService, 48 | private groupService: GroupService, 49 | private route: ActivatedRoute, 50 | private router: Router,) { 51 | this.expenseForm = this.fb.group({ 52 | expenseName: ['', Validators.required], 53 | payer: ['', Validators.required], 54 | participants: [[]], 55 | expenseDate: [new Date().toISOString().split('T')[0]], 56 | description: [''], 57 | amount: ['', [Validators.required, Validators.min(0), this.validateDecimal]], 58 | category: ['Other'], 59 | }); 60 | } 61 | 62 | ngOnInit(): void { 63 | this.route.queryParams.pipe( 64 | switchMap(params => { 65 | this.groupId = params['groupId']; 66 | return this.fetchMembers().pipe( 67 | catchError(error => { 68 | console.error('Error fetching members:', error); 69 | return of([]); 70 | }), 71 | finalize(() => { 72 | this.participants.forEach((participant) => { 73 | this.participantAmounts[participant.id] = 0 74 | this.participantShares[participant.id] = 0 75 | this.participantPercentages[participant.id] = 0 76 | }); 77 | if (this.route.snapshot.queryParams['mode'] === 'edit' && this.route.snapshot.queryParams['expense']) { 78 | this.expensetoEdit = JSON.parse(this.route.snapshot.queryParams['expense']); 79 | this.mode = 'edit'; 80 | this.populateFormWithExpenseData(this.expensetoEdit); 81 | } 82 | this.loading = false; 83 | }) 84 | ); 85 | }) 86 | ) 87 | .subscribe(); 88 | } 89 | 90 | populateFormWithExpenseData(expense: any): void { 91 | const expenseDate = new Date(expense.expenseDate).toISOString().split('T')[0]; 92 | let participants: any = []; 93 | Object.keys(expense.participants).forEach(participantId => { 94 | let memberDetails = this.members.find(member => member.id === participantId); 95 | participants.push(memberDetails); 96 | this.participantAmounts[participantId] = expense.participants[participantId]; 97 | this.participantShares[participantId] = 0; 98 | this.participantPercentages[participantId] = parseFloat(((expense.participants[participantId] * 100) / expense.amount).toFixed(2)); 99 | }); 100 | this.expenseForm.setValue({ 101 | expenseName: expense.expenseName, 102 | payer: expense.payer, 103 | expenseDate: expenseDate, 104 | description: expense.description, 105 | amount: expense.amount, 106 | participants: participants, 107 | category: expense.category, 108 | }); 109 | this.participants = participants 110 | this.selectedSplitType = expense.splitType 111 | } 112 | 113 | fetchMembers() { 114 | if (this.groupId) { 115 | return this.groupService.getMembers(this.groupId).pipe( 116 | tap(members => { 117 | this.members = members; 118 | this.participants = members; 119 | }) 120 | ); 121 | } else { 122 | return of([]); 123 | } 124 | } 125 | 126 | canAddExpense(): boolean { 127 | const payerId = this.expenseForm.get('payer')?.value; 128 | return this.participants.length >= 1 && this.participants.some(participant => participant.id !== payerId); 129 | } 130 | 131 | onAddOrUpdateExpense() { 132 | if (this.expenseForm.valid && this.canAddExpense()) { 133 | const expenseData = { 134 | ...this.expenseForm.value, 135 | groupId: this.groupId 136 | } 137 | const payerId = expenseData.payer; 138 | const payer = this.members.find(member => member.id === payerId); 139 | let payerName = '' 140 | if (payer) { 141 | payerName = payer.name; 142 | } 143 | expenseData.payerName = payerName 144 | expenseData.amount = parseFloat(expenseData.amount.toFixed(2)); 145 | 146 | let participants: any = {} 147 | 148 | if (this.selectedSplitType === 'unequal') { 149 | this.participants.forEach((participant) => { 150 | participants[participant.id] = this.participantAmounts[participant.id] 151 | }); 152 | expenseData.splitType = 'unequal' 153 | } 154 | else if (this.selectedSplitType === 'shares') { 155 | const totalShares = Object.values(this.participantShares) 156 | .reduce((sum, share) => sum + share, 0); 157 | this.participants.forEach((participant) => { 158 | const splitAmount = parseFloat(((expenseData.amount * this.participantShares[participant.id]) / totalShares).toFixed(2)); 159 | participants[participant.id] = splitAmount 160 | }); 161 | expenseData.splitType = 'shares' 162 | } 163 | else if (this.selectedSplitType === 'percentages') { 164 | this.participants.forEach((participant) => { 165 | const splitAmount = parseFloat(((expenseData.amount * this.participantPercentages[participant.id]) / 100).toFixed(2)); 166 | participants[participant.id] = splitAmount 167 | }); 168 | expenseData.splitType = 'percentages' 169 | } 170 | else { 171 | const splitAmount = parseFloat((expenseData.amount / this.participants.length).toFixed(2)); 172 | this.participants.forEach((participant) => { 173 | participants[participant.id] = splitAmount 174 | }); 175 | expenseData.splitType = 'equal' 176 | } 177 | 178 | expenseData.participants = participants 179 | // console.log(expenseData) 180 | if (!expenseData.expenseDate) { 181 | expenseData.expenseDate = new Date().toISOString().split('T')[0] 182 | } 183 | if (this.mode === 'add') { 184 | this.onAddExpense(expenseData) 185 | } 186 | else { 187 | this.onUpdateExpense(expenseData) 188 | } 189 | } 190 | } 191 | onAddExpense(expenseData: any) { 192 | // console.log(expenseData) 193 | this.expenseService.addExpense(expenseData).subscribe( 194 | (response) => { 195 | // console.log('Expense added successfully'); 196 | this.expenseForm.reset(); 197 | this.router.navigate(['group', this.groupId, 'list-balance'], { queryParams: { groupId: this.groupId } }); 198 | 199 | }, 200 | (error) => { 201 | console.error('Error adding expense:', error); 202 | } 203 | ); 204 | 205 | } 206 | 207 | onUpdateExpense(expenseData: any) { 208 | this.expenseService.editExpense(this.expensetoEdit._id, expenseData, this.expensetoEdit).subscribe( 209 | (response) => { 210 | // console.log('Expense edited successfully'); 211 | this.expenseForm.reset(); 212 | this.router.navigate(['group', this.groupId, 'list-balance'], { queryParams: { groupId: this.groupId } }); 213 | }, 214 | (error) => { 215 | console.error('Error editing expense:', error); 216 | } 217 | ); 218 | } 219 | 220 | toggleParticipant(participantId: string): void { 221 | if (this.participants.find(participant => participant.id === participantId)) { 222 | this.participants = this.participants.filter(participant => participant.id !== participantId); 223 | delete this.participantAmounts[participantId]; 224 | delete this.participantShares[participantId]; 225 | delete this.participantPercentages[participantId]; 226 | } 227 | else { 228 | const memberDetails = this.members.find(member => member.id === participantId) 229 | this.participants.push(memberDetails) 230 | this.participantAmounts[participantId] = 0; 231 | this.participantShares[participantId] = 0; 232 | this.participantPercentages[participantId] = 0; 233 | } 234 | } 235 | 236 | isParticipantSelected(participantId: string): boolean { 237 | return this.participants.find(participant => participant.id === participantId); 238 | } 239 | 240 | updateParticipantAmount(memberId: string, event: any): void { 241 | if (this.selectedSplitType === 'percentages') { 242 | this.participantPercentages[memberId] = event.target.valueAsNumber; 243 | } 244 | else { 245 | this.participantAmounts[memberId] = event.target.valueAsNumber; 246 | } 247 | } 248 | updateParticipantAmountDecimal(memberId: string, event: any): void { 249 | if (this.selectedSplitType === 'percentages') { 250 | this.participantPercentages[memberId] = parseFloat(event.target.valueAsNumber.toFixed(2)); 251 | } 252 | else { 253 | this.participantAmounts[memberId] = parseFloat(event.target.valueAsNumber.toFixed(2)); 254 | } 255 | 256 | } 257 | updateParticipantShares(memberId: string, event: any): void { 258 | this.participantShares[memberId] = event.target.valueAsNumber; 259 | // console.log(this.participantShares[memberId], typeof(this.participantShares[memberId])) 260 | } 261 | 262 | toggleSplitType(splitType: 'equal' | 'unequal' | 'shares' | 'percentages') { 263 | this.selectedSplitType = splitType; 264 | } 265 | 266 | isTotalAmountValid(): boolean { 267 | if (this.selectedSplitType === 'unequal') { 268 | const isAmountValid = Object.keys(this.participantAmounts).every(participantId => { 269 | const amount = this.participantAmounts[participantId]; 270 | // console.log(typeof(amount), this.participantAmounts) 271 | return amount > 0; 272 | }); 273 | 274 | const totalParticipantAmount = Object.values(this.participantAmounts) 275 | .reduce((sum, amount) => sum + amount, 0); 276 | 277 | return isAmountValid && totalParticipantAmount.toFixed(2) === this.expenseForm.value.amount.toFixed(2); 278 | } 279 | 280 | else if (this.selectedSplitType === 'shares') { 281 | const isShareValid = Object.keys(this.participantShares).every(participantId => { 282 | const share = this.participantShares[participantId]; 283 | // console.log(typeof(share), this.participantShares, Number.isInteger(share) && share > 0) 284 | return Number.isInteger(share) && share > 0; 285 | }); 286 | 287 | return isShareValid; 288 | } 289 | 290 | else if (this.selectedSplitType === 'percentages') { 291 | const isPercentageValid = Object.keys(this.participantPercentages).every(participantId => { 292 | const percentage = this.participantPercentages[participantId]; 293 | return percentage > 0; 294 | }); 295 | 296 | const totalParticipantPercentage = Object.values(this.participantPercentages) 297 | .reduce((sum, percentage) => sum + percentage, 0); 298 | return isPercentageValid && totalParticipantPercentage == 100; 299 | } 300 | 301 | return true; 302 | } 303 | 304 | validateDecimal(control: any): { [key: string]: any } | null { 305 | const value = control.value; 306 | if (value !== null && value !== undefined) { 307 | const decimalRegex = /^\d+(\.\d{1,2})?$/; 308 | if (!decimalRegex.test(value)) { 309 | return { 'invalidDecimal': true }; 310 | } 311 | } 312 | return null; 313 | } 314 | 315 | toggleCalculator() { 316 | this.openClose = this.openClose === 'open' ? 'closed' : 'open'; 317 | } 318 | } 319 | --------------------------------------------------------------------------------