Update formatting and improve consistency across configuration and documentation files
- Adjusted formatting in .prettierrc for consistent newline handling. - Enhanced API documentation in BACKEND_PROMPT.md for better readability and structure. - Updated docker-compose.yml to standardize quotes and improve health check commands. - Refactored ESLint configuration for better readability and consistency. - Made minor formatting adjustments in various frontend components for improved user experience and code clarity.
This commit is contained in:
@@ -253,7 +253,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | -------------------- | ---------------------------------- |
|
||||||
| POST | `/api/auth/register` | Register new user |
|
| POST | `/api/auth/register` | Register new user |
|
||||||
| POST | `/api/auth/login` | Login, returns JWT + refresh token |
|
| POST | `/api/auth/login` | Login, returns JWT + refresh token |
|
||||||
| POST | `/api/auth/refresh` | Refresh access token |
|
| POST | `/api/auth/refresh` | Refresh access token |
|
||||||
@@ -263,7 +263,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Assets
|
### Assets
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ----------------- | ------------------------ |
|
||||||
| GET | `/api/assets` | List all assets for user |
|
| GET | `/api/assets` | List all assets for user |
|
||||||
| POST | `/api/assets` | Create new asset |
|
| POST | `/api/assets` | Create new asset |
|
||||||
| GET | `/api/assets/:id` | Get asset by ID |
|
| GET | `/api/assets/:id` | Get asset by ID |
|
||||||
@@ -273,7 +273,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Liabilities
|
### Liabilities
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ---------------------- | ----------------------------- |
|
||||||
| GET | `/api/liabilities` | List all liabilities for user |
|
| GET | `/api/liabilities` | List all liabilities for user |
|
||||||
| POST | `/api/liabilities` | Create new liability |
|
| POST | `/api/liabilities` | Create new liability |
|
||||||
| GET | `/api/liabilities/:id` | Get liability by ID |
|
| GET | `/api/liabilities/:id` | Get liability by ID |
|
||||||
@@ -283,7 +283,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Net Worth Snapshots
|
### Net Worth Snapshots
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | -------------------------- | ---------------------------------------- |
|
||||||
| GET | `/api/net-worth/snapshots` | List snapshots (with date range filter) |
|
| GET | `/api/net-worth/snapshots` | List snapshots (with date range filter) |
|
||||||
| POST | `/api/net-worth/snapshots` | Create snapshot (auto-calculates totals) |
|
| POST | `/api/net-worth/snapshots` | Create snapshot (auto-calculates totals) |
|
||||||
| GET | `/api/net-worth/current` | Get current net worth calculation |
|
| GET | `/api/net-worth/current` | Get current net worth calculation |
|
||||||
@@ -291,7 +291,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Debt Categories
|
### Debt Categories
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | --------------------------- | ------------------------------------------- |
|
||||||
| GET | `/api/debts/categories` | List all categories |
|
| GET | `/api/debts/categories` | List all categories |
|
||||||
| POST | `/api/debts/categories` | Create category |
|
| POST | `/api/debts/categories` | Create category |
|
||||||
| PUT | `/api/debts/categories/:id` | Update category |
|
| PUT | `/api/debts/categories/:id` | Update category |
|
||||||
@@ -300,7 +300,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Debt Accounts
|
### Debt Accounts
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ------------------------- | -------------------------------- |
|
||||||
| GET | `/api/debts/accounts` | List all debt accounts |
|
| GET | `/api/debts/accounts` | List all debt accounts |
|
||||||
| POST | `/api/debts/accounts` | Create debt account |
|
| POST | `/api/debts/accounts` | Create debt account |
|
||||||
| GET | `/api/debts/accounts/:id` | Get account with payment history |
|
| GET | `/api/debts/accounts/:id` | Get account with payment history |
|
||||||
@@ -310,7 +310,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Debt Payments
|
### Debt Payments
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ---------------------------------- | --------------------------------- |
|
||||||
| GET | `/api/debts/accounts/:id/payments` | List payments for account |
|
| GET | `/api/debts/accounts/:id/payments` | List payments for account |
|
||||||
| POST | `/api/debts/accounts/:id/payments` | Record payment (updates balance) |
|
| POST | `/api/debts/accounts/:id/payments` | Record payment (updates balance) |
|
||||||
| DELETE | `/api/debts/payments/:id` | Delete payment (restores balance) |
|
| DELETE | `/api/debts/payments/:id` | Delete payment (restores balance) |
|
||||||
@@ -318,7 +318,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Clients
|
### Clients
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ------------------ | ----------------------------- |
|
||||||
| GET | `/api/clients` | List all clients |
|
| GET | `/api/clients` | List all clients |
|
||||||
| POST | `/api/clients` | Create client |
|
| POST | `/api/clients` | Create client |
|
||||||
| GET | `/api/clients/:id` | Get client with invoice stats |
|
| GET | `/api/clients/:id` | Get client with invoice stats |
|
||||||
@@ -328,7 +328,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Invoices
|
### Invoices
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | -------------------------- | -------------------------------------------- |
|
||||||
| GET | `/api/invoices` | List invoices (filterable by status, client) |
|
| GET | `/api/invoices` | List invoices (filterable by status, client) |
|
||||||
| POST | `/api/invoices` | Create invoice with line items |
|
| POST | `/api/invoices` | Create invoice with line items |
|
||||||
| GET | `/api/invoices/:id` | Get invoice with line items |
|
| GET | `/api/invoices/:id` | Get invoice with line items |
|
||||||
@@ -339,7 +339,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Income Sources
|
### Income Sources
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | -------------------------- | -------------------- |
|
||||||
| GET | `/api/cashflow/income` | List income sources |
|
| GET | `/api/cashflow/income` | List income sources |
|
||||||
| POST | `/api/cashflow/income` | Create income source |
|
| POST | `/api/cashflow/income` | Create income source |
|
||||||
| PUT | `/api/cashflow/income/:id` | Update income source |
|
| PUT | `/api/cashflow/income/:id` | Update income source |
|
||||||
@@ -348,7 +348,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Expenses
|
### Expenses
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ---------------------------- | -------------- |
|
||||||
| GET | `/api/cashflow/expenses` | List expenses |
|
| GET | `/api/cashflow/expenses` | List expenses |
|
||||||
| POST | `/api/cashflow/expenses` | Create expense |
|
| POST | `/api/cashflow/expenses` | Create expense |
|
||||||
| PUT | `/api/cashflow/expenses/:id` | Update expense |
|
| PUT | `/api/cashflow/expenses/:id` | Update expense |
|
||||||
@@ -357,7 +357,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Transactions
|
### Transactions
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | -------------------------------- | ----------------------------------------------- |
|
||||||
| GET | `/api/cashflow/transactions` | List transactions (with date range, pagination) |
|
| GET | `/api/cashflow/transactions` | List transactions (with date range, pagination) |
|
||||||
| POST | `/api/cashflow/transactions` | Create transaction |
|
| POST | `/api/cashflow/transactions` | Create transaction |
|
||||||
| DELETE | `/api/cashflow/transactions/:id` | Delete transaction |
|
| DELETE | `/api/cashflow/transactions/:id` | Delete transaction |
|
||||||
@@ -365,13 +365,13 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
### Dashboard / Summary
|
### Dashboard / Summary
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ------------------------ | ---------------------------- |
|
||||||
| GET | `/api/dashboard/summary` | Get aggregated summary stats |
|
| GET | `/api/dashboard/summary` | Get aggregated summary stats |
|
||||||
|
|
||||||
### Health Check
|
### Health Check
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ------------- | ------------------------------- |
|
||||||
| GET | `/api/health` | Health check (no auth required) |
|
| GET | `/api/health` | Health check (no auth required) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -390,6 +390,7 @@ All API routes are prefixed with `/api` to avoid conflicts with frontend routes.
|
|||||||
- Store refresh token hash in DB or Redis
|
- Store refresh token hash in DB or Redis
|
||||||
|
|
||||||
3. **JWT Payload:**
|
3. **JWT Payload:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface JWTPayload {
|
interface JWTPayload {
|
||||||
sub: string; // user ID
|
sub: string; // user ID
|
||||||
@@ -521,7 +522,7 @@ import path from 'path';
|
|||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
app.register(fastifyStatic, {
|
app.register(fastifyStatic, {
|
||||||
root: path.join(__dirname, '../public'),
|
root: path.join(__dirname, '../public'),
|
||||||
prefix: '/',
|
prefix: '/'
|
||||||
});
|
});
|
||||||
|
|
||||||
// SPA fallback - serve index.html for all non-API routes
|
// SPA fallback - serve index.html for all non-API routes
|
||||||
@@ -641,14 +642,14 @@ services:
|
|||||||
POSTGRES_PASSWORD: wealth_dev
|
POSTGRES_PASSWORD: wealth_dev
|
||||||
POSTGRES_DB: wealth
|
POSTGRES_DB: wealth
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- '5432:5432'
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
api:
|
api:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- '3000:3000'
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql://wealth:wealth_dev@db:5432/wealth
|
DATABASE_URL: postgresql://wealth:wealth_dev@db:5432/wealth
|
||||||
JWT_SECRET: dev-secret-change-in-production
|
JWT_SECRET: dev-secret-change-in-production
|
||||||
@@ -807,4 +808,3 @@ interface Transaction {
|
|||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class DatabaseConnection {
|
|||||||
public static getInstance(): PrismaClient {
|
public static getInstance(): PrismaClient {
|
||||||
if (!DatabaseConnection.instance) {
|
if (!DatabaseConnection.instance) {
|
||||||
DatabaseConnection.instance = new PrismaClient({
|
DatabaseConnection.instance = new PrismaClient({
|
||||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,12 +94,8 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const totalInvoices = invoices.length;
|
const totalInvoices = invoices.length;
|
||||||
const paidInvoices = invoices.filter(inv => inv.status === 'paid').length;
|
const paidInvoices = invoices.filter(inv => inv.status === 'paid').length;
|
||||||
const outstandingAmount = invoices
|
const outstandingAmount = invoices.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled').reduce((sum, inv) => sum + inv.total, 0);
|
||||||
.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled')
|
const overdueInvoices = invoices.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled' && inv.dueDate < now).length;
|
||||||
.reduce((sum, inv) => sum + inv.total, 0);
|
|
||||||
const overdueInvoices = invoices.filter(
|
|
||||||
inv => inv.status !== 'paid' && inv.status !== 'cancelled' && inv.dueDate < now
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return {totalInvoices, paidInvoices, outstandingAmount, overdueInvoices};
|
return {totalInvoices, paidInvoices, outstandingAmount, overdueInvoices};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ services:
|
|||||||
POSTGRES_PASSWORD: wealth_dev
|
POSTGRES_PASSWORD: wealth_dev
|
||||||
POSTGRES_DB: wealth_dev
|
POSTGRES_DB: wealth_dev
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- '5432:5432'
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_dev_data:/var/lib/postgresql/data
|
- postgres_dev_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U wealth -d wealth_dev"]
|
test: ['CMD-SHELL', 'pg_isready -U wealth -d wealth_dev']
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -28,11 +28,11 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${STAGING_DB_PASSWORD:-wealth_staging}
|
POSTGRES_PASSWORD: ${STAGING_DB_PASSWORD:-wealth_staging}
|
||||||
POSTGRES_DB: wealth_staging
|
POSTGRES_DB: wealth_staging
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- '5433:5432'
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_staging_data:/var/lib/postgresql/data
|
- postgres_staging_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U wealth -d wealth_staging"]
|
test: ['CMD-SHELL', 'pg_isready -U wealth -d wealth_staging']
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -46,11 +46,11 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${PROD_DB_PASSWORD:?PROD_DB_PASSWORD is required}
|
POSTGRES_PASSWORD: ${PROD_DB_PASSWORD:?PROD_DB_PASSWORD is required}
|
||||||
POSTGRES_DB: wealth_prod
|
POSTGRES_DB: wealth_prod
|
||||||
ports:
|
ports:
|
||||||
- "5434:5432"
|
- '5434:5432'
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_prod_data:/var/lib/postgresql/data
|
- postgres_prod_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U wealth -d wealth_prod"]
|
test: ['CMD-SHELL', 'pg_isready -U wealth -d wealth_prod']
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -63,4 +63,3 @@ volumes:
|
|||||||
postgres_dev_data:
|
postgres_dev_data:
|
||||||
postgres_staging_data:
|
postgres_staging_data:
|
||||||
postgres_prod_data:
|
postgres_prod_data:
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,7 @@ export default defineConfig([
|
|||||||
// Frontend Web - TypeScript/React files
|
// Frontend Web - TypeScript/React files
|
||||||
{
|
{
|
||||||
files: ['frontend-web/**/*.{ts,tsx}'],
|
files: ['frontend-web/**/*.{ts,tsx}'],
|
||||||
extends: [
|
extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite],
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
reactHooks.configs.flat.recommended,
|
|
||||||
reactRefresh.configs.vite
|
|
||||||
],
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser
|
globals: globals.browser
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ export default function Layout() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="flex h-9 w-full items-center gap-3 rounded-lg px-2.5 text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors"
|
className="flex h-9 w-full items-center gap-3 rounded-lg px-2.5 text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors">
|
||||||
>
|
|
||||||
<LogOut className="h-[18px] w-[18px] shrink-0" />
|
<LogOut className="h-[18px] w-[18px] shrink-0" />
|
||||||
<span className="text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">Log out</span>
|
<span className="text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">Log out</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,4 +10,3 @@ export default function ProtectedRoute() {
|
|||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function AddAssetDialog({open, onOpenChange}: Props) {
|
|||||||
createAsset({
|
createAsset({
|
||||||
name: sanitizeString(form.name),
|
name: sanitizeString(form.name),
|
||||||
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
|
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
|
||||||
value: valueNum,
|
value: valueNum
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function AddLiabilityDialog({open, onOpenChange}: Props) {
|
|||||||
createLiability({
|
createLiability({
|
||||||
name: form.name,
|
name: form.name,
|
||||||
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
|
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
|
||||||
currentBalance: parseFloat(form.balance) || 0,
|
currentBalance: parseFloat(form.balance) || 0
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
|
|||||||
data: {
|
data: {
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
|
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
|
||||||
value: valueNum,
|
value: valueNum
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function EditLiabilityDialog({open, onOpenChange, liability}: Pro
|
|||||||
data: {
|
data: {
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
|
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
|
||||||
currentBalance: balanceNum,
|
currentBalance: balanceNum
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: ''
|
||||||
});
|
});
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -36,7 +36,7 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
|
|||||||
await dispatch(
|
await dispatch(
|
||||||
loginUser({
|
loginUser({
|
||||||
email: form.email,
|
email: form.email,
|
||||||
password: form.password,
|
password: form.password
|
||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
|||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: ''
|
||||||
});
|
});
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -45,7 +45,7 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
|||||||
registerUser({
|
registerUser({
|
||||||
email: form.email,
|
email: form.email,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
name: form.name,
|
name: form.name
|
||||||
})
|
})
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
@@ -117,9 +117,7 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">By signing up, you agree to our Terms of Service and Privacy Policy.</p>
|
||||||
By signing up, you agree to our Terms of Service and Privacy Policy.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="flex-col gap-2 sm:flex-col">
|
<DialogFooter className="flex-col gap-2 sm:flex-col">
|
||||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||||
@@ -134,4 +132,3 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,5 +69,5 @@ export const authService = {
|
|||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const incomeService = {
|
|||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
return apiClient.delete<void>(`/cashflow/income/${id}`);
|
return apiClient.delete<void>(`/cashflow/income/${id}`);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expenseService = {
|
export const expenseService = {
|
||||||
@@ -70,7 +70,7 @@ export const expenseService = {
|
|||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
return apiClient.delete<void>(`/cashflow/expenses/${id}`);
|
return apiClient.delete<void>(`/cashflow/expenses/${id}`);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transactionService = {
|
export const transactionService = {
|
||||||
@@ -84,5 +84,5 @@ export const transactionService = {
|
|||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
return apiClient.delete<void>(`/cashflow/transactions/${id}`);
|
return apiClient.delete<void>(`/cashflow/transactions/${id}`);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ApiClient {
|
|||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(options.headers as Record<string, string>),
|
...(options.headers as Record<string, string>)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -38,7 +38,7 @@ class ApiClient {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers,
|
headers
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle non-JSON responses
|
// Handle non-JSON responses
|
||||||
@@ -48,7 +48,7 @@ class ApiClient {
|
|||||||
throw {
|
throw {
|
||||||
message: 'Request failed',
|
message: 'Request failed',
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
error: response.statusText,
|
error: response.statusText
|
||||||
} as ApiError;
|
} as ApiError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ class ApiClient {
|
|||||||
throw {
|
throw {
|
||||||
message: data.message || 'Request failed',
|
message: data.message || 'Request failed',
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
error: data.error,
|
error: data.error
|
||||||
} as ApiError;
|
} as ApiError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class ApiClient {
|
|||||||
throw {
|
throw {
|
||||||
message: 'Network error',
|
message: 'Network error',
|
||||||
statusCode: 0,
|
statusCode: 0,
|
||||||
error: String(error),
|
error: String(error)
|
||||||
} as ApiError;
|
} as ApiError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,21 +85,21 @@ class ApiClient {
|
|||||||
async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||||
return this.request<T>(endpoint, {
|
return this.request<T>(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data ? JSON.stringify(data) : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async put<T>(endpoint: string, data?: unknown): Promise<T> {
|
async put<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||||
return this.request<T>(endpoint, {
|
return this.request<T>(endpoint, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data ? JSON.stringify(data) : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
|
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||||
return this.request<T>(endpoint, {
|
return this.request<T>(endpoint, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data ? JSON.stringify(data) : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const assetService = {
|
|||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
return apiClient.delete<void>(`/assets/${id}`);
|
return apiClient.delete<void>(`/assets/${id}`);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const liabilityService = {
|
export const liabilityService = {
|
||||||
@@ -112,7 +112,7 @@ export const liabilityService = {
|
|||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
return apiClient.delete<void>(`/liabilities/${id}`);
|
return apiClient.delete<void>(`/liabilities/${id}`);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const snapshotService = {
|
export const snapshotService = {
|
||||||
@@ -124,13 +124,7 @@ export const snapshotService = {
|
|||||||
return apiClient.get<{snapshot: NetWorthSnapshot}>(`/networth/snapshots/${id}`);
|
return apiClient.get<{snapshot: NetWorthSnapshot}>(`/networth/snapshots/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async create(data: {
|
async create(data: {date: string; totalAssets: number; totalLiabilities: number; netWorth: number; notes?: string}): Promise<{snapshot: NetWorthSnapshot}> {
|
||||||
date: string;
|
|
||||||
totalAssets: number;
|
|
||||||
totalLiabilities: number;
|
|
||||||
netWorth: number;
|
|
||||||
notes?: string;
|
|
||||||
}): Promise<{snapshot: NetWorthSnapshot}> {
|
|
||||||
return apiClient.post<{snapshot: NetWorthSnapshot}>('/networth/snapshots', data);
|
return apiClient.post<{snapshot: NetWorthSnapshot}>('/networth/snapshots', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -140,5 +134,5 @@ export const snapshotService = {
|
|||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
return apiClient.delete<void>(`/networth/snapshots/${id}`);
|
return apiClient.delete<void>(`/networth/snapshots/${id}`);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,5 +33,5 @@ export const tokenStorage = {
|
|||||||
clear(): void {
|
clear(): void {
|
||||||
this.removeToken();
|
this.removeToken();
|
||||||
this.removeUser();
|
this.removeUser();
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,33 +9,33 @@ const features = [
|
|||||||
{
|
{
|
||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
title: 'Net Worth Tracking',
|
title: 'Net Worth Tracking',
|
||||||
description: 'Monitor your assets and liabilities over time with beautiful charts and insights.',
|
description: 'Monitor your assets and liabilities over time with beautiful charts and insights.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
title: 'Debt Management',
|
title: 'Debt Management',
|
||||||
description: 'Organize and track debt paydown across multiple accounts and categories.',
|
description: 'Organize and track debt paydown across multiple accounts and categories.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: ArrowLeftRight,
|
icon: ArrowLeftRight,
|
||||||
title: 'Cashflow Analysis',
|
title: 'Cashflow Analysis',
|
||||||
description: 'Understand your income and expenses to optimize your savings rate.',
|
description: 'Understand your income and expenses to optimize your savings rate.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: FileText,
|
icon: FileText,
|
||||||
title: 'Invoicing',
|
title: 'Invoicing',
|
||||||
description: 'Create professional invoices and track payments from your clients.',
|
description: 'Create professional invoices and track payments from your clients.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
title: 'Visual Reports',
|
title: 'Visual Reports',
|
||||||
description: 'Clean, minimal dashboards that put your data front and center.',
|
description: 'Clean, minimal dashboards that put your data front and center.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
title: 'Private & Secure',
|
title: 'Private & Secure',
|
||||||
description: 'Your financial data stays on your device. No cloud sync required.',
|
description: 'Your financial data stays on your device. No cloud sync required.'
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
@@ -64,9 +64,7 @@ export default function LandingPage() {
|
|||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<section className="py-20 px-6">
|
<section className="py-20 px-6">
|
||||||
<div className="max-w-3xl mx-auto text-center">
|
<div className="max-w-3xl mx-auto text-center">
|
||||||
<h1 className="text-4xl font-semibold tracking-tight mb-4">
|
<h1 className="text-4xl font-semibold tracking-tight mb-4">Take control of your finances</h1>
|
||||||
Take control of your finances
|
|
||||||
</h1>
|
|
||||||
<p className="text-lg text-muted-foreground mb-8 max-w-xl mx-auto">
|
<p className="text-lg text-muted-foreground mb-8 max-w-xl mx-auto">
|
||||||
A clean, minimal tool to track your net worth, manage debt, monitor cashflow, and invoice clients—all in one place.
|
A clean, minimal tool to track your net worth, manage debt, monitor cashflow, and invoice clients—all in one place.
|
||||||
</p>
|
</p>
|
||||||
@@ -86,7 +84,7 @@ export default function LandingPage() {
|
|||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<h2 className="text-xl font-semibold text-center mb-10">Everything you need</h2>
|
<h2 className="text-xl font-semibold text-center mb-10">Everything you need</h2>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
{features.map((feature) => (
|
{features.map(feature => (
|
||||||
<Card key={feature.title} className="card-elevated">
|
<Card key={feature.title} className="card-elevated">
|
||||||
<CardContent className="p-5">
|
<CardContent className="p-5">
|
||||||
<feature.icon className="h-5 w-5 mb-3 text-muted-foreground" />
|
<feature.icon className="h-5 w-5 mb-3 text-muted-foreground" />
|
||||||
@@ -103,9 +101,7 @@ export default function LandingPage() {
|
|||||||
<section className="py-16 px-6 border-t border-border">
|
<section className="py-16 px-6 border-t border-border">
|
||||||
<div className="max-w-xl mx-auto text-center">
|
<div className="max-w-xl mx-auto text-center">
|
||||||
<h2 className="text-xl font-semibold mb-3">Ready to build wealth?</h2>
|
<h2 className="text-xl font-semibold mb-3">Ready to build wealth?</h2>
|
||||||
<p className="text-muted-foreground mb-6">
|
<p className="text-muted-foreground mb-6">Start tracking your finances today. It's free to get started.</p>
|
||||||
Start tracking your finances today. It's free to get started.
|
|
||||||
</p>
|
|
||||||
<Button size="lg" onClick={() => setSignUpOpen(true)}>
|
<Button size="lg" onClick={() => setSignUpOpen(true)}>
|
||||||
Create your account
|
Create your account
|
||||||
</Button>
|
</Button>
|
||||||
@@ -116,11 +112,10 @@ export default function LandingPage() {
|
|||||||
<section className="py-8 px-6 border-t border-border bg-muted/30">
|
<section className="py-8 px-6 border-t border-border bg-muted/30">
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="max-w-3xl mx-auto">
|
||||||
<p className="text-xs text-muted-foreground text-center leading-relaxed">
|
<p className="text-xs text-muted-foreground text-center leading-relaxed">
|
||||||
<strong>Disclaimer:</strong> This application is for informational and personal tracking purposes only.
|
<strong>Disclaimer:</strong> This application is for informational and personal tracking purposes only. It does not constitute financial,
|
||||||
It does not constitute financial, investment, tax, or legal advice. The information provided should not
|
investment, tax, or legal advice. The information provided should not be relied upon for making financial decisions. Always consult with qualified
|
||||||
be relied upon for making financial decisions. Always consult with qualified professionals before making
|
professionals before making any financial decisions. We make no guarantees about the accuracy or completeness of the data you enter or the
|
||||||
any financial decisions. We make no guarantees about the accuracy or completeness of the data you enter
|
calculations performed. Use at your own risk. Past performance is not indicative of future results.
|
||||||
or the calculations performed. Use at your own risk. Past performance is not indicative of future results.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -136,9 +131,22 @@ export default function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<LoginDialog open={loginOpen} onOpenChange={setLoginOpen} onSwitchToSignUp={() => { setLoginOpen(false); setSignUpOpen(true); }} />
|
<LoginDialog
|
||||||
<SignUpDialog open={signUpOpen} onOpenChange={setSignUpOpen} onSwitchToLogin={() => { setSignUpOpen(false); setLoginOpen(true); }} />
|
open={loginOpen}
|
||||||
|
onOpenChange={setLoginOpen}
|
||||||
|
onSwitchToSignUp={() => {
|
||||||
|
setLoginOpen(false);
|
||||||
|
setSignUpOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SignUpDialog
|
||||||
|
open={signUpOpen}
|
||||||
|
onOpenChange={setSignUpOpen}
|
||||||
|
onSwitchToLogin={() => {
|
||||||
|
setSignUpOpen(false);
|
||||||
|
setLoginOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import {createSlice, createAsyncThunk, type PayloadAction} from '@reduxjs/toolkit';
|
import {createSlice, createAsyncThunk, type PayloadAction} from '@reduxjs/toolkit';
|
||||||
import {incomeService, expenseService, transactionService, type IncomeSource as ApiIncome, type Expense as ApiExpense, type Transaction as ApiTransaction} from '@/lib/api/cashflow.service';
|
import {
|
||||||
|
incomeService,
|
||||||
|
expenseService,
|
||||||
|
transactionService,
|
||||||
|
type IncomeSource as ApiIncome,
|
||||||
|
type Expense as ApiExpense,
|
||||||
|
type Transaction as ApiTransaction
|
||||||
|
} from '@/lib/api/cashflow.service';
|
||||||
|
|
||||||
export interface IncomeSource {
|
export interface IncomeSource {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -66,7 +73,7 @@ const mapApiIncomeToIncome = (apiIncome: ApiIncome): IncomeSource => ({
|
|||||||
category: 'Income',
|
category: 'Income',
|
||||||
nextDate: new Date().toISOString(),
|
nextDate: new Date().toISOString(),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
createdAt: apiIncome.createdAt || new Date().toISOString(),
|
createdAt: apiIncome.createdAt || new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
|
const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
|
||||||
@@ -78,7 +85,7 @@ const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
|
|||||||
nextDate: new Date().toISOString(),
|
nextDate: new Date().toISOString(),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isEssential: apiExpense.isEssential || false,
|
isEssential: apiExpense.isEssential || false,
|
||||||
createdAt: apiExpense.createdAt || new Date().toISOString(),
|
createdAt: apiExpense.createdAt || new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transaction => ({
|
const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transaction => ({
|
||||||
@@ -88,7 +95,7 @@ const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transac
|
|||||||
amount: apiTransaction.amount,
|
amount: apiTransaction.amount,
|
||||||
category: apiTransaction.category || 'Other',
|
category: apiTransaction.category || 'Other',
|
||||||
date: apiTransaction.date,
|
date: apiTransaction.date,
|
||||||
note: apiTransaction.notes,
|
note: apiTransaction.notes
|
||||||
});
|
});
|
||||||
|
|
||||||
// Async thunks
|
// Async thunks
|
||||||
@@ -210,7 +217,7 @@ const cashflowSlice = createSlice({
|
|||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
type CreateAssetRequest,
|
type CreateAssetRequest,
|
||||||
type UpdateAssetRequest,
|
type UpdateAssetRequest,
|
||||||
type CreateLiabilityRequest,
|
type CreateLiabilityRequest,
|
||||||
type UpdateLiabilityRequest,
|
type UpdateLiabilityRequest
|
||||||
} from '@/lib/api/networth.service';
|
} from '@/lib/api/networth.service';
|
||||||
|
|
||||||
export interface Asset {
|
export interface Asset {
|
||||||
@@ -57,7 +57,7 @@ const mapApiAssetToAsset = (apiAsset: ApiAsset): Asset => ({
|
|||||||
name: apiAsset.name,
|
name: apiAsset.name,
|
||||||
type: apiAsset.type.toLowerCase() as Asset['type'],
|
type: apiAsset.type.toLowerCase() as Asset['type'],
|
||||||
value: apiAsset.value,
|
value: apiAsset.value,
|
||||||
updatedAt: apiAsset.updatedAt || new Date().toISOString(),
|
updatedAt: apiAsset.updatedAt || new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({
|
const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({
|
||||||
@@ -65,7 +65,7 @@ const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({
|
|||||||
name: apiLiability.name,
|
name: apiLiability.name,
|
||||||
type: apiLiability.type.toLowerCase() as Liability['type'],
|
type: apiLiability.type.toLowerCase() as Liability['type'],
|
||||||
balance: apiLiability.currentBalance,
|
balance: apiLiability.currentBalance,
|
||||||
updatedAt: apiLiability.updatedAt || new Date().toISOString(),
|
updatedAt: apiLiability.updatedAt || new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Async thunks for assets
|
// Async thunks for assets
|
||||||
@@ -191,7 +191,7 @@ const netWorthSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setError: (state, action: PayloadAction<string | null>) => {
|
setError: (state, action: PayloadAction<string | null>) => {
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
// Fetch assets
|
// Fetch assets
|
||||||
@@ -267,7 +267,7 @@ const netWorthSlice = createSlice({
|
|||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {setLoading, setError} = netWorthSlice.actions;
|
export const {setLoading, setError} = netWorthSlice.actions;
|
||||||
|
|||||||
Reference in New Issue
Block a user