Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20,049 changes: 12,521 additions & 7,528 deletions client/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,23 @@
"@testing-library/user-event": "^12.8.3",
"apexcharts": "^3.28.1",
"axios": "^0.21.1",
"chart.js": "^4.5.0",
"date-fns": "^2.0.0-beta.5",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"react": "^17.0.2",
"react-apexcharts": "^1.3.9",
"react-chartjs-2": "^5.3.0",
"react-dom": "^17.0.2",
"react-dropzone": "^11.3.4",
"react-multiple-select-dropdown-lite": "^2.0.4",
"react-nice-dates": "^3.1.0",
"react-progress-button": "^5.1.0",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-scripts": "^4.0.3",
"react-simple-snackbar": "^1.1.11",
"react-tiny-fab": "^4.0.3",
"recharts": "^2.0.9",
Expand Down
41 changes: 21 additions & 20 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,28 @@ function App() {
const user = JSON.parse(localStorage.getItem('profile'))

return (
<div>
<div className="app-container">
<BrowserRouter>
<SnackbarProvider>
{user && <NavBar />}
<Header />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/invoice" exact component={Invoice} />
<Route path="/edit/invoice/:id" exact component={Invoice} />
<Route path="/invoice/:id" exact component={InvoiceDetails} />
<Route path="/invoices" exact component={Invoices} />
<Route path="/login" exact component={Login} />
<Route path="/settings" exact component={Settings} />
<Route path="/dashboard" exact component={Dashboard} />
<Route path="/customers" exact component={ClientList} />
<Route path="/forgot" exact component={Forgot} />
<Route path="/reset/:token" exact component={Reset} />
<Redirect exact from="/new-invoice" to="/invoice" />

</Switch>
<Footer />
<SnackbarProvider>
{user && <NavBar />}
<div className="main-content">
<Header />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/invoice" exact component={Invoice} />
<Route path="/edit/invoice/:id" exact component={Invoice} />
<Route path="/invoice/:id" exact component={InvoiceDetails} />
<Route path="/invoices" exact component={Invoices} />
<Route path="/login" exact component={Login} />
<Route path="/settings" exact component={Settings} />
<Route path="/dashboard" exact component={Dashboard} />
<Route path="/customers" exact component={ClientList} />
<Route path="/forgot" exact component={Forgot} />
<Route path="/reset/:token" exact component={Reset} />
<Redirect exact from="/new-invoice" to="/invoice" />
</Switch>
<Footer />
</div>
</SnackbarProvider>
</BrowserRouter>
</div>
Expand Down
80 changes: 55 additions & 25 deletions client/src/actions/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,71 @@ import * as api from '../api/index'
import { AUTH, CREATE_PROFILE } from './constants'


export const signin =(formData, openSnackbar, setLoading) => async(dispatch) => {

export const signin = (formData, openSnackbar, setLoading) => async (dispatch) => {
try {
//login the user
const { data } = await api.signIn(formData)
const { data } = await api.signIn(formData);

if (!data?.result) {
openSnackbar("Login failed. Please check your credentials.");
return { success: false };
}

dispatch({ type: AUTH, data})
// setLoading(false)
openSnackbar("Signin successfull")
// history.push('/dashboard')
window.location.href="/dashboard"
dispatch({ type: AUTH, data });
localStorage.setItem('profile', JSON.stringify({ result: data.result, token: data.token }));

openSnackbar("Sign in successful!");
return { success: true };

} catch (error) {
// console.log(error?.response?.data?.message)
openSnackbar(error?.response?.data?.message)
setLoading(false)
const message = error?.response?.data?.message || "Sign in failed. Please try again.";
openSnackbar(message);
console.error("Signin error:", error);
return { success: false, error: message };
}
}

export const signup =(formData, openSnackbar, setLoading) => async(dispatch) => {

export const signup = (formData, openSnackbar, setLoading) => async (dispatch) => {
try {
//Sign up the user
const { data } = await api.signUp(formData)
dispatch({ type: AUTH, data})
const { info } = await api.createProfile({name: data?.result?.name, email: data?.result?.email, userId: data?.result?._id, phoneNumber: '', businessName: '', contactAddress: '', logo: '', website: ''});
dispatch({ type: CREATE_PROFILE, payload: info });
window.location.href="/dashboard"
// history.push('/dashboard')
openSnackbar("Sign up successfull")
// Sign up the user
const { data } = await api.signUp(formData);

if (!data?.result) {
openSnackbar("Registration failed. Please try again.");
return { success: false };
}

// Create the user's profile
const profileData = {
name: data.result.name,
email: data.result.email,
userId: data.result._id,
phoneNumber: '',
businessName: '',
contactAddress: '',
logo: '',
website: ''
};

try {
const { info } = await api.createProfile(profileData);
dispatch({ type: CREATE_PROFILE, payload: info });
} catch (profileError) {
console.log("Profile creation error:", profileError);
// Continue even if profile creation fails
}

// Dispatch auth data and store in localStorage
dispatch({ type: AUTH, data });
localStorage.setItem('profile', JSON.stringify({ result: data.result, token: data.token }));

openSnackbar("Sign up successful!");
return { success: true };

} catch (error) {
console.log(error)
openSnackbar(error?.response?.data?.message)
setLoading(false)
const message = error?.response?.data?.message || "Sign up failed. Please try again.";
openSnackbar(message);
console.error("Signup error:", error);
return { success: false, error: message };
}
}

Expand Down
150 changes: 112 additions & 38 deletions client/src/components/Dashboard/Dashboard.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { toCommas } from '../../utils/utils'
import styles from './Dashboard.module.css'
import { useHistory, useLocation } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { getInvoicesByUser } from '../../actions/invoiceActions'
import Empty from '../svgIcons/Empty'
import Chart from './Chart'
// import Donut from './Donut'
import Donut from './Donut'
import moment from 'moment'
import { Check, Pie, Bag, Card, Clock, Frown } from './Icons'
import Spinner from '../Spinner/Spinner'
Expand Down Expand Up @@ -86,10 +86,41 @@ const Dashboard = () => {
}


const logout = () => {
dispatch({ type: 'LOGOUT' });
history.push('/login');
};

return (
<div className={styles.pageContainer}>


<div className={styles.topBar}>
<div className={styles.userMenu}>
<div className={styles.userInfo}>
<div className={styles.userAvatar}>
{user?.result?.name?.charAt(0)}
</div>
<span className={styles.userName}>{user?.result?.name}</span>
<div className={styles.userDropdown}>
<button onClick={() => history.push('/settings')} className={styles.userMenuItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34"></path>
<polygon points="18 2 22 6 12 16 8 16 8 12 18 2"></polygon>
</svg>
Edit Profile
</button>
<button onClick={logout} className={styles.userMenuItem}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
Logout
</button>
</div>
</div>
</div>
</div>

<section className={styles.stat}>
<ul className={styles.autoGrid}>
<li className={styles.listItem} style={{backgroundColor: '#1976d2', color: 'white'}}>
Expand Down Expand Up @@ -178,46 +209,89 @@ const Dashboard = () => {

</section>

{paymentHistory.length !== 0 && (
<section>
<Chart paymentHistory={paymentHistory} />
<div className={styles.chartsContainer}>
<section className={styles.chartSection}>
{paymentHistory.length !== 0 && <Chart paymentHistory={paymentHistory} />}
</section>
<section className={styles.donutSection}>
<Donut paid={paid.length} unpaid={unpaidInvoice.length} partial={partial.length} />
</section>
</div>

<section className={styles.quickActions}>
<h2>Quick Actions</h2>
<div className={styles.actionButtons}>
<button onClick={() => history.push('/invoice')} className={styles.actionButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="12" y1="18" x2="12" y2="12"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
Create Invoice
</button>
<button onClick={() => history.push('/customers')} className={styles.actionButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
Add Customer
</button>
<button onClick={() => history.push('/settings')} className={styles.actionButton}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Settings
</button>
</div>
</section>
)}

<section>
<h1 style={{textAlign: 'center', padding: '30px' }}>{paymentHistory.length ? 'Recent Payments' : 'No payment received yet'}</h1>
<div>
<div className={styles.table}>

<table>
<tbody>

<section className={styles.recentPaymentsSection}>
<h2>{paymentHistory.length ? 'Recent Payments' : 'No payment received yet'}</h2>
<div className={styles.table}>
<table>
<thead>
{paymentHistory.length !== 0 && (
<tr>
<th style={{padding: '15px'}}></th>
<th style={{padding: '15px'}}>Paid By</th>
<th style={{padding: '15px'}}>Date Paid</th>
<th style={{padding: '15px'}}>Amount Paid</th>
<th style={{padding: '15px'}}>Payment Method</th>
<th style={{padding: '15px'}}>Note</th>
</tr>
<th></th>
<th>Paid By</th>
<th>Date Paid</th>
<th>Amount Paid</th>
<th>Payment Method</th>
<th>Note</th>
</tr>
)}

</thead>
<tbody>
{sortHistoryByDate.slice(-10).map((record) => (
<tr className={styles.tableRow} key={record._id}>
<td><button>{record?.paidBy?.charAt(0)}</button></td>
<td>{record.paidBy}</td>
<td>{moment(record.datePaid).format('MMMM Do YYYY')}</td>
<td><h3 style={{color: '#00A86B', fontSize: '14px'}} >{toCommas(record.amountPaid)}</h3></td>
<td>{record.paymentMethod}</td>
<td>{record.note}</td>
</tr>

<tr className={styles.tableRow} key={record._id}>
<td>
<div className={styles.avatar}>
{record?.paidBy?.charAt(0)}
</div>
</td>
<td>{record.paidBy}</td>
<td>{moment(record.datePaid).format('MMM D, YYYY')}</td>
<td>
<span className={styles.amount}>
{toCommas(record.amountPaid)}
</span>
</td>
<td>
<span className={`${styles.paymentMethod} ${styles[record.paymentMethod.toLowerCase()]}`}>
{record.paymentMethod}
</span>
</td>
<td>{record.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
</tbody>
</table>
</div>
</section>

</div>
)
Expand Down
Loading