Add lock files for package management and update architecture documentation
- Introduced bun.lock and package-lock.json to manage dependencies for the project. - Enhanced backend API architecture documentation with additional security and documentation guidelines. - Made minor formatting adjustments across various files for consistency and clarity.
This commit is contained in:
@@ -7,6 +7,7 @@ This backend API is built following **SOLID principles** and **clean architectur
|
||||
## SOLID Principles Implementation
|
||||
|
||||
### 1. Single Responsibility Principle (SRP)
|
||||
|
||||
Each class has one well-defined responsibility:
|
||||
|
||||
- **Controllers** - Handle HTTP requests/responses only
|
||||
@@ -15,23 +16,25 @@ Each class has one well-defined responsibility:
|
||||
- **Middleware** - Handle cross-cutting concerns (auth, errors)
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
// AssetService - ONLY handles asset business logic
|
||||
export class AssetService {
|
||||
async create(userId: string, data: CreateAssetDTO): Promise<Asset>
|
||||
async update(id: string, userId: string, data: UpdateAssetDTO): Promise<Asset>
|
||||
async create(userId: string, data: CreateAssetDTO): Promise<Asset>;
|
||||
async update(id: string, userId: string, data: UpdateAssetDTO): Promise<Asset>;
|
||||
// ...
|
||||
}
|
||||
|
||||
// AssetRepository - ONLY handles database operations
|
||||
export class AssetRepository {
|
||||
async findById(id: string): Promise<Asset | null>
|
||||
async create(data: Prisma.AssetCreateInput): Promise<Asset>
|
||||
async findById(id: string): Promise<Asset | null>;
|
||||
async create(data: Prisma.AssetCreateInput): Promise<Asset>;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Open/Closed Principle (OCP)
|
||||
|
||||
The system is open for extension but closed for modification:
|
||||
|
||||
- **Custom Error Classes** - Extend `AppError` base class for new error types
|
||||
@@ -39,6 +42,7 @@ The system is open for extension but closed for modification:
|
||||
- **Service Pattern** - Add new services without modifying existing ones
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
// Extensible error hierarchy
|
||||
export abstract class AppError extends Error {
|
||||
@@ -55,6 +59,7 @@ export class ValidationError extends AppError {
|
||||
```
|
||||
|
||||
### 3. Liskov Substitution Principle (LSP)
|
||||
|
||||
Derived classes can substitute their base classes:
|
||||
|
||||
```typescript
|
||||
@@ -71,6 +76,7 @@ export interface IUserScopedRepository<T> extends Omit<IRepository<T>, 'findAll'
|
||||
```
|
||||
|
||||
### 4. Interface Segregation Principle (ISP)
|
||||
|
||||
Clients depend only on interfaces they use:
|
||||
|
||||
- `IRepository<T>` - Base CRUD operations
|
||||
@@ -78,6 +84,7 @@ Clients depend only on interfaces they use:
|
||||
- Specific methods in services (e.g., `getTotalValue()` in AssetService)
|
||||
|
||||
### 5. Dependency Inversion Principle (DIP)
|
||||
|
||||
High-level modules depend on abstractions:
|
||||
|
||||
```typescript
|
||||
@@ -99,6 +106,7 @@ class DatabaseConnection {
|
||||
## Architecture Layers
|
||||
|
||||
### 1. Presentation Layer (Controllers & Routes)
|
||||
|
||||
- **Location**: `src/controllers/`, `src/routes/`
|
||||
- **Purpose**: Handle HTTP requests/responses
|
||||
- **Responsibilities**:
|
||||
@@ -119,6 +127,7 @@ export class AssetController {
|
||||
```
|
||||
|
||||
### 2. Business Logic Layer (Services)
|
||||
|
||||
- **Location**: `src/services/`
|
||||
- **Purpose**: Implement business rules
|
||||
- **Responsibilities**:
|
||||
@@ -139,6 +148,7 @@ export class InvoiceService {
|
||||
```
|
||||
|
||||
### 3. Data Access Layer (Repositories)
|
||||
|
||||
- **Location**: `src/repositories/`
|
||||
- **Purpose**: Abstract database operations
|
||||
- **Responsibilities**:
|
||||
@@ -151,13 +161,14 @@ export class AssetRepository implements IUserScopedRepository<Asset> {
|
||||
async findAllByUser(userId: string): Promise<Asset[]> {
|
||||
return prisma.asset.findMany({
|
||||
where: {userId},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Cross-Cutting Concerns (Middleware & Utils)
|
||||
|
||||
- **Location**: `src/middleware/`, `src/utils/`
|
||||
- **Purpose**: Handle common functionality
|
||||
- **Components**:
|
||||
@@ -221,21 +232,25 @@ User
|
||||
## Security Features
|
||||
|
||||
### 1. Authentication
|
||||
|
||||
- JWT tokens with configurable expiration
|
||||
- Secure password hashing (bcrypt with 10 rounds)
|
||||
- Password complexity requirements
|
||||
|
||||
### 2. Authorization
|
||||
|
||||
- User-scoped data access
|
||||
- Repository methods verify ownership
|
||||
- Middleware extracts authenticated user
|
||||
|
||||
### 3. Input Validation
|
||||
|
||||
- Zod schemas for runtime validation
|
||||
- Type-safe request/response handling
|
||||
- SQL injection prevention (Prisma ORM)
|
||||
|
||||
### 4. Error Handling
|
||||
|
||||
- Custom error classes
|
||||
- No sensitive information in error messages
|
||||
- Proper HTTP status codes
|
||||
@@ -243,6 +258,7 @@ User
|
||||
## API Design
|
||||
|
||||
### RESTful Conventions
|
||||
|
||||
- `GET /api/resources` - List all
|
||||
- `GET /api/resources/:id` - Get one
|
||||
- `POST /api/resources` - Create
|
||||
@@ -251,11 +267,16 @@ User
|
||||
- `DELETE /api/resources/:id` - Delete
|
||||
|
||||
### Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": { /* data */ },
|
||||
"resource": {
|
||||
/* data */
|
||||
},
|
||||
// or
|
||||
"resources": [ /* array */ ],
|
||||
"resources": [
|
||||
/* array */
|
||||
],
|
||||
// or on error
|
||||
"error": "ErrorType",
|
||||
"message": "Human-readable message"
|
||||
@@ -263,6 +284,7 @@ User
|
||||
```
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
- `200 OK` - Successful GET/PUT/PATCH
|
||||
- `201 Created` - Successful POST
|
||||
- `204 No Content` - Successful DELETE
|
||||
@@ -276,16 +298,19 @@ User
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Test services in isolation
|
||||
- Mock repository dependencies
|
||||
- Test business logic thoroughly
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Test API endpoints
|
||||
- Use test database
|
||||
- Verify request/response flow
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Test complete user flows
|
||||
- Verify authentication
|
||||
- Test error scenarios
|
||||
@@ -293,16 +318,19 @@ User
|
||||
## Performance Considerations
|
||||
|
||||
### Database
|
||||
|
||||
- Indexes on frequently queried fields
|
||||
- Connection pooling (Prisma)
|
||||
- Efficient query composition
|
||||
|
||||
### Caching
|
||||
|
||||
- JWT tokens cached in client
|
||||
- Consider Redis for session management
|
||||
- Database query result caching
|
||||
|
||||
### Scalability
|
||||
|
||||
- Stateless API (horizontal scaling)
|
||||
- Database migrations for schema changes
|
||||
- Environment-based configuration
|
||||
@@ -328,18 +356,21 @@ User
|
||||
## Best Practices
|
||||
|
||||
### Code Organization
|
||||
|
||||
✅ One class per file
|
||||
✅ Group related files in directories
|
||||
✅ Use barrel exports (index.ts)
|
||||
✅ Consistent naming conventions
|
||||
|
||||
### Error Handling
|
||||
|
||||
✅ Use custom error classes
|
||||
✅ Validate at boundaries
|
||||
✅ Log errors appropriately
|
||||
✅ Return user-friendly messages
|
||||
|
||||
### Security
|
||||
|
||||
✅ Never log sensitive data
|
||||
✅ Validate all inputs
|
||||
✅ Use parameterized queries (Prisma)
|
||||
@@ -347,6 +378,7 @@ User
|
||||
✅ Keep dependencies updated
|
||||
|
||||
### Documentation
|
||||
|
||||
✅ JSDoc comments for public APIs
|
||||
✅ README for setup instructions
|
||||
✅ API documentation (Swagger)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||
globs: '*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json'
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
console.log("Hello via Bun!");
|
||||
console.log('Hello via Bun!');
|
||||
|
||||
@@ -12,7 +12,7 @@ class DatabaseConnection {
|
||||
public static getInstance(): PrismaClient {
|
||||
if (!DatabaseConnection.instance) {
|
||||
DatabaseConnection.instance = new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
|
||||
});
|
||||
}
|
||||
return DatabaseConnection.instance;
|
||||
|
||||
@@ -10,7 +10,7 @@ const envSchema = z.object({
|
||||
DATABASE_URL: z.string().min(1),
|
||||
JWT_SECRET: z.string().min(32),
|
||||
JWT_EXPIRES_IN: z.string().default('7d'),
|
||||
CORS_ORIGIN: z.string().default('http://localhost:5174'),
|
||||
CORS_ORIGIN: z.string().default('http://localhost:5174')
|
||||
});
|
||||
|
||||
type EnvConfig = z.infer<typeof envSchema>;
|
||||
|
||||
@@ -9,13 +9,13 @@ const ASSET_TYPES = ['cash', 'investment', 'property', 'vehicle', 'other'] as co
|
||||
const createAssetSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
type: z.enum(ASSET_TYPES),
|
||||
value: z.number().min(0),
|
||||
value: z.number().min(0)
|
||||
});
|
||||
|
||||
const updateAssetSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
type: z.enum(ASSET_TYPES).optional(),
|
||||
value: z.number().min(0).optional(),
|
||||
value: z.number().min(0).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -32,12 +32,14 @@ export class AssetController {
|
||||
async getAll(request: FastifyRequest, reply: FastifyReply) {
|
||||
const userId = getUserId(request);
|
||||
const assets = await this.assetService.getAll(userId);
|
||||
|
||||
return reply.send({assets});
|
||||
}
|
||||
|
||||
async getById(request: FastifyRequest<{Params: {id: string}}>, reply: FastifyReply) {
|
||||
const userId = getUserId(request);
|
||||
const asset = await this.assetService.getById(request.params.id, userId);
|
||||
|
||||
return reply.send({asset});
|
||||
}
|
||||
|
||||
@@ -45,6 +47,7 @@ export class AssetController {
|
||||
const userId = getUserId(request);
|
||||
const data = createAssetSchema.parse(request.body);
|
||||
const asset = await this.assetService.create(userId, data);
|
||||
|
||||
return reply.status(201).send({asset});
|
||||
}
|
||||
|
||||
@@ -52,12 +55,14 @@ export class AssetController {
|
||||
const userId = getUserId(request);
|
||||
const data = updateAssetSchema.parse(request.body);
|
||||
const asset = await this.assetService.update(request.params.id, userId, data);
|
||||
|
||||
return reply.send({asset});
|
||||
}
|
||||
|
||||
async delete(request: FastifyRequest<{Params: {id: string}}>, reply: FastifyReply) {
|
||||
const userId = getUserId(request);
|
||||
await this.assetService.delete(request.params.id, userId);
|
||||
|
||||
return reply.status(204).send();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
|
||||
const registerSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
name: z.string().min(1),
|
||||
name: z.string().min(1)
|
||||
});
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
password: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -37,12 +37,12 @@ export class AuthController {
|
||||
|
||||
const token = request.server.jwt.sign({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
email: user.email
|
||||
});
|
||||
|
||||
return reply.status(201).send({
|
||||
user,
|
||||
token,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ export class AuthController {
|
||||
|
||||
const token = request.server.jwt.sign({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
email: user.email
|
||||
});
|
||||
|
||||
const {password: _, ...userWithoutPassword} = user;
|
||||
|
||||
return reply.send({
|
||||
user: userWithoutPassword,
|
||||
token,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const createIncomeSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
amount: z.number().min(0.01),
|
||||
frequency: z.string(),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
const updateIncomeSchema = createIncomeSchema.partial();
|
||||
@@ -17,8 +17,11 @@ const createExpenseSchema = z.object({
|
||||
amount: z.number().min(0.01),
|
||||
category: z.string(),
|
||||
frequency: z.string(),
|
||||
dueDate: z.string().transform(str => new Date(str)).optional(),
|
||||
notes: z.string().optional(),
|
||||
dueDate: z
|
||||
.string()
|
||||
.transform(str => new Date(str))
|
||||
.optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
const updateExpenseSchema = createExpenseSchema.partial();
|
||||
@@ -29,7 +32,7 @@ const createTransactionSchema = z.object({
|
||||
amount: z.number().min(0.01),
|
||||
date: z.string().transform(str => new Date(str)),
|
||||
description: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -151,11 +154,7 @@ export class CashflowController {
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
const transactions = await this.cashflowService.getTransactionsByDateRange(
|
||||
userId,
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
const transactions = await this.cashflowService.getTransactionsByDateRange(userId, new Date(startDate), new Date(endDate));
|
||||
return reply.send({transactions});
|
||||
}
|
||||
|
||||
@@ -181,11 +180,7 @@ export class CashflowController {
|
||||
const userId = getUserId(request);
|
||||
const {startDate, endDate} = request.query as {startDate: string; endDate: string};
|
||||
|
||||
const summary = await this.cashflowService.getCashflowSummary(
|
||||
userId,
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
const summary = await this.cashflowService.getCashflowSummary(userId, new Date(startDate), new Date(endDate));
|
||||
|
||||
return reply.send(summary);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const createClientSchema = z.object({
|
||||
email: z.string().email(),
|
||||
phone: z.string().max(50).optional(),
|
||||
address: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
const updateClientSchema = z.object({
|
||||
@@ -16,7 +16,7 @@ const updateClientSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
phone: z.string().max(50).optional(),
|
||||
address: z.string().optional(),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,11 @@ const createAccountSchema = z.object({
|
||||
currentBalance: z.number().min(0),
|
||||
interestRate: z.number().min(0).max(100).optional(),
|
||||
minimumPayment: z.number().min(0).optional(),
|
||||
dueDate: z.string().transform(str => new Date(str)).optional(),
|
||||
notes: z.string().optional(),
|
||||
dueDate: z
|
||||
.string()
|
||||
.transform(str => new Date(str))
|
||||
.optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
const updateAccountSchema = z.object({
|
||||
@@ -23,8 +26,11 @@ const updateAccountSchema = z.object({
|
||||
currentBalance: z.number().min(0).optional(),
|
||||
interestRate: z.number().min(0).max(100).optional(),
|
||||
minimumPayment: z.number().min(0).optional(),
|
||||
dueDate: z.string().transform(str => new Date(str)).optional(),
|
||||
notes: z.string().optional(),
|
||||
dueDate: z
|
||||
.string()
|
||||
.transform(str => new Date(str))
|
||||
.optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,13 +6,19 @@ import {z} from 'zod';
|
||||
const createCategorySchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
description: z.string().optional(),
|
||||
color: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).optional(),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
|
||||
.optional()
|
||||
});
|
||||
|
||||
const updateCategorySchema = z.object({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
description: z.string().optional(),
|
||||
color: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).optional(),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
|
||||
.optional()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ const createPaymentSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
amount: z.number().min(0.01),
|
||||
paymentDate: z.string().transform(str => new Date(str)),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -46,11 +46,7 @@ export class DebtPaymentController {
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
const payments = await this.paymentService.getByDateRange(
|
||||
userId,
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
const payments = await this.paymentService.getByDateRange(userId, new Date(startDate), new Date(endDate));
|
||||
return reply.send({payments});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const lineItemSchema = z.object({
|
||||
description: z.string().min(1),
|
||||
quantity: z.number().min(1),
|
||||
unitPrice: z.number().min(0),
|
||||
amount: z.number().min(0),
|
||||
amount: z.number().min(0)
|
||||
});
|
||||
|
||||
const createInvoiceSchema = z.object({
|
||||
@@ -16,19 +16,25 @@ const createInvoiceSchema = z.object({
|
||||
dueDate: z.string().transform(str => new Date(str)),
|
||||
lineItems: z.array(lineItemSchema).min(1),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
terms: z.string().optional()
|
||||
});
|
||||
|
||||
const updateInvoiceSchema = z.object({
|
||||
issueDate: z.string().transform(str => new Date(str)).optional(),
|
||||
dueDate: z.string().transform(str => new Date(str)).optional(),
|
||||
issueDate: z
|
||||
.string()
|
||||
.transform(str => new Date(str))
|
||||
.optional(),
|
||||
dueDate: z
|
||||
.string()
|
||||
.transform(str => new Date(str))
|
||||
.optional(),
|
||||
lineItems: z.array(lineItemSchema).min(1).optional(),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
terms: z.string().optional()
|
||||
});
|
||||
|
||||
const updateStatusSchema = z.object({
|
||||
status: z.enum(['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED']),
|
||||
status: z.enum(['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED'])
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -59,7 +65,7 @@ export class InvoiceController {
|
||||
|
||||
const invoices = await this.invoiceService.getAllByUser(userId, {
|
||||
clientId,
|
||||
status,
|
||||
status
|
||||
});
|
||||
|
||||
return reply.send({invoices});
|
||||
|
||||
@@ -9,9 +9,12 @@ const createLiabilitySchema = z.object({
|
||||
currentBalance: z.number().min(0),
|
||||
interestRate: z.number().min(0).max(100).optional(),
|
||||
minimumPayment: z.number().min(0).optional(),
|
||||
dueDate: z.string().transform(str => new Date(str)).optional(),
|
||||
dueDate: z
|
||||
.string()
|
||||
.transform(str => new Date(str))
|
||||
.optional(),
|
||||
creditor: z.string().max(255).optional(),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
const updateLiabilitySchema = z.object({
|
||||
@@ -20,9 +23,12 @@ const updateLiabilitySchema = z.object({
|
||||
currentBalance: z.number().min(0).optional(),
|
||||
interestRate: z.number().min(0).max(100).optional(),
|
||||
minimumPayment: z.number().min(0).optional(),
|
||||
dueDate: z.string().transform(str => new Date(str)).optional(),
|
||||
dueDate: z
|
||||
.string()
|
||||
.transform(str => new Date(str))
|
||||
.optional(),
|
||||
creditor: z.string().max(255).optional(),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,16 +8,16 @@ const createSnapshotSchema = z.object({
|
||||
totalAssets: z.number().min(0),
|
||||
totalLiabilities: z.number().min(0),
|
||||
netWorth: z.number(),
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
const createFromCurrentSchema = z.object({
|
||||
notes: z.string().optional(),
|
||||
notes: z.string().optional()
|
||||
});
|
||||
|
||||
const dateRangeSchema = z.object({
|
||||
startDate: z.string().transform(str => new Date(str)),
|
||||
endDate: z.string().transform(str => new Date(str)),
|
||||
endDate: z.string().transform(str => new Date(str))
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -55,11 +55,7 @@ export class NetWorthController {
|
||||
const {startDate, endDate} = request.query as {startDate: string; endDate: string};
|
||||
|
||||
const parsed = dateRangeSchema.parse({startDate, endDate});
|
||||
const snapshots = await this.netWorthService.getSnapshotsByDateRange(
|
||||
userId,
|
||||
parsed.startDate,
|
||||
parsed.endDate
|
||||
);
|
||||
const snapshots = await this.netWorthService.getSnapshotsByDateRange(userId, parsed.startDate, parsed.endDate);
|
||||
|
||||
return reply.send({snapshots});
|
||||
}
|
||||
@@ -119,10 +115,7 @@ export class NetWorthController {
|
||||
const userId = getUserId(request);
|
||||
const {limit} = request.query as {limit?: string};
|
||||
|
||||
const stats = await this.netWorthService.getGrowthStats(
|
||||
userId,
|
||||
limit ? parseInt(limit) : undefined
|
||||
);
|
||||
const stats = await this.netWorthService.getGrowthStats(userId, limit ? parseInt(limit) : undefined);
|
||||
|
||||
return reply.send({stats});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ async function main() {
|
||||
// Start server
|
||||
await server.listen({
|
||||
port: env.PORT,
|
||||
host: '0.0.0.0',
|
||||
host: '0.0.0.0'
|
||||
});
|
||||
|
||||
server.log.info(`🚀 Server listening on http://localhost:${env.PORT}`);
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
|
||||
if (error instanceof AppError) {
|
||||
return reply.status(error.statusCode).send({
|
||||
error: error.name,
|
||||
message: error.message,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
|
||||
return reply.status(400).send({
|
||||
error: 'ValidationError',
|
||||
message: 'Invalid request data',
|
||||
details: error.errors,
|
||||
details: error.errors
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
|
||||
return reply.status(400).send({
|
||||
error: 'ValidationError',
|
||||
message: error.message,
|
||||
details: error.validation,
|
||||
details: error.validation
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,13 +42,13 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
|
||||
if (prismaError.code === 'P2002') {
|
||||
return reply.status(409).send({
|
||||
error: 'ConflictError',
|
||||
message: 'A record with this value already exists',
|
||||
message: 'A record with this value already exists'
|
||||
});
|
||||
}
|
||||
if (prismaError.code === 'P2025') {
|
||||
return reply.status(404).send({
|
||||
error: 'NotFoundError',
|
||||
message: 'Record not found',
|
||||
message: 'Record not found'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,6 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
|
||||
|
||||
return reply.status(statusCode).send({
|
||||
error: 'ServerError',
|
||||
message,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ export class AssetRepository {
|
||||
|
||||
async findByIdAndUser(id: string, userId: string): Promise<Asset | null> {
|
||||
return prisma.asset.findFirst({
|
||||
where: {id, userId},
|
||||
where: {id, userId}
|
||||
});
|
||||
}
|
||||
|
||||
async findAllByUser(userId: string, filters?: Record<string, any>): Promise<Asset[]> {
|
||||
return prisma.asset.findMany({
|
||||
where: {userId, ...filters},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class AssetRepository {
|
||||
async update(id: string, data: Prisma.AssetUpdateInput): Promise<Asset> {
|
||||
return prisma.asset.update({
|
||||
where: {id},
|
||||
data,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class AssetRepository {
|
||||
async getTotalValue(userId: string): Promise<number> {
|
||||
const result = await prisma.asset.aggregate({
|
||||
where: {userId},
|
||||
_sum: {value: true},
|
||||
_sum: {value: true}
|
||||
});
|
||||
return result._sum.value || 0;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class IncomeSourceRepository {
|
||||
async findAllByUser(userId: string): Promise<IncomeSource[]> {
|
||||
return prisma.incomeSource.findMany({
|
||||
where: {userId},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export class IncomeSourceRepository {
|
||||
async getTotalMonthlyIncome(userId: string): Promise<number> {
|
||||
const result = await prisma.incomeSource.aggregate({
|
||||
where: {userId},
|
||||
_sum: {amount: true},
|
||||
_sum: {amount: true}
|
||||
});
|
||||
return result._sum.amount || 0;
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export class ExpenseRepository {
|
||||
async findAllByUser(userId: string): Promise<Expense[]> {
|
||||
return prisma.expense.findMany({
|
||||
where: {userId},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,18 +77,21 @@ export class ExpenseRepository {
|
||||
async getTotalMonthlyExpenses(userId: string): Promise<number> {
|
||||
const result = await prisma.expense.aggregate({
|
||||
where: {userId},
|
||||
_sum: {amount: true},
|
||||
_sum: {amount: true}
|
||||
});
|
||||
return result._sum.amount || 0;
|
||||
}
|
||||
|
||||
async getByCategory(userId: string): Promise<Record<string, Expense[]>> {
|
||||
const expenses = await this.findAllByUser(userId);
|
||||
return expenses.reduce((acc, expense) => {
|
||||
if (!acc[expense.category]) acc[expense.category] = [];
|
||||
acc[expense.category].push(expense);
|
||||
return acc;
|
||||
}, {} as Record<string, Expense[]>);
|
||||
return expenses.reduce(
|
||||
(acc, expense) => {
|
||||
if (!acc[expense.category]) acc[expense.category] = [];
|
||||
acc[expense.category].push(expense);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Expense[]>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ export class TransactionRepository {
|
||||
async findAllByUser(userId: string): Promise<Transaction[]> {
|
||||
return prisma.transaction.findMany({
|
||||
where: {userId},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,38 +130,38 @@ export class TransactionRepository {
|
||||
return prisma.transaction.findMany({
|
||||
where: {
|
||||
userId,
|
||||
date: {gte: startDate, lte: endDate},
|
||||
date: {gte: startDate, lte: endDate}
|
||||
},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
async getByType(userId: string, type: string): Promise<Transaction[]> {
|
||||
return prisma.transaction.findMany({
|
||||
where: {userId, type},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
async getCashflowSummary(userId: string, startDate: Date, endDate: Date): Promise<{
|
||||
async getCashflowSummary(
|
||||
userId: string,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<{
|
||||
totalIncome: number;
|
||||
totalExpenses: number;
|
||||
netCashflow: number;
|
||||
}> {
|
||||
const transactions = await this.getByDateRange(userId, startDate, endDate);
|
||||
|
||||
const totalIncome = transactions
|
||||
.filter(t => t.type === 'income')
|
||||
.reduce((sum, t) => sum + t.amount, 0);
|
||||
const totalIncome = transactions.filter(t => t.type === 'income').reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
const totalExpenses = transactions
|
||||
.filter(t => t.type === 'expense')
|
||||
.reduce((sum, t) => sum + t.amount, 0);
|
||||
const totalExpenses = transactions.filter(t => t.type === 'expense').reduce((sum, t) => sum + t.amount, 0);
|
||||
|
||||
return {
|
||||
totalIncome,
|
||||
totalExpenses,
|
||||
netCashflow: totalIncome - totalExpenses,
|
||||
netCashflow: totalIncome - totalExpenses
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ export class ClientRepository {
|
||||
return prisma.client.findUnique({
|
||||
where: {id},
|
||||
include: {
|
||||
invoices: true,
|
||||
},
|
||||
invoices: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ export class ClientRepository {
|
||||
return prisma.client.findFirst({
|
||||
where: {id, userId},
|
||||
include: {
|
||||
invoices: true,
|
||||
},
|
||||
invoices: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ export class ClientRepository {
|
||||
where: {userId},
|
||||
include: {
|
||||
invoices: {
|
||||
orderBy: {createdAt: 'desc'},
|
||||
},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
}
|
||||
},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export class ClientRepository {
|
||||
return prisma.client.create({
|
||||
data,
|
||||
include: {
|
||||
invoices: true,
|
||||
},
|
||||
invoices: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ export class ClientRepository {
|
||||
where: {id},
|
||||
data,
|
||||
include: {
|
||||
invoices: true,
|
||||
},
|
||||
invoices: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.client.delete({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ export class ClientRepository {
|
||||
return prisma.client.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
email,
|
||||
},
|
||||
email
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,13 +82,13 @@ export class ClientRepository {
|
||||
const result = await prisma.invoice.aggregate({
|
||||
where: {
|
||||
client: {
|
||||
userId,
|
||||
userId
|
||||
},
|
||||
status: 'paid',
|
||||
status: 'paid'
|
||||
},
|
||||
_sum: {
|
||||
total: true,
|
||||
},
|
||||
total: true
|
||||
}
|
||||
});
|
||||
|
||||
return result._sum.total || 0;
|
||||
@@ -105,11 +105,11 @@ export class ClientRepository {
|
||||
select: {
|
||||
id: true,
|
||||
total: true,
|
||||
status: true,
|
||||
},
|
||||
},
|
||||
status: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
|
||||
return clients.map(client => ({
|
||||
@@ -117,13 +117,9 @@ export class ClientRepository {
|
||||
stats: {
|
||||
totalInvoices: client.invoices.length,
|
||||
paidInvoices: client.invoices.filter(inv => inv.status === 'paid').length,
|
||||
totalRevenue: client.invoices
|
||||
.filter(inv => inv.status === 'paid')
|
||||
.reduce((sum, inv) => sum + inv.total, 0),
|
||||
outstandingAmount: client.invoices
|
||||
.filter(inv => inv.status !== 'paid')
|
||||
.reduce((sum, inv) => sum + inv.total, 0),
|
||||
},
|
||||
totalRevenue: client.invoices.filter(inv => inv.status === 'paid').reduce((sum, inv) => sum + inv.total, 0),
|
||||
outstandingAmount: client.invoices.filter(inv => inv.status !== 'paid').reduce((sum, inv) => sum + inv.total, 0)
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ export class DebtAccountRepository {
|
||||
include: {
|
||||
category: true,
|
||||
payments: {
|
||||
orderBy: {date: 'desc'},
|
||||
},
|
||||
},
|
||||
orderBy: {date: 'desc'}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ export class DebtAccountRepository {
|
||||
include: {
|
||||
category: true,
|
||||
payments: {
|
||||
orderBy: {date: 'desc'},
|
||||
},
|
||||
},
|
||||
orderBy: {date: 'desc'}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ export class DebtAccountRepository {
|
||||
include: {
|
||||
category: true,
|
||||
payments: {
|
||||
orderBy: {date: 'desc'},
|
||||
},
|
||||
orderBy: {date: 'desc'}
|
||||
}
|
||||
},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,10 +50,10 @@ export class DebtAccountRepository {
|
||||
where: {categoryId},
|
||||
include: {
|
||||
payments: {
|
||||
orderBy: {date: 'desc'},
|
||||
},
|
||||
orderBy: {date: 'desc'}
|
||||
}
|
||||
},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ export class DebtAccountRepository {
|
||||
data,
|
||||
include: {
|
||||
category: true,
|
||||
payments: true,
|
||||
},
|
||||
payments: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,14 +73,14 @@ export class DebtAccountRepository {
|
||||
data,
|
||||
include: {
|
||||
category: true,
|
||||
payments: true,
|
||||
},
|
||||
payments: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.debtAccount.delete({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,8 +91,8 @@ export class DebtAccountRepository {
|
||||
const result = await prisma.debtAccount.aggregate({
|
||||
where: {userId},
|
||||
_sum: {
|
||||
currentBalance: true,
|
||||
},
|
||||
currentBalance: true
|
||||
}
|
||||
});
|
||||
|
||||
return result._sum.currentBalance || 0;
|
||||
@@ -107,9 +107,9 @@ export class DebtAccountRepository {
|
||||
include: {
|
||||
category: true,
|
||||
payments: {
|
||||
orderBy: {date: 'desc'},
|
||||
},
|
||||
},
|
||||
orderBy: {date: 'desc'}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return accounts.map(account => {
|
||||
@@ -122,8 +122,8 @@ export class DebtAccountRepository {
|
||||
totalPaid,
|
||||
numberOfPayments: account.payments.length,
|
||||
lastPaymentDate: lastPayment?.date || null,
|
||||
lastPaymentAmount: lastPayment?.amount || null,
|
||||
},
|
||||
lastPaymentAmount: lastPayment?.amount || null
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
include: {
|
||||
accounts: {
|
||||
include: {
|
||||
payments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
payments: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
include: {
|
||||
accounts: {
|
||||
include: {
|
||||
payments: true,
|
||||
payments: true
|
||||
},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
}
|
||||
},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
return prisma.debtCategory.create({
|
||||
data,
|
||||
include: {
|
||||
accounts: true,
|
||||
},
|
||||
accounts: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,14 +51,14 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
where: {id},
|
||||
data,
|
||||
include: {
|
||||
accounts: true,
|
||||
},
|
||||
accounts: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.debtCategory.delete({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
return prisma.debtCategory.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
name,
|
||||
},
|
||||
name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
const result = await prisma.debtAccount.aggregate({
|
||||
where: {categoryId},
|
||||
_sum: {
|
||||
currentBalance: true,
|
||||
},
|
||||
currentBalance: true
|
||||
}
|
||||
});
|
||||
|
||||
return result._sum.currentBalance || 0;
|
||||
@@ -97,19 +97,15 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
|
||||
return Promise.all(
|
||||
categories.map(async category => {
|
||||
const totalDebt = await this.getTotalDebt(category.id);
|
||||
const totalPayments = category.accounts.reduce(
|
||||
(sum, account) =>
|
||||
sum + account.payments.reduce((pSum, payment) => pSum + payment.amount, 0),
|
||||
0
|
||||
);
|
||||
const totalPayments = category.accounts.reduce((sum, account) => sum + account.payments.reduce((pSum, payment) => pSum + payment.amount, 0), 0);
|
||||
|
||||
return {
|
||||
...category,
|
||||
stats: {
|
||||
totalAccounts: category.accounts.length,
|
||||
totalDebt,
|
||||
totalPayments,
|
||||
},
|
||||
totalPayments
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -14,17 +14,17 @@ export class DebtPaymentRepository {
|
||||
include: {
|
||||
account: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
category: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async findByAccount(accountId: string): Promise<DebtPayment[]> {
|
||||
return prisma.debtPayment.findMany({
|
||||
where: {accountId},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,18 +33,18 @@ export class DebtPaymentRepository {
|
||||
where: {
|
||||
account: {
|
||||
category: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
userId
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
account: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
category: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,16 +54,16 @@ export class DebtPaymentRepository {
|
||||
include: {
|
||||
account: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
category: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.debtPayment.delete({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@ export class DebtPaymentRepository {
|
||||
const result = await prisma.debtPayment.aggregate({
|
||||
where: {accountId},
|
||||
_sum: {
|
||||
amount: true,
|
||||
},
|
||||
amount: true
|
||||
}
|
||||
});
|
||||
|
||||
return result._sum.amount || 0;
|
||||
@@ -89,13 +89,13 @@ export class DebtPaymentRepository {
|
||||
where: {
|
||||
account: {
|
||||
category: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
userId
|
||||
}
|
||||
}
|
||||
},
|
||||
_sum: {
|
||||
amount: true,
|
||||
},
|
||||
amount: true
|
||||
}
|
||||
});
|
||||
|
||||
return result._sum.amount || 0;
|
||||
@@ -109,22 +109,22 @@ export class DebtPaymentRepository {
|
||||
where: {
|
||||
account: {
|
||||
category: {
|
||||
userId,
|
||||
},
|
||||
userId
|
||||
}
|
||||
},
|
||||
date: {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
lte: endDate
|
||||
}
|
||||
},
|
||||
include: {
|
||||
account: {
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
category: true
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
||||
async findById(id: string): Promise<Invoice | null> {
|
||||
return prisma.invoice.findUnique({
|
||||
where: {id},
|
||||
include: {lineItems: true, client: true},
|
||||
include: {lineItems: true, client: true}
|
||||
}) as unknown as Invoice;
|
||||
}
|
||||
|
||||
async findByIdAndUser(id: string, userId: string): Promise<InvoiceWithLineItems | null> {
|
||||
return prisma.invoice.findFirst({
|
||||
where: {id, userId},
|
||||
include: {lineItems: true, client: true},
|
||||
include: {lineItems: true, client: true}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
||||
return prisma.invoice.findMany({
|
||||
where: {userId, ...filters},
|
||||
include: {lineItems: true, client: true},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: Prisma.InvoiceCreateInput): Promise<Invoice> {
|
||||
return prisma.invoice.create({
|
||||
data,
|
||||
include: {lineItems: true, client: true},
|
||||
include: {lineItems: true, client: true}
|
||||
}) as unknown as Invoice;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
||||
return prisma.invoice.update({
|
||||
where: {id},
|
||||
data,
|
||||
include: {lineItems: true, client: true},
|
||||
include: {lineItems: true, client: true}
|
||||
}) as unknown as Invoice;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
||||
where: {
|
||||
userId,
|
||||
invoiceNumber,
|
||||
...(excludeId && {id: {not: excludeId}}),
|
||||
},
|
||||
...(excludeId && {id: {not: excludeId}})
|
||||
}
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
@@ -69,8 +69,8 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
|
||||
const count = await prisma.invoice.count({
|
||||
where: {
|
||||
userId,
|
||||
invoiceNumber: {startsWith: `INV-${year}-`},
|
||||
},
|
||||
invoiceNumber: {startsWith: `INV-${year}-`}
|
||||
}
|
||||
});
|
||||
return `INV-${year}-${String(count + 1).padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
@@ -10,39 +10,39 @@ const prisma = DatabaseConnection.getInstance();
|
||||
export class LiabilityRepository {
|
||||
async findById(id: string): Promise<Liability | null> {
|
||||
return prisma.liability.findUnique({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
async findByIdAndUser(id: string, userId: string): Promise<Liability | null> {
|
||||
return prisma.liability.findFirst({
|
||||
where: {id, userId},
|
||||
where: {id, userId}
|
||||
});
|
||||
}
|
||||
|
||||
async findAllByUser(userId: string): Promise<Liability[]> {
|
||||
return prisma.liability.findMany({
|
||||
where: {userId},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
orderBy: {createdAt: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: Prisma.LiabilityCreateInput): Promise<Liability> {
|
||||
return prisma.liability.create({
|
||||
data,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, data: Prisma.LiabilityUpdateInput): Promise<Liability> {
|
||||
return prisma.liability.update({
|
||||
where: {id},
|
||||
data,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.liability.delete({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ export class LiabilityRepository {
|
||||
const result = await prisma.liability.aggregate({
|
||||
where: {userId},
|
||||
_sum: {
|
||||
balance: true,
|
||||
},
|
||||
balance: true
|
||||
}
|
||||
});
|
||||
|
||||
return result._sum.balance || 0;
|
||||
@@ -66,13 +66,16 @@ export class LiabilityRepository {
|
||||
async getByType(userId: string): Promise<Record<string, Liability[]>> {
|
||||
const liabilities = await this.findAllByUser(userId);
|
||||
|
||||
return liabilities.reduce((acc, liability) => {
|
||||
const type = liability.type;
|
||||
if (!acc[type]) {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(liability);
|
||||
return acc;
|
||||
}, {} as Record<string, Liability[]>);
|
||||
return liabilities.reduce(
|
||||
(acc, liability) => {
|
||||
const type = liability.type;
|
||||
if (!acc[type]) {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(liability);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Liability[]>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,33 +11,33 @@ const prisma = DatabaseConnection.getInstance();
|
||||
export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWorthSnapshot> {
|
||||
async findById(id: string): Promise<NetWorthSnapshot | null> {
|
||||
return prisma.netWorthSnapshot.findUnique({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
async findAllByUser(userId: string): Promise<NetWorthSnapshot[]> {
|
||||
return prisma.netWorthSnapshot.findMany({
|
||||
where: {userId},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: Prisma.NetWorthSnapshotCreateInput): Promise<NetWorthSnapshot> {
|
||||
return prisma.netWorthSnapshot.create({
|
||||
data,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: string, data: Prisma.NetWorthSnapshotUpdateInput): Promise<NetWorthSnapshot> {
|
||||
return prisma.netWorthSnapshot.update({
|
||||
where: {id},
|
||||
data,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.netWorthSnapshot.delete({
|
||||
where: {id},
|
||||
where: {id}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
|
||||
async getLatest(userId: string): Promise<NetWorthSnapshot | null> {
|
||||
return prisma.netWorthSnapshot.findFirst({
|
||||
where: {userId},
|
||||
orderBy: {date: 'desc'},
|
||||
orderBy: {date: 'desc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
|
||||
userId,
|
||||
date: {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
},
|
||||
lte: endDate
|
||||
}
|
||||
},
|
||||
orderBy: {date: 'asc'},
|
||||
orderBy: {date: 'asc'}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,8 +74,8 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
|
||||
const count = await prisma.netWorthSnapshot.count({
|
||||
where: {
|
||||
userId,
|
||||
date,
|
||||
},
|
||||
date
|
||||
}
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
@@ -88,7 +88,7 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
|
||||
const snapshots = await prisma.netWorthSnapshot.findMany({
|
||||
where: {userId},
|
||||
orderBy: {date: 'desc'},
|
||||
take: limit,
|
||||
take: limit
|
||||
});
|
||||
|
||||
const stats = [];
|
||||
@@ -96,14 +96,13 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
|
||||
const current = snapshots[i];
|
||||
const previous = snapshots[i + 1];
|
||||
const growthAmount = current.netWorth - previous.netWorth;
|
||||
const growthPercent =
|
||||
previous.netWorth !== 0 ? (growthAmount / previous.netWorth) * 100 : 0;
|
||||
const growthPercent = previous.netWorth !== 0 ? (growthAmount / previous.netWorth) * 100 : 0;
|
||||
|
||||
stats.push({
|
||||
date: current.date,
|
||||
netWorth: current.netWorth,
|
||||
growthAmount,
|
||||
growthPercent: parseFloat(growthPercent.toFixed(2)),
|
||||
growthPercent: parseFloat(growthPercent.toFixed(2))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ export class UserRepository implements IRepository<User> {
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
password: false, // Never return password
|
||||
},
|
||||
password: false // Never return password
|
||||
}
|
||||
}) as unknown as User[];
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class UserRepository implements IRepository<User> {
|
||||
async update(id: string, data: Prisma.UserUpdateInput): Promise<User> {
|
||||
return prisma.user.update({
|
||||
where: {id},
|
||||
data,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ export interface IRepository<T, CreateInput = unknown, UpdateInput = unknown> {
|
||||
* User-scoped repository interface
|
||||
* For entities that belong to a specific user
|
||||
*/
|
||||
export interface IUserScopedRepository<T, CreateInput = unknown, UpdateInput = unknown>
|
||||
extends Omit<IRepository<T, CreateInput, UpdateInput>, 'findAll'> {
|
||||
export interface IUserScopedRepository<T, CreateInput = unknown, UpdateInput = unknown> extends Omit<IRepository<T, CreateInput, UpdateInput>, 'findAll'> {
|
||||
findAllByUser(userId: string, filters?: Record<string, unknown>): Promise<T[]>;
|
||||
findByIdAndUser(id: string, userId: string): Promise<T | null>;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ export async function assetRoutes(fastify: FastifyInstance) {
|
||||
schema: {
|
||||
tags: ['Assets'],
|
||||
description: 'Get all user assets',
|
||||
security: [{bearerAuth: []}],
|
||||
security: [{bearerAuth: []}]
|
||||
},
|
||||
handler: controller.getAll.bind(controller),
|
||||
handler: controller.getAll.bind(controller)
|
||||
});
|
||||
|
||||
fastify.get('/:id', {
|
||||
@@ -29,11 +29,11 @@ export async function assetRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
},
|
||||
id: {type: 'string', format: 'uuid'}
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: controller.getById.bind(controller),
|
||||
handler: controller.getById.bind(controller)
|
||||
});
|
||||
|
||||
fastify.post('/', {
|
||||
@@ -47,11 +47,11 @@ export async function assetRoutes(fastify: FastifyInstance) {
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
type: {type: 'string', enum: ['CASH', 'INVESTMENT', 'PROPERTY', 'VEHICLE', 'OTHER']},
|
||||
value: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
value: {type: 'number', minimum: 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: controller.create.bind(controller),
|
||||
handler: controller.create.bind(controller)
|
||||
});
|
||||
|
||||
fastify.put('/:id', {
|
||||
@@ -62,19 +62,19 @@ export async function assetRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
id: {type: 'string', format: 'uuid'}
|
||||
}
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
type: {type: 'string', enum: ['CASH', 'INVESTMENT', 'PROPERTY', 'VEHICLE', 'OTHER']},
|
||||
value: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
value: {type: 'number', minimum: 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: controller.update.bind(controller),
|
||||
handler: controller.update.bind(controller)
|
||||
});
|
||||
|
||||
fastify.delete('/:id', {
|
||||
@@ -85,10 +85,10 @@ export async function assetRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
},
|
||||
id: {type: 'string', format: 'uuid'}
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: controller.delete.bind(controller),
|
||||
handler: controller.delete.bind(controller)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ export async function authRoutes(fastify: FastifyInstance) {
|
||||
properties: {
|
||||
email: {type: 'string', format: 'email'},
|
||||
password: {type: 'string', minLength: 8},
|
||||
name: {type: 'string', minLength: 1},
|
||||
},
|
||||
},
|
||||
name: {type: 'string', minLength: 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: controller.register.bind(controller),
|
||||
handler: controller.register.bind(controller)
|
||||
});
|
||||
|
||||
fastify.post('/login', {
|
||||
@@ -34,20 +34,20 @@ export async function authRoutes(fastify: FastifyInstance) {
|
||||
required: ['email', 'password'],
|
||||
properties: {
|
||||
email: {type: 'string', format: 'email'},
|
||||
password: {type: 'string'},
|
||||
},
|
||||
},
|
||||
password: {type: 'string'}
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: controller.login.bind(controller),
|
||||
handler: controller.login.bind(controller)
|
||||
});
|
||||
|
||||
fastify.get('/profile', {
|
||||
schema: {
|
||||
tags: ['Authentication'],
|
||||
description: 'Get current user profile',
|
||||
security: [{bearerAuth: []}],
|
||||
security: [{bearerAuth: []}]
|
||||
},
|
||||
preHandler: authenticate,
|
||||
handler: controller.getProfile.bind(controller),
|
||||
handler: controller.getProfile.bind(controller)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {CashflowController} from '../controllers/CashflowController';
|
||||
import {CashflowService} from '../services/CashflowService';
|
||||
import {
|
||||
IncomeSourceRepository,
|
||||
ExpenseRepository,
|
||||
TransactionRepository,
|
||||
} from '../repositories/CashflowRepository';
|
||||
import {IncomeSourceRepository, ExpenseRepository, TransactionRepository} from '../repositories/CashflowRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const incomeRepository = new IncomeSourceRepository();
|
||||
@@ -19,199 +15,267 @@ export async function cashflowRoutes(fastify: FastifyInstance) {
|
||||
|
||||
// ===== Income Source Routes =====
|
||||
|
||||
fastify.get('/income', {
|
||||
schema: {
|
||||
description: 'Get all income sources',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.get(
|
||||
'/income',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all income sources',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.getAllIncome.bind(cashflowController));
|
||||
cashflowController.getAllIncome.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.get('/income/total', {
|
||||
schema: {
|
||||
description: 'Get total monthly income',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.get(
|
||||
'/income/total',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get total monthly income',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.getTotalMonthlyIncome.bind(cashflowController));
|
||||
cashflowController.getTotalMonthlyIncome.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.get('/income/:id', {
|
||||
schema: {
|
||||
description: 'Get income source by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.get(
|
||||
'/income/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get income source by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.getOneIncome.bind(cashflowController));
|
||||
cashflowController.getOneIncome.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.post('/income', {
|
||||
schema: {
|
||||
description: 'Create income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'amount', 'frequency'],
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
frequency: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
fastify.post(
|
||||
'/income',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'amount', 'frequency'],
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
frequency: {type: 'string'},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}, cashflowController.createIncome.bind(cashflowController));
|
||||
cashflowController.createIncome.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.put('/income/:id', {
|
||||
schema: {
|
||||
description: 'Update income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.put(
|
||||
'/income/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.updateIncome.bind(cashflowController));
|
||||
cashflowController.updateIncome.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.delete('/income/:id', {
|
||||
schema: {
|
||||
description: 'Delete income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.delete(
|
||||
'/income/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.deleteIncome.bind(cashflowController));
|
||||
cashflowController.deleteIncome.bind(cashflowController)
|
||||
);
|
||||
|
||||
// ===== Expense Routes =====
|
||||
|
||||
fastify.get('/expenses', {
|
||||
schema: {
|
||||
description: 'Get all expenses',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
byCategory: {type: 'string', enum: ['true', 'false']},
|
||||
},
|
||||
},
|
||||
fastify.get(
|
||||
'/expenses',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all expenses',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
byCategory: {type: 'string', enum: ['true', 'false']}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}, cashflowController.getAllExpenses.bind(cashflowController));
|
||||
cashflowController.getAllExpenses.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.get('/expenses/total', {
|
||||
schema: {
|
||||
description: 'Get total monthly expenses',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.get(
|
||||
'/expenses/total',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get total monthly expenses',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.getTotalMonthlyExpenses.bind(cashflowController));
|
||||
cashflowController.getTotalMonthlyExpenses.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.get('/expenses/:id', {
|
||||
schema: {
|
||||
description: 'Get expense by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.get(
|
||||
'/expenses/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get expense by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.getOneExpense.bind(cashflowController));
|
||||
cashflowController.getOneExpense.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.post('/expenses', {
|
||||
schema: {
|
||||
description: 'Create expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'amount', 'category', 'frequency'],
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
category: {type: 'string'},
|
||||
frequency: {type: 'string'},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
fastify.post(
|
||||
'/expenses',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'amount', 'category', 'frequency'],
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
category: {type: 'string'},
|
||||
frequency: {type: 'string'},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}, cashflowController.createExpense.bind(cashflowController));
|
||||
cashflowController.createExpense.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.put('/expenses/:id', {
|
||||
schema: {
|
||||
description: 'Update expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.put(
|
||||
'/expenses/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.updateExpense.bind(cashflowController));
|
||||
cashflowController.updateExpense.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.delete('/expenses/:id', {
|
||||
schema: {
|
||||
description: 'Delete expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.delete(
|
||||
'/expenses/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.deleteExpense.bind(cashflowController));
|
||||
cashflowController.deleteExpense.bind(cashflowController)
|
||||
);
|
||||
|
||||
// ===== Transaction Routes =====
|
||||
|
||||
fastify.get('/transactions', {
|
||||
schema: {
|
||||
description: 'Get all transactions',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
},
|
||||
fastify.get(
|
||||
'/transactions',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all transactions',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}, cashflowController.getAllTransactions.bind(cashflowController));
|
||||
cashflowController.getAllTransactions.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.get('/transactions/summary', {
|
||||
schema: {
|
||||
description: 'Get cashflow summary for date range',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
required: ['startDate', 'endDate'],
|
||||
properties: {
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
},
|
||||
fastify.get(
|
||||
'/transactions/summary',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get cashflow summary for date range',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
required: ['startDate', 'endDate'],
|
||||
properties: {
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}, cashflowController.getCashflowSummary.bind(cashflowController));
|
||||
cashflowController.getCashflowSummary.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.get('/transactions/:id', {
|
||||
schema: {
|
||||
description: 'Get transaction by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.get(
|
||||
'/transactions/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get transaction by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.getOneTransaction.bind(cashflowController));
|
||||
cashflowController.getOneTransaction.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.post('/transactions', {
|
||||
schema: {
|
||||
description: 'Create transaction',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['type', 'category', 'amount', 'date'],
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
category: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
date: {type: 'string', format: 'date-time'},
|
||||
description: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
fastify.post(
|
||||
'/transactions',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create transaction',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['type', 'category', 'amount', 'date'],
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
category: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
date: {type: 'string', format: 'date-time'},
|
||||
description: {type: 'string'},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}, cashflowController.createTransaction.bind(cashflowController));
|
||||
cashflowController.createTransaction.bind(cashflowController)
|
||||
);
|
||||
|
||||
fastify.delete('/transactions/:id', {
|
||||
schema: {
|
||||
description: 'Delete transaction',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
fastify.delete(
|
||||
'/transactions/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete transaction',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}]
|
||||
}
|
||||
},
|
||||
}, cashflowController.deleteTransaction.bind(cashflowController));
|
||||
cashflowController.deleteTransaction.bind(cashflowController)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
withStats: {
|
||||
type: 'string',
|
||||
enum: ['true', 'false'],
|
||||
description: 'Include invoice statistics for each client',
|
||||
},
|
||||
},
|
||||
description: 'Include invoice statistics for each client'
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -49,14 +49,14 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
address: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
clientController.getAll.bind(clientController)
|
||||
);
|
||||
@@ -76,11 +76,11 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
description: 'Total revenue',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalRevenue: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalRevenue: {type: 'number'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
clientController.getTotalRevenue.bind(clientController)
|
||||
);
|
||||
@@ -98,8 +98,8 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -116,13 +116,13 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
address: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
clientController.getOne.bind(clientController)
|
||||
);
|
||||
@@ -145,19 +145,19 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
email: {type: 'string', format: 'email'},
|
||||
phone: {type: 'string', maxLength: 50},
|
||||
address: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Client created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
client: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
clientController.create.bind(clientController)
|
||||
);
|
||||
@@ -175,8 +175,8 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -185,19 +185,19 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
email: {type: 'string', format: 'email'},
|
||||
phone: {type: 'string', maxLength: 50},
|
||||
address: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Client updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
client: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
clientController.update.bind(clientController)
|
||||
);
|
||||
@@ -215,16 +215,16 @@ export async function clientRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Client deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
clientController.delete.bind(clientController)
|
||||
);
|
||||
|
||||
@@ -5,11 +5,7 @@ import {AssetRepository} from '../repositories/AssetRepository';
|
||||
import {LiabilityRepository} from '../repositories/LiabilityRepository';
|
||||
import {InvoiceRepository} from '../repositories/InvoiceRepository';
|
||||
import {DebtAccountRepository} from '../repositories/DebtAccountRepository';
|
||||
import {
|
||||
IncomeSourceRepository,
|
||||
ExpenseRepository,
|
||||
TransactionRepository,
|
||||
} from '../repositories/CashflowRepository';
|
||||
import {IncomeSourceRepository, ExpenseRepository, TransactionRepository} from '../repositories/CashflowRepository';
|
||||
import {NetWorthSnapshotRepository} from '../repositories/NetWorthSnapshotRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
@@ -60,8 +56,8 @@ export async function dashboardRoutes(fastify: FastifyInstance) {
|
||||
assets: {type: 'number'},
|
||||
liabilities: {type: 'number'},
|
||||
change: {type: 'number'},
|
||||
lastUpdated: {type: 'string'},
|
||||
},
|
||||
lastUpdated: {type: 'string'}
|
||||
}
|
||||
},
|
||||
invoices: {
|
||||
type: 'object',
|
||||
@@ -69,15 +65,15 @@ export async function dashboardRoutes(fastify: FastifyInstance) {
|
||||
total: {type: 'number'},
|
||||
paid: {type: 'number'},
|
||||
outstanding: {type: 'number'},
|
||||
overdue: {type: 'number'},
|
||||
},
|
||||
overdue: {type: 'number'}
|
||||
}
|
||||
},
|
||||
debts: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {type: 'number'},
|
||||
accounts: {type: 'number'},
|
||||
},
|
||||
accounts: {type: 'number'}
|
||||
}
|
||||
},
|
||||
cashflow: {
|
||||
type: 'object',
|
||||
@@ -85,21 +81,21 @@ export async function dashboardRoutes(fastify: FastifyInstance) {
|
||||
monthlyIncome: {type: 'number'},
|
||||
monthlyExpenses: {type: 'number'},
|
||||
monthlyNet: {type: 'number'},
|
||||
last30Days: {type: 'object'},
|
||||
},
|
||||
last30Days: {type: 'object'}
|
||||
}
|
||||
},
|
||||
assets: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {type: 'number'},
|
||||
count: {type: 'number'},
|
||||
allocation: {type: 'array'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
allocation: {type: 'array'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dashboardController.getSummary.bind(dashboardController)
|
||||
);
|
||||
|
||||
@@ -42,9 +42,9 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
withStats: {
|
||||
type: 'string',
|
||||
enum: ['true', 'false'],
|
||||
description: 'Include statistics for each category',
|
||||
},
|
||||
},
|
||||
description: 'Include statistics for each category'
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -61,14 +61,14 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
description: {type: 'string', nullable: true},
|
||||
color: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
categoryController.getAll.bind(categoryController)
|
||||
);
|
||||
@@ -86,8 +86,8 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -102,13 +102,13 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
description: {type: 'string', nullable: true},
|
||||
color: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
categoryController.getOne.bind(categoryController)
|
||||
);
|
||||
@@ -129,19 +129,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
description: {type: 'string'},
|
||||
color: {type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'},
|
||||
},
|
||||
color: {type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Debt category created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
category: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
categoryController.create.bind(categoryController)
|
||||
);
|
||||
@@ -159,27 +159,27 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
description: {type: 'string'},
|
||||
color: {type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'},
|
||||
},
|
||||
color: {type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt category updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
category: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
categoryController.update.bind(categoryController)
|
||||
);
|
||||
@@ -197,16 +197,16 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Debt category deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
categoryController.delete.bind(categoryController)
|
||||
);
|
||||
@@ -227,19 +227,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withStats: {type: 'string', enum: ['true', 'false']},
|
||||
categoryId: {type: 'string', description: 'Filter by category ID'},
|
||||
},
|
||||
categoryId: {type: 'string', description: 'Filter by category ID'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of debt accounts',
|
||||
type: 'object',
|
||||
properties: {
|
||||
accounts: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accounts: {type: 'array', items: {type: 'object'}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
accountController.getAll.bind(accountController)
|
||||
);
|
||||
@@ -259,11 +259,11 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
description: 'Total debt',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalDebt: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalDebt: {type: 'number'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
accountController.getTotalDebt.bind(accountController)
|
||||
);
|
||||
@@ -281,19 +281,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt account details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
account: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
account: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
accountController.getOne.bind(accountController)
|
||||
);
|
||||
@@ -321,19 +321,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
interestRate: {type: 'number', minimum: 0, maximum: 100},
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Debt account created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
account: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
account: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
accountController.create.bind(accountController)
|
||||
);
|
||||
@@ -351,8 +351,8 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -364,19 +364,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
interestRate: {type: 'number', minimum: 0, maximum: 100},
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt account updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
account: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
account: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
accountController.update.bind(accountController)
|
||||
);
|
||||
@@ -394,16 +394,16 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Debt account deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
accountController.delete.bind(accountController)
|
||||
);
|
||||
@@ -425,19 +425,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
properties: {
|
||||
accountId: {type: 'string', description: 'Filter by account ID'},
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
endDate: {type: 'string', format: 'date-time'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of debt payments',
|
||||
type: 'object',
|
||||
properties: {
|
||||
payments: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
payments: {type: 'array', items: {type: 'object'}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
paymentController.getAll.bind(paymentController)
|
||||
);
|
||||
@@ -457,11 +457,11 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
description: 'Total payments',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalPayments: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalPayments: {type: 'number'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
paymentController.getTotalPayments.bind(paymentController)
|
||||
);
|
||||
@@ -479,19 +479,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt payment details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
payment: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
payment: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
paymentController.getOne.bind(paymentController)
|
||||
);
|
||||
@@ -513,19 +513,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
accountId: {type: 'string', format: 'uuid'},
|
||||
amount: {type: 'number', minimum: 0.01},
|
||||
paymentDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Debt payment created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
payment: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
payment: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
paymentController.create.bind(paymentController)
|
||||
);
|
||||
@@ -543,16 +543,16 @@ export async function debtRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Debt payment deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
paymentController.delete.bind(paymentController)
|
||||
);
|
||||
|
||||
@@ -28,8 +28,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clientId: {type: 'string', description: 'Filter by client ID'},
|
||||
status: {type: 'string', description: 'Filter by status'},
|
||||
},
|
||||
status: {type: 'string', description: 'Filter by status'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -52,14 +52,14 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
notes: {type: 'string', nullable: true},
|
||||
terms: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.getAll.bind(invoiceController)
|
||||
);
|
||||
@@ -85,13 +85,13 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
total: {type: 'number'},
|
||||
paid: {type: 'number'},
|
||||
outstanding: {type: 'number'},
|
||||
overdue: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
overdue: {type: 'number'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.getStats.bind(invoiceController)
|
||||
);
|
||||
@@ -111,11 +111,11 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
description: 'List of overdue invoices',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoices: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoices: {type: 'array', items: {type: 'object'}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.getOverdue.bind(invoiceController)
|
||||
);
|
||||
@@ -133,8 +133,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -155,13 +155,13 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
notes: {type: 'string', nullable: true},
|
||||
terms: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.getOne.bind(invoiceController)
|
||||
);
|
||||
@@ -193,24 +193,24 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
description: {type: 'string', minLength: 1},
|
||||
quantity: {type: 'number', minimum: 1},
|
||||
unitPrice: {type: 'number', minimum: 0},
|
||||
amount: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
amount: {type: 'number', minimum: 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
notes: {type: 'string'},
|
||||
terms: {type: 'string'},
|
||||
},
|
||||
terms: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Invoice created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoice: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.create.bind(invoiceController)
|
||||
);
|
||||
@@ -228,8 +228,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -246,24 +246,24 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
description: {type: 'string', minLength: 1},
|
||||
quantity: {type: 'number', minimum: 1},
|
||||
unitPrice: {type: 'number', minimum: 0},
|
||||
amount: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
amount: {type: 'number', minimum: 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
notes: {type: 'string'},
|
||||
terms: {type: 'string'},
|
||||
},
|
||||
terms: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Invoice updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoice: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.update.bind(invoiceController)
|
||||
);
|
||||
@@ -281,8 +281,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -290,20 +290,20 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED'],
|
||||
},
|
||||
},
|
||||
enum: ['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED']
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Invoice status updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoice: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.updateStatus.bind(invoiceController)
|
||||
);
|
||||
@@ -321,16 +321,16 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Invoice deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
invoiceController.delete.bind(invoiceController)
|
||||
);
|
||||
|
||||
@@ -42,14 +42,14 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
creditor: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
liabilityController.getAll.bind(liabilityController)
|
||||
);
|
||||
@@ -69,11 +69,11 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
description: 'Total liability value',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalValue: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalValue: {type: 'number'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
liabilityController.getTotalValue.bind(liabilityController)
|
||||
);
|
||||
@@ -97,13 +97,13 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
items: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
liabilityController.getByType.bind(liabilityController)
|
||||
);
|
||||
@@ -121,8 +121,8 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -142,13 +142,13 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
creditor: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
updatedAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
liabilityController.getOne.bind(liabilityController)
|
||||
);
|
||||
@@ -174,19 +174,19 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
creditor: {type: 'string', maxLength: 255},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Liability created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
liability: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liability: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
liabilityController.create.bind(liabilityController)
|
||||
);
|
||||
@@ -204,8 +204,8 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
@@ -217,19 +217,19 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
creditor: {type: 'string', maxLength: 255},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Liability updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
liability: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liability: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
liabilityController.update.bind(liabilityController)
|
||||
);
|
||||
@@ -247,16 +247,16 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Liability deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
liabilityController.delete.bind(liabilityController)
|
||||
);
|
||||
|
||||
@@ -35,11 +35,11 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
totalLiabilities: {type: 'number'},
|
||||
netWorth: {type: 'number'},
|
||||
asOf: {type: 'string'},
|
||||
isCalculated: {type: 'boolean'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isCalculated: {type: 'boolean'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.getCurrent.bind(netWorthController)
|
||||
);
|
||||
@@ -70,14 +70,14 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
totalLiabilities: {type: 'number'},
|
||||
netWorth: {type: 'number'},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: {type: 'string'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.getAllSnapshots.bind(netWorthController)
|
||||
);
|
||||
@@ -97,19 +97,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
required: ['startDate', 'endDate'],
|
||||
properties: {
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
endDate: {type: 'string', format: 'date-time'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Snapshots in date range',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshots: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshots: {type: 'array', items: {type: 'object'}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.getByDateRange.bind(netWorthController)
|
||||
);
|
||||
@@ -127,8 +127,8 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {type: 'string', description: 'Number of periods to include (default: 12)'},
|
||||
},
|
||||
limit: {type: 'string', description: 'Number of periods to include (default: 12)'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
@@ -143,14 +143,14 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
date: {type: 'string'},
|
||||
netWorth: {type: 'number'},
|
||||
growthAmount: {type: 'number'},
|
||||
growthPercent: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
growthPercent: {type: 'number'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.getGrowthStats.bind(netWorthController)
|
||||
);
|
||||
@@ -168,19 +168,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Snapshot details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshot: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshot: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.getOne.bind(netWorthController)
|
||||
);
|
||||
@@ -203,19 +203,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
totalAssets: {type: 'number', minimum: 0},
|
||||
totalLiabilities: {type: 'number', minimum: 0},
|
||||
netWorth: {type: 'number'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Snapshot created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshot: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshot: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.createSnapshot.bind(netWorthController)
|
||||
);
|
||||
@@ -233,19 +233,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
notes: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Snapshot created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshot: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
snapshot: {type: 'object'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.createFromCurrent.bind(netWorthController)
|
||||
);
|
||||
@@ -263,16 +263,16 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
id: {type: 'string'}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Snapshot deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'null'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
netWorthController.delete.bind(netWorthController)
|
||||
);
|
||||
|
||||
@@ -20,24 +20,28 @@ import {dashboardRoutes} from './routes/dashboard.routes';
|
||||
* Implements Single Responsibility: Server configuration
|
||||
*/
|
||||
export async function buildServer() {
|
||||
if (env.NODE_ENV !== 'production') {
|
||||
console.log('Development mode enabled. Environment variables [%o]', env);
|
||||
}
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
level: env.NODE_ENV === 'development' ? 'info' : 'error',
|
||||
transport: env.NODE_ENV === 'development' ? {target: 'pino-pretty'} : undefined,
|
||||
},
|
||||
transport: env.NODE_ENV === 'development' ? {target: 'pino-pretty'} : undefined
|
||||
}
|
||||
});
|
||||
|
||||
// Register plugins
|
||||
await fastify.register(cors, {
|
||||
origin: env.CORS_ORIGIN,
|
||||
credentials: true,
|
||||
credentials: true
|
||||
});
|
||||
|
||||
await fastify.register(jwt, {
|
||||
secret: env.JWT_SECRET,
|
||||
sign: {
|
||||
expiresIn: env.JWT_EXPIRES_IN,
|
||||
},
|
||||
expiresIn: env.JWT_EXPIRES_IN
|
||||
}
|
||||
});
|
||||
|
||||
// Register Swagger for API documentation
|
||||
@@ -46,32 +50,32 @@ export async function buildServer() {
|
||||
info: {
|
||||
title: 'Personal Finances API',
|
||||
description: 'API for managing personal finances including assets, liabilities, invoices, and more',
|
||||
version: '1.0.0',
|
||||
version: '1.0.0'
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: `http://localhost:${env.PORT}`,
|
||||
description: 'Development server',
|
||||
},
|
||||
description: 'Development server'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bearerFormat: 'JWT'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await fastify.register(swaggerUi, {
|
||||
routePrefix: '/docs',
|
||||
uiConfig: {
|
||||
docExpansion: 'list',
|
||||
deepLinking: false,
|
||||
},
|
||||
deepLinking: false
|
||||
}
|
||||
});
|
||||
|
||||
// Register error handler
|
||||
@@ -80,7 +84,7 @@ export async function buildServer() {
|
||||
// Health check
|
||||
fastify.get('/health', async () => ({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
|
||||
// Register routes
|
||||
|
||||
@@ -44,7 +44,7 @@ export class AssetService {
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
value: data.value,
|
||||
user: {connect: {id: userId}},
|
||||
user: {connect: {id: userId}}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export class AssetService {
|
||||
this.validateAssetData({
|
||||
name: data.name || asset.name,
|
||||
type: (data.type || asset.type) as AssetType,
|
||||
value: data.value !== undefined ? data.value : asset.value,
|
||||
value: data.value !== undefined ? data.value : asset.value
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,14 +80,17 @@ export class AssetService {
|
||||
|
||||
async getByType(userId: string): Promise<Record<string, Asset[]>> {
|
||||
const assets = await this.assetRepository.findAllByUser(userId);
|
||||
return assets.reduce((acc, asset) => {
|
||||
const type = asset.type;
|
||||
if (!acc[type]) {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(asset);
|
||||
return acc;
|
||||
}, {} as Record<string, Asset[]>);
|
||||
return assets.reduce(
|
||||
(acc, asset) => {
|
||||
const type = asset.type;
|
||||
if (!acc[type]) {
|
||||
acc[type] = [];
|
||||
}
|
||||
acc[type].push(asset);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Asset[]>
|
||||
);
|
||||
}
|
||||
|
||||
private validateAssetData(data: CreateAssetDTO): void {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AuthService {
|
||||
const user = await this.userRepository.create({
|
||||
email,
|
||||
password: hashedPassword,
|
||||
name,
|
||||
name
|
||||
});
|
||||
|
||||
// Create default debt categories for new user
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {IncomeSource, Expense, Transaction} from '@prisma/client';
|
||||
import {
|
||||
IncomeSourceRepository,
|
||||
ExpenseRepository,
|
||||
TransactionRepository,
|
||||
} from '../repositories/CashflowRepository';
|
||||
import {IncomeSourceRepository, ExpenseRepository, TransactionRepository} from '../repositories/CashflowRepository';
|
||||
import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
|
||||
|
||||
export interface CreateIncomeSourceDTO {
|
||||
@@ -47,7 +43,7 @@ export class CashflowService {
|
||||
|
||||
return this.incomeRepository.create({
|
||||
...data,
|
||||
user: {connect: {id: userId}},
|
||||
user: {connect: {id: userId}}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,7 +81,7 @@ export class CashflowService {
|
||||
|
||||
return this.expenseRepository.create({
|
||||
...data,
|
||||
user: {connect: {id: userId}},
|
||||
user: {connect: {id: userId}}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,7 +124,7 @@ export class CashflowService {
|
||||
|
||||
return this.transactionRepository.create({
|
||||
...data,
|
||||
user: {connect: {id: userId}},
|
||||
user: {connect: {id: userId}}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ export class ClientService {
|
||||
address: data.address,
|
||||
notes: data.notes,
|
||||
user: {
|
||||
connect: {id: userId},
|
||||
},
|
||||
connect: {id: userId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,18 +48,14 @@ export class DashboardService {
|
||||
// Get recent transactions (last 30 days)
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
const recentCashflow = await this.transactionRepository.getCashflowSummary(
|
||||
userId,
|
||||
thirtyDaysAgo,
|
||||
new Date()
|
||||
);
|
||||
const recentCashflow = await this.transactionRepository.getCashflowSummary(userId, thirtyDaysAgo, new Date());
|
||||
|
||||
// Get assets by type
|
||||
const assetsByType = await this.assetRepository.getByType(userId);
|
||||
const assetAllocation = Object.entries(assetsByType).map(([type, assets]) => ({
|
||||
type,
|
||||
count: assets.length,
|
||||
totalValue: assets.reduce((sum, asset) => sum + asset.currentValue, 0),
|
||||
totalValue: assets.reduce((sum, asset) => sum + asset.currentValue, 0)
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -68,29 +64,29 @@ export class DashboardService {
|
||||
assets: totalAssets,
|
||||
liabilities: totalLiabilities,
|
||||
change: netWorthChange,
|
||||
lastUpdated: new Date(),
|
||||
lastUpdated: new Date()
|
||||
},
|
||||
invoices: {
|
||||
total: invoiceStats.totalInvoices,
|
||||
paid: invoiceStats.paidInvoices,
|
||||
outstanding: invoiceStats.outstandingAmount,
|
||||
overdue: invoiceStats.overdueInvoices,
|
||||
overdue: invoiceStats.overdueInvoices
|
||||
},
|
||||
debts: {
|
||||
total: totalDebt,
|
||||
accounts: (await this.debtAccountRepository.findAllByUser(userId)).length,
|
||||
accounts: (await this.debtAccountRepository.findAllByUser(userId)).length
|
||||
},
|
||||
cashflow: {
|
||||
monthlyIncome: totalMonthlyIncome,
|
||||
monthlyExpenses: totalMonthlyExpenses,
|
||||
monthlyNet: monthlyCashflow,
|
||||
last30Days: recentCashflow,
|
||||
last30Days: recentCashflow
|
||||
},
|
||||
assets: {
|
||||
total: totalAssets,
|
||||
count: (await this.assetRepository.findAllByUser(userId)).length,
|
||||
allocation: assetAllocation,
|
||||
},
|
||||
allocation: assetAllocation
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ export class DebtAccountService {
|
||||
dueDate: data.dueDate,
|
||||
notes: data.notes,
|
||||
category: {
|
||||
connect: {id: data.categoryId},
|
||||
},
|
||||
connect: {id: data.categoryId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export class DebtCategoryService {
|
||||
{name: 'Auto Loans', description: 'Car and vehicle loans', color: '#10b981'},
|
||||
{name: 'Mortgages', description: 'Home mortgages', color: '#f59e0b'},
|
||||
{name: 'Personal Loans', description: 'Personal loan debts', color: '#8b5cf6'},
|
||||
{name: 'Other', description: 'Other debt types', color: '#6b7280'},
|
||||
{name: 'Other', description: 'Other debt types', color: '#6b7280'}
|
||||
];
|
||||
|
||||
const categories: DebtCategory[] = [];
|
||||
@@ -43,8 +43,8 @@ export class DebtCategoryService {
|
||||
description: category.description,
|
||||
color: category.color,
|
||||
user: {
|
||||
connect: {id: userId},
|
||||
},
|
||||
connect: {id: userId}
|
||||
}
|
||||
});
|
||||
categories.push(created);
|
||||
}
|
||||
@@ -69,8 +69,8 @@ export class DebtCategoryService {
|
||||
description: data.description,
|
||||
color: data.color,
|
||||
user: {
|
||||
connect: {id: userId},
|
||||
},
|
||||
connect: {id: userId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,14 +42,14 @@ export class DebtPaymentService {
|
||||
paymentDate: data.paymentDate,
|
||||
notes: data.notes,
|
||||
account: {
|
||||
connect: {id: data.accountId},
|
||||
},
|
||||
connect: {id: data.accountId}
|
||||
}
|
||||
});
|
||||
|
||||
// Update account current balance
|
||||
const newBalance = account.currentBalance - data.amount;
|
||||
await this.accountRepository.update(data.accountId, {
|
||||
currentBalance: Math.max(0, newBalance), // Don't allow negative balance
|
||||
currentBalance: Math.max(0, newBalance) // Don't allow negative balance
|
||||
});
|
||||
|
||||
return payment;
|
||||
@@ -114,7 +114,7 @@ export class DebtPaymentService {
|
||||
if (account) {
|
||||
const newBalance = account.currentBalance + payment.amount;
|
||||
await this.accountRepository.update(payment.accountId, {
|
||||
currentBalance: newBalance,
|
||||
currentBalance: newBalance
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -66,8 +66,7 @@ export class InvoiceService {
|
||||
this.validateInvoiceData(data);
|
||||
|
||||
// Generate invoice number if not provided
|
||||
const invoiceNumber =
|
||||
data.invoiceNumber || (await this.invoiceRepository.generateInvoiceNumber(userId));
|
||||
const invoiceNumber = data.invoiceNumber || (await this.invoiceRepository.generateInvoiceNumber(userId));
|
||||
|
||||
// Check if invoice number already exists
|
||||
const exists = await this.invoiceRepository.invoiceNumberExists(userId, invoiceNumber);
|
||||
@@ -80,7 +79,7 @@ export class InvoiceService {
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.unitPrice,
|
||||
total: item.quantity * item.unitPrice,
|
||||
total: item.quantity * item.unitPrice
|
||||
}));
|
||||
|
||||
const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0);
|
||||
@@ -99,8 +98,8 @@ export class InvoiceService {
|
||||
user: {connect: {id: userId}},
|
||||
client: {connect: {id: data.clientId}},
|
||||
lineItems: {
|
||||
create: lineItems,
|
||||
},
|
||||
create: lineItems
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,7 +126,7 @@ export class InvoiceService {
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.unitPrice,
|
||||
total: item.quantity * item.unitPrice,
|
||||
total: item.quantity * item.unitPrice
|
||||
}));
|
||||
|
||||
const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0);
|
||||
@@ -137,7 +136,7 @@ export class InvoiceService {
|
||||
updateData.total = total;
|
||||
updateData.lineItems = {
|
||||
deleteMany: {},
|
||||
create: lineItems,
|
||||
create: lineItems
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,7 +154,7 @@ export class InvoiceService {
|
||||
|
||||
async getStats(userId: string): Promise<InvoiceStats> {
|
||||
const invoices = await this.invoiceRepository.findAllByUser(userId);
|
||||
|
||||
|
||||
const stats: InvoiceStats = {
|
||||
total: invoices.length,
|
||||
draft: 0,
|
||||
@@ -164,12 +163,12 @@ export class InvoiceService {
|
||||
overdue: 0,
|
||||
totalAmount: 0,
|
||||
paidAmount: 0,
|
||||
outstandingAmount: 0,
|
||||
outstandingAmount: 0
|
||||
};
|
||||
|
||||
for (const inv of invoices) {
|
||||
stats.totalAmount += inv.total;
|
||||
|
||||
|
||||
switch (inv.status) {
|
||||
case 'draft':
|
||||
stats.draft++;
|
||||
|
||||
@@ -48,8 +48,8 @@ export class LiabilityService {
|
||||
creditor: data.creditor,
|
||||
notes: data.notes,
|
||||
user: {
|
||||
connect: {id: userId},
|
||||
},
|
||||
connect: {id: userId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -40,9 +40,7 @@ export class NetWorthService {
|
||||
const calculatedNetWorth = data.totalAssets - data.totalLiabilities;
|
||||
if (Math.abs(calculatedNetWorth - data.netWorth) > 0.01) {
|
||||
// Allow small floating point differences
|
||||
throw new ValidationError(
|
||||
`Net worth calculation mismatch. Expected ${calculatedNetWorth}, got ${data.netWorth}`
|
||||
);
|
||||
throw new ValidationError(`Net worth calculation mismatch. Expected ${calculatedNetWorth}, got ${data.netWorth}`);
|
||||
}
|
||||
|
||||
return this.snapshotRepository.create({
|
||||
@@ -52,8 +50,8 @@ export class NetWorthService {
|
||||
netWorth: data.netWorth,
|
||||
notes: data.notes,
|
||||
user: {
|
||||
connect: {id: userId},
|
||||
},
|
||||
connect: {id: userId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,7 +68,7 @@ export class NetWorthService {
|
||||
totalAssets,
|
||||
totalLiabilities,
|
||||
netWorth,
|
||||
notes,
|
||||
notes
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,11 +82,7 @@ export class NetWorthService {
|
||||
/**
|
||||
* Get snapshots within a date range
|
||||
*/
|
||||
async getSnapshotsByDateRange(
|
||||
userId: string,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<NetWorthSnapshot[]> {
|
||||
async getSnapshotsByDateRange(userId: string, startDate: Date, endDate: Date): Promise<NetWorthSnapshot[]> {
|
||||
return this.snapshotRepository.getByDateRange(userId, startDate, endDate);
|
||||
}
|
||||
|
||||
@@ -106,8 +100,7 @@ export class NetWorthService {
|
||||
|
||||
// If we have a recent snapshot (within last 24 hours), use it
|
||||
if (latestSnapshot) {
|
||||
const hoursSinceSnapshot =
|
||||
(Date.now() - latestSnapshot.date.getTime()) / (1000 * 60 * 60);
|
||||
const hoursSinceSnapshot = (Date.now() - latestSnapshot.date.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
if (hoursSinceSnapshot < 24) {
|
||||
return {
|
||||
@@ -115,7 +108,7 @@ export class NetWorthService {
|
||||
totalLiabilities: latestSnapshot.totalLiabilities,
|
||||
netWorth: latestSnapshot.netWorth,
|
||||
asOf: latestSnapshot.date,
|
||||
isCalculated: false,
|
||||
isCalculated: false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -129,7 +122,7 @@ export class NetWorthService {
|
||||
totalLiabilities,
|
||||
netWorth: totalAssets - totalLiabilities,
|
||||
asOf: new Date(),
|
||||
isCalculated: true,
|
||||
isCalculated: true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1
backend-api/src/types/fastify.d.ts
vendored
1
backend-api/src/types/fastify.d.ts
vendored
@@ -12,4 +12,3 @@ declare module '@fastify/jwt' {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export class PasswordService {
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
errors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
355
bun.lock
Normal file
355
bun.lock
Normal file
@@ -0,0 +1,355 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "personal-finances",
|
||||
"dependencies": {
|
||||
"concurrently": "^9.2.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.49.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/type-utils": "8.49.0", "@typescript-eslint/utils": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.49.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.49.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.49.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.49.0", "@typescript-eslint/types": "^8.49.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0" } }, "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.49.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/utils": "8.49.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.49.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.49.0", "@typescript-eslint/tsconfig-utils": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.49.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.6", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.24", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
||||
|
||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": "cli.js" }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.49.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.49.0", "@typescript-eslint/parser": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/utils": "8.49.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
}
|
||||
}
|
||||
1
frontend-web/.env.example
Normal file
1
frontend-web/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_URL=http://localhost:3000
|
||||
4
frontend-web/.gitignore
vendored
4
frontend-web/.gitignore
vendored
@@ -12,6 +12,10 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {lazy, Suspense} from 'react';
|
||||
import {lazy, Suspense, useEffect} from 'react';
|
||||
import {BrowserRouter, Routes, Route, Navigate} from 'react-router-dom';
|
||||
import {useAppSelector} from '@/store';
|
||||
import {useAppSelector, useAppDispatch} from '@/store';
|
||||
import {loadUserFromStorage} from '@/store/slices/userSlice';
|
||||
import {fetchAssets, fetchLiabilities, fetchSnapshots} from '@/store/slices/netWorthSlice';
|
||||
import {fetchIncomeSources, fetchExpenses, fetchTransactions} from '@/store/slices/cashflowSlice';
|
||||
import Layout from '@/components/Layout';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
|
||||
@@ -20,7 +23,30 @@ const PageLoader = () => (
|
||||
);
|
||||
|
||||
function AppRoutes() {
|
||||
const {isAuthenticated} = useAppSelector(state => state.user);
|
||||
const dispatch = useAppDispatch();
|
||||
const {isAuthenticated, isLoading} = useAppSelector(state => state.user);
|
||||
|
||||
// Load user from storage on app start
|
||||
useEffect(() => {
|
||||
dispatch(loadUserFromStorage());
|
||||
}, [dispatch]);
|
||||
|
||||
// Fetch all data when user is authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
dispatch(fetchAssets());
|
||||
dispatch(fetchLiabilities());
|
||||
dispatch(fetchSnapshots());
|
||||
dispatch(fetchIncomeSources());
|
||||
dispatch(fetchExpenses());
|
||||
dispatch(fetchTransactions());
|
||||
}
|
||||
}, [isAuthenticated, dispatch]);
|
||||
|
||||
// Show loading while checking authentication
|
||||
if (isLoading) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, addAsset} from '@/store';
|
||||
import {useAppDispatch, createAsset} from '@/store';
|
||||
import {validatePositiveNumber, validateRequired, sanitizeString} from '@/lib/validation';
|
||||
|
||||
interface Props {
|
||||
@@ -46,12 +46,10 @@ export default function AddAssetDialog({open, onOpenChange}: Props) {
|
||||
if (valueNum === null) return;
|
||||
|
||||
dispatch(
|
||||
addAsset({
|
||||
id: crypto.randomUUID(),
|
||||
createAsset({
|
||||
name: sanitizeString(form.name),
|
||||
type: form.type as (typeof assetTypes)[number],
|
||||
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
|
||||
value: valueNum,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
);
|
||||
onOpenChange(false);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, addLiability} from '@/store';
|
||||
import {useAppDispatch, createLiability} from '@/store';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -20,12 +20,10 @@ export default function AddLiabilityDialog({open, onOpenChange}: Props) {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
dispatch(
|
||||
addLiability({
|
||||
id: crypto.randomUUID(),
|
||||
createLiability({
|
||||
name: form.name,
|
||||
type: form.type as (typeof liabilityTypes)[number],
|
||||
balance: parseFloat(form.balance) || 0,
|
||||
updatedAt: new Date().toISOString()
|
||||
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
|
||||
currentBalance: parseFloat(form.balance) || 0,
|
||||
})
|
||||
);
|
||||
onOpenChange(false);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, updateAsset, removeAsset, type Asset} from '@/store';
|
||||
import {useAppDispatch, updateAsset, deleteAsset, type Asset} from '@/store';
|
||||
import {validatePositiveNumber, validateRequired} from '@/lib/validation';
|
||||
|
||||
interface Props {
|
||||
@@ -60,10 +60,11 @@ export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
|
||||
dispatch(
|
||||
updateAsset({
|
||||
id: asset.id,
|
||||
name: form.name.trim(),
|
||||
type: form.type as (typeof assetTypes)[number],
|
||||
value: valueNum,
|
||||
updatedAt: new Date().toISOString()
|
||||
data: {
|
||||
name: form.name.trim(),
|
||||
type: form.type.toUpperCase() as 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER',
|
||||
value: valueNum,
|
||||
}
|
||||
})
|
||||
);
|
||||
onOpenChange(false);
|
||||
@@ -72,7 +73,7 @@ export default function EditAssetDialog({open, onOpenChange, asset}: Props) {
|
||||
const handleDelete = () => {
|
||||
if (!asset) return;
|
||||
if (confirm(`Are you sure you want to delete "${asset.name}"?`)) {
|
||||
dispatch(removeAsset(asset.id));
|
||||
dispatch(deleteAsset(asset.id));
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, updateLiability, removeLiability, type Liability} from '@/store';
|
||||
import {useAppDispatch, updateLiability, deleteLiability, type Liability} from '@/store';
|
||||
import {validatePositiveNumber, validateRequired} from '@/lib/validation';
|
||||
|
||||
interface Props {
|
||||
@@ -60,10 +60,11 @@ export default function EditLiabilityDialog({open, onOpenChange, liability}: Pro
|
||||
dispatch(
|
||||
updateLiability({
|
||||
id: liability.id,
|
||||
name: form.name.trim(),
|
||||
type: form.type as (typeof liabilityTypes)[number],
|
||||
balance: balanceNum,
|
||||
updatedAt: new Date().toISOString()
|
||||
data: {
|
||||
name: form.name.trim(),
|
||||
type: form.type.toUpperCase() as 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER',
|
||||
currentBalance: balanceNum,
|
||||
}
|
||||
})
|
||||
);
|
||||
onOpenChange(false);
|
||||
@@ -72,7 +73,7 @@ export default function EditLiabilityDialog({open, onOpenChange, liability}: Pro
|
||||
const handleDelete = () => {
|
||||
if (!liability) return;
|
||||
if (confirm(`Are you sure you want to delete "${liability.name}"?`)) {
|
||||
dispatch(removeLiability(liability.id));
|
||||
dispatch(deleteLiability(liability.id));
|
||||
onOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, Di
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, setUser} from '@/store';
|
||||
import {useAppDispatch} from '@/store';
|
||||
import {loginUser} from '@/store/slices/userSlice';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -18,26 +19,33 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
|
||||
password: '',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
// Mock login - in production this would validate against an API
|
||||
if (!form.email || !form.password) {
|
||||
setError('Please enter your email and password');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock successful login
|
||||
dispatch(setUser({
|
||||
id: crypto.randomUUID(),
|
||||
email: form.email,
|
||||
name: form.email.split('@')[0],
|
||||
}));
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await dispatch(
|
||||
loginUser({
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
onOpenChange(false);
|
||||
setForm({email: '', password: ''});
|
||||
onOpenChange(false);
|
||||
setForm({email: '', password: ''});
|
||||
} catch (err: any) {
|
||||
setError(err || 'Login failed. Please check your credentials.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -76,8 +84,10 @@ export default function LoginDialog({open, onOpenChange, onSwitchToSignUp}: Prop
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
</div>
|
||||
<DialogFooter className="flex-col gap-2 sm:flex-col">
|
||||
<Button type="submit" className="w-full">Log in</Button>
|
||||
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToSignUp}>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Log in'}
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToSignUp} disabled={isLoading}>
|
||||
Don't have an account? Sign up
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -3,7 +3,8 @@ import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, Di
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, setUser} from '@/store';
|
||||
import {useAppDispatch} from '@/store';
|
||||
import {registerUser} from '@/store/slices/userSlice';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -20,8 +21,9 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
@@ -30,20 +32,28 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.password.length < 6) {
|
||||
setError('Password must be at least 6 characters');
|
||||
if (form.password.length < 8) {
|
||||
setError('Password must be at least 8 characters');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock sign up - in production this would call an API
|
||||
dispatch(setUser({
|
||||
id: crypto.randomUUID(),
|
||||
email: form.email,
|
||||
name: form.name,
|
||||
}));
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await dispatch(
|
||||
registerUser({
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
name: form.name,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
onOpenChange(false);
|
||||
setForm({name: '', email: '', password: '', confirmPassword: ''});
|
||||
onOpenChange(false);
|
||||
setForm({name: '', email: '', password: '', confirmPassword: ''});
|
||||
} catch (err: any) {
|
||||
setError(err || 'Registration failed. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -109,8 +119,10 @@ export default function SignUpDialog({open, onOpenChange, onSwitchToLogin}: Prop
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="flex-col gap-2 sm:flex-col">
|
||||
<Button type="submit" className="w-full">Create account</Button>
|
||||
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToLogin}>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Creating account...' : 'Create account'}
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" className="w-full" onClick={onSwitchToLogin} disabled={isLoading}>
|
||||
Already have an account? Log in
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
71
frontend-web/src/lib/api/auth.service.ts
Normal file
71
frontend-web/src/lib/api/auth.service.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Authentication Service
|
||||
*/
|
||||
|
||||
import {apiClient} from './client';
|
||||
import {tokenStorage} from './token';
|
||||
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
token: string;
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const authService = {
|
||||
async register(data: RegisterRequest): Promise<AuthResponse> {
|
||||
const response = await apiClient.post<AuthResponse>('/register', data);
|
||||
tokenStorage.setToken(response.token);
|
||||
tokenStorage.setUser(JSON.stringify(response.user));
|
||||
return response;
|
||||
},
|
||||
|
||||
async login(data: LoginRequest): Promise<AuthResponse> {
|
||||
const response = await apiClient.post<AuthResponse>('/login', data);
|
||||
tokenStorage.setToken(response.token);
|
||||
tokenStorage.setUser(JSON.stringify(response.user));
|
||||
return response;
|
||||
},
|
||||
|
||||
async getProfile(): Promise<UserProfile> {
|
||||
return apiClient.get<UserProfile>('/profile');
|
||||
},
|
||||
|
||||
logout(): void {
|
||||
tokenStorage.clear();
|
||||
},
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return !!tokenStorage.getToken();
|
||||
},
|
||||
|
||||
getCurrentUser(): {id: string; email: string; name: string} | null {
|
||||
const userStr = tokenStorage.getUser();
|
||||
if (!userStr) return null;
|
||||
try {
|
||||
return JSON.parse(userStr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
88
frontend-web/src/lib/api/cashflow.service.ts
Normal file
88
frontend-web/src/lib/api/cashflow.service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Cashflow Service
|
||||
*/
|
||||
|
||||
import {apiClient} from './client';
|
||||
|
||||
export interface IncomeSource {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
frequency: 'WEEKLY' | 'BIWEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'YEARLY' | 'ONCE';
|
||||
notes?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Expense {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
frequency: 'WEEKLY' | 'BIWEEKLY' | 'MONTHLY' | 'QUARTERLY' | 'YEARLY' | 'ONCE';
|
||||
category?: string;
|
||||
isEssential?: boolean;
|
||||
notes?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
type: 'INCOME' | 'EXPENSE';
|
||||
amount: number;
|
||||
description: string;
|
||||
category?: string;
|
||||
date: string;
|
||||
notes?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export const incomeService = {
|
||||
async getAll(): Promise<{incomeSources: IncomeSource[]}> {
|
||||
return apiClient.get<{incomeSources: IncomeSource[]}>('/cashflow/income');
|
||||
},
|
||||
|
||||
async create(data: Partial<IncomeSource>): Promise<{incomeSource: IncomeSource}> {
|
||||
return apiClient.post<{incomeSource: IncomeSource}>('/cashflow/income', data);
|
||||
},
|
||||
|
||||
async update(id: string, data: Partial<IncomeSource>): Promise<{incomeSource: IncomeSource}> {
|
||||
return apiClient.put<{incomeSource: IncomeSource}>(`/cashflow/income/${id}`, data);
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return apiClient.delete<void>(`/cashflow/income/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const expenseService = {
|
||||
async getAll(): Promise<{expenses: Expense[]}> {
|
||||
return apiClient.get<{expenses: Expense[]}>('/cashflow/expenses');
|
||||
},
|
||||
|
||||
async create(data: Partial<Expense>): Promise<{expense: Expense}> {
|
||||
return apiClient.post<{expense: Expense}>('/cashflow/expenses', data);
|
||||
},
|
||||
|
||||
async update(id: string, data: Partial<Expense>): Promise<{expense: Expense}> {
|
||||
return apiClient.put<{expense: Expense}>(`/cashflow/expenses/${id}`, data);
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return apiClient.delete<void>(`/cashflow/expenses/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const transactionService = {
|
||||
async getAll(): Promise<{transactions: Transaction[]}> {
|
||||
return apiClient.get<{transactions: Transaction[]}>('/cashflow/transactions');
|
||||
},
|
||||
|
||||
async create(data: Partial<Transaction>): Promise<{transaction: Transaction}> {
|
||||
return apiClient.post<{transaction: Transaction}>('/cashflow/transactions', data);
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return apiClient.delete<void>(`/cashflow/transactions/${id}`);
|
||||
},
|
||||
};
|
||||
110
frontend-web/src/lib/api/client.ts
Normal file
110
frontend-web/src/lib/api/client.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* API Client
|
||||
* Base configuration for all API requests
|
||||
*/
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
||||
|
||||
export interface ApiError {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
private baseURL: string;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
|
||||
private getAuthToken(): string | null {
|
||||
return localStorage.getItem('auth_token');
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${this.baseURL}${endpoint}`;
|
||||
const token = this.getAuthToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
// Handle non-JSON responses
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
if (!response.ok) {
|
||||
throw {
|
||||
message: 'Request failed',
|
||||
statusCode: response.status,
|
||||
error: response.statusText,
|
||||
} as ApiError;
|
||||
}
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw {
|
||||
message: data.message || 'Request failed',
|
||||
statusCode: response.status,
|
||||
error: data.error,
|
||||
} as ApiError;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
if ((error as ApiError).statusCode) {
|
||||
throw error;
|
||||
}
|
||||
throw {
|
||||
message: 'Network error',
|
||||
statusCode: 0,
|
||||
error: String(error),
|
||||
} as ApiError;
|
||||
}
|
||||
}
|
||||
|
||||
async get<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, {method: 'GET'});
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async put<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, {method: 'DELETE'});
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient(API_URL);
|
||||
144
frontend-web/src/lib/api/networth.service.ts
Normal file
144
frontend-web/src/lib/api/networth.service.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Net Worth Service
|
||||
*/
|
||||
|
||||
import {apiClient} from './client';
|
||||
|
||||
export interface Asset {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER';
|
||||
value: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateAssetRequest {
|
||||
name: string;
|
||||
type: 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER';
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface UpdateAssetRequest {
|
||||
name?: string;
|
||||
type?: 'CASH' | 'INVESTMENT' | 'PROPERTY' | 'VEHICLE' | 'OTHER';
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export interface Liability {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER';
|
||||
currentBalance: number;
|
||||
interestRate?: number;
|
||||
minimumPayment?: number;
|
||||
dueDate?: string;
|
||||
creditor?: string;
|
||||
notes?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateLiabilityRequest {
|
||||
name: string;
|
||||
type: 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER';
|
||||
currentBalance: number;
|
||||
interestRate?: number;
|
||||
minimumPayment?: number;
|
||||
dueDate?: string;
|
||||
creditor?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface UpdateLiabilityRequest {
|
||||
name?: string;
|
||||
type?: 'CREDIT_CARD' | 'LOAN' | 'MORTGAGE' | 'OTHER';
|
||||
currentBalance?: number;
|
||||
interestRate?: number;
|
||||
minimumPayment?: number;
|
||||
dueDate?: string;
|
||||
creditor?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface NetWorthSnapshot {
|
||||
id: string;
|
||||
date: string;
|
||||
totalAssets: number;
|
||||
totalLiabilities: number;
|
||||
netWorth: number;
|
||||
notes?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export const assetService = {
|
||||
async getAll(): Promise<{assets: Asset[]}> {
|
||||
return apiClient.get<{assets: Asset[]}>('/assets');
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<{asset: Asset}> {
|
||||
return apiClient.get<{asset: Asset}>(`/assets/${id}`);
|
||||
},
|
||||
|
||||
async create(data: CreateAssetRequest): Promise<{asset: Asset}> {
|
||||
return apiClient.post<{asset: Asset}>('/assets', data);
|
||||
},
|
||||
|
||||
async update(id: string, data: UpdateAssetRequest): Promise<{asset: Asset}> {
|
||||
return apiClient.put<{asset: Asset}>(`/assets/${id}`, data);
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return apiClient.delete<void>(`/assets/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const liabilityService = {
|
||||
async getAll(): Promise<{liabilities: Liability[]}> {
|
||||
return apiClient.get<{liabilities: Liability[]}>('/liabilities');
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<{liability: Liability}> {
|
||||
return apiClient.get<{liability: Liability}>(`/liabilities/${id}`);
|
||||
},
|
||||
|
||||
async create(data: CreateLiabilityRequest): Promise<{liability: Liability}> {
|
||||
return apiClient.post<{liability: Liability}>('/liabilities', data);
|
||||
},
|
||||
|
||||
async update(id: string, data: UpdateLiabilityRequest): Promise<{liability: Liability}> {
|
||||
return apiClient.put<{liability: Liability}>(`/liabilities/${id}`, data);
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return apiClient.delete<void>(`/liabilities/${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const snapshotService = {
|
||||
async getAll(): Promise<{snapshots: NetWorthSnapshot[]}> {
|
||||
return apiClient.get<{snapshots: NetWorthSnapshot[]}>('/networth/snapshots');
|
||||
},
|
||||
|
||||
async getById(id: string): Promise<{snapshot: NetWorthSnapshot}> {
|
||||
return apiClient.get<{snapshot: NetWorthSnapshot}>(`/networth/snapshots/${id}`);
|
||||
},
|
||||
|
||||
async create(data: {
|
||||
date: string;
|
||||
totalAssets: number;
|
||||
totalLiabilities: number;
|
||||
netWorth: number;
|
||||
notes?: string;
|
||||
}): Promise<{snapshot: NetWorthSnapshot}> {
|
||||
return apiClient.post<{snapshot: NetWorthSnapshot}>('/networth/snapshots', data);
|
||||
},
|
||||
|
||||
async createFromCurrent(notes?: string): Promise<{snapshot: NetWorthSnapshot}> {
|
||||
return apiClient.post<{snapshot: NetWorthSnapshot}>('/networth/snapshots/record', {notes});
|
||||
},
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
return apiClient.delete<void>(`/networth/snapshots/${id}`);
|
||||
},
|
||||
};
|
||||
37
frontend-web/src/lib/api/token.ts
Normal file
37
frontend-web/src/lib/api/token.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Token Storage Utilities
|
||||
*/
|
||||
|
||||
const TOKEN_KEY = 'auth_token';
|
||||
const USER_KEY = 'auth_user';
|
||||
|
||||
export const tokenStorage = {
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
},
|
||||
|
||||
setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
},
|
||||
|
||||
removeToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
},
|
||||
|
||||
getUser(): string | null {
|
||||
return localStorage.getItem(USER_KEY);
|
||||
},
|
||||
|
||||
setUser(user: string): void {
|
||||
localStorage.setItem(USER_KEY, user);
|
||||
},
|
||||
|
||||
removeUser(): void {
|
||||
localStorage.removeItem(USER_KEY);
|
||||
},
|
||||
|
||||
clear(): void {
|
||||
this.removeToken();
|
||||
this.removeUser();
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useState} from 'react';
|
||||
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {useAppSelector, useAppDispatch, addSnapshot, type Asset, type Liability} from '@/store';
|
||||
import {useAppSelector, type Asset, type Liability} from '@/store';
|
||||
import {AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer} from 'recharts';
|
||||
import {format} from 'date-fns';
|
||||
import AddAssetDialog from '@/components/dialogs/AddAssetDialog';
|
||||
@@ -12,7 +12,6 @@ import {formatCurrency, formatPercentage} from '@/lib/formatters';
|
||||
import {calculateMonthlyChange, calculateYTDGrowth} from '@/lib/calculations';
|
||||
|
||||
export default function NetWorthPage() {
|
||||
const dispatch = useAppDispatch();
|
||||
const [assetDialogOpen, setAssetDialogOpen] = useState(false);
|
||||
const [liabilityDialogOpen, setLiabilityDialogOpen] = useState(false);
|
||||
const [editAssetDialogOpen, setEditAssetDialogOpen] = useState(false);
|
||||
@@ -34,14 +33,8 @@ export default function NetWorthPage() {
|
||||
const ytdGrowth = calculateYTDGrowth(snapshots);
|
||||
|
||||
const handleRecordSnapshot = () => {
|
||||
const snapshot = {
|
||||
id: crypto.randomUUID(),
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
totalAssets,
|
||||
totalLiabilities,
|
||||
netWorth
|
||||
};
|
||||
dispatch(addSnapshot(snapshot));
|
||||
// TODO: Implement createSnapshot thunk
|
||||
console.log('Record snapshot functionality not yet implemented');
|
||||
};
|
||||
|
||||
const handleEditAsset = (asset: Asset) => {
|
||||
|
||||
@@ -13,14 +13,15 @@ export type {User, UserState} from './slices/userSlice';
|
||||
export {
|
||||
setLoading as setNetWorthLoading,
|
||||
setError as setNetWorthError,
|
||||
addAsset,
|
||||
fetchAssets,
|
||||
createAsset,
|
||||
updateAsset,
|
||||
removeAsset,
|
||||
addLiability,
|
||||
deleteAsset,
|
||||
fetchLiabilities,
|
||||
createLiability,
|
||||
updateLiability,
|
||||
removeLiability,
|
||||
addSnapshot,
|
||||
setSnapshots
|
||||
deleteLiability,
|
||||
fetchSnapshots
|
||||
} from './slices/netWorthSlice';
|
||||
export type {Asset, Liability, NetWorthSnapshot, NetWorthState} from './slices/netWorthSlice';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {createSlice, 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';
|
||||
|
||||
export interface IncomeSource {
|
||||
id: string;
|
||||
@@ -47,162 +48,77 @@ const defaultCategories = {
|
||||
expense: ['Housing', 'Utilities', 'Transportation', 'Food', 'Insurance', 'Healthcare', 'Subscriptions', 'Entertainment', 'Shopping', 'Savings', 'Other']
|
||||
};
|
||||
|
||||
// Mock data
|
||||
const mockIncomeSources: IncomeSource[] = [
|
||||
{
|
||||
id: 'i1',
|
||||
name: 'Software Engineer Salary',
|
||||
amount: 8500,
|
||||
frequency: 'monthly',
|
||||
category: 'Salary',
|
||||
nextDate: '2024-12-15',
|
||||
isActive: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{id: 'i2', name: 'Consulting', amount: 2000, frequency: 'monthly', category: 'Freelance', nextDate: '2024-12-20', isActive: true, createdAt: '2024-03-01'},
|
||||
{
|
||||
id: 'i3',
|
||||
name: 'Dividend Income',
|
||||
amount: 450,
|
||||
frequency: 'quarterly',
|
||||
category: 'Investments',
|
||||
nextDate: '2024-12-31',
|
||||
isActive: true,
|
||||
createdAt: '2024-01-01'
|
||||
}
|
||||
];
|
||||
|
||||
const mockExpenses: Expense[] = [
|
||||
{
|
||||
id: 'e1',
|
||||
name: 'Mortgage',
|
||||
amount: 2200,
|
||||
frequency: 'monthly',
|
||||
category: 'Housing',
|
||||
nextDate: '2024-12-01',
|
||||
isActive: true,
|
||||
isEssential: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e2',
|
||||
name: 'Car Payment',
|
||||
amount: 450,
|
||||
frequency: 'monthly',
|
||||
category: 'Transportation',
|
||||
nextDate: '2024-12-05',
|
||||
isActive: true,
|
||||
isEssential: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e3',
|
||||
name: 'Car Insurance',
|
||||
amount: 180,
|
||||
frequency: 'monthly',
|
||||
category: 'Insurance',
|
||||
nextDate: '2024-12-10',
|
||||
isActive: true,
|
||||
isEssential: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e4',
|
||||
name: 'Utilities',
|
||||
amount: 250,
|
||||
frequency: 'monthly',
|
||||
category: 'Utilities',
|
||||
nextDate: '2024-12-15',
|
||||
isActive: true,
|
||||
isEssential: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e5',
|
||||
name: 'Groceries',
|
||||
amount: 600,
|
||||
frequency: 'monthly',
|
||||
category: 'Food',
|
||||
nextDate: '2024-12-01',
|
||||
isActive: true,
|
||||
isEssential: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e6',
|
||||
name: 'Gym Membership',
|
||||
amount: 50,
|
||||
frequency: 'monthly',
|
||||
category: 'Healthcare',
|
||||
nextDate: '2024-12-01',
|
||||
isActive: true,
|
||||
isEssential: false,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e7',
|
||||
name: 'Netflix',
|
||||
amount: 15,
|
||||
frequency: 'monthly',
|
||||
category: 'Subscriptions',
|
||||
nextDate: '2024-12-08',
|
||||
isActive: true,
|
||||
isEssential: false,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e8',
|
||||
name: 'Spotify',
|
||||
amount: 12,
|
||||
frequency: 'monthly',
|
||||
category: 'Subscriptions',
|
||||
nextDate: '2024-12-12',
|
||||
isActive: true,
|
||||
isEssential: false,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e9',
|
||||
name: 'Health Insurance',
|
||||
amount: 350,
|
||||
frequency: 'monthly',
|
||||
category: 'Insurance',
|
||||
nextDate: '2024-12-01',
|
||||
isActive: true,
|
||||
isEssential: true,
|
||||
createdAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 'e10',
|
||||
name: '401k Contribution',
|
||||
amount: 1500,
|
||||
frequency: 'monthly',
|
||||
category: 'Savings',
|
||||
nextDate: '2024-12-15',
|
||||
isActive: true,
|
||||
isEssential: true,
|
||||
createdAt: '2024-01-01'
|
||||
}
|
||||
];
|
||||
|
||||
const mockTransactions: Transaction[] = [
|
||||
{id: 't1', type: 'income', name: 'Salary', amount: 8500, category: 'Salary', date: '2024-11-15'},
|
||||
{id: 't2', type: 'expense', name: 'Mortgage', amount: 2200, category: 'Housing', date: '2024-11-01'},
|
||||
{id: 't3', type: 'expense', name: 'Groceries', amount: 145, category: 'Food', date: '2024-11-28'},
|
||||
{id: 't4', type: 'expense', name: 'Gas', amount: 55, category: 'Transportation', date: '2024-11-25'},
|
||||
{id: 't5', type: 'income', name: 'Consulting Payment', amount: 2000, category: 'Freelance', date: '2024-11-20'},
|
||||
{id: 't6', type: 'expense', name: 'Restaurant', amount: 85, category: 'Food', date: '2024-11-22'}
|
||||
];
|
||||
|
||||
const initialState: CashflowState = {
|
||||
incomeSources: mockIncomeSources,
|
||||
expenses: mockExpenses,
|
||||
transactions: mockTransactions,
|
||||
incomeSources: [],
|
||||
expenses: [],
|
||||
transactions: [],
|
||||
categories: defaultCategories,
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
// Helper mappers
|
||||
const mapApiIncomeToIncome = (apiIncome: ApiIncome): IncomeSource => ({
|
||||
id: apiIncome.id,
|
||||
name: apiIncome.name,
|
||||
amount: apiIncome.amount,
|
||||
frequency: apiIncome.frequency.toLowerCase() as IncomeSource['frequency'],
|
||||
category: 'Income',
|
||||
nextDate: new Date().toISOString(),
|
||||
isActive: true,
|
||||
createdAt: apiIncome.createdAt || new Date().toISOString(),
|
||||
});
|
||||
|
||||
const mapApiExpenseToExpense = (apiExpense: ApiExpense): Expense => ({
|
||||
id: apiExpense.id,
|
||||
name: apiExpense.name,
|
||||
amount: apiExpense.amount,
|
||||
frequency: apiExpense.frequency.toLowerCase() as Expense['frequency'],
|
||||
category: apiExpense.category || 'Other',
|
||||
nextDate: new Date().toISOString(),
|
||||
isActive: true,
|
||||
isEssential: apiExpense.isEssential || false,
|
||||
createdAt: apiExpense.createdAt || new Date().toISOString(),
|
||||
});
|
||||
|
||||
const mapApiTransactionToTransaction = (apiTransaction: ApiTransaction): Transaction => ({
|
||||
id: apiTransaction.id,
|
||||
type: apiTransaction.type.toLowerCase() as Transaction['type'],
|
||||
name: apiTransaction.description,
|
||||
amount: apiTransaction.amount,
|
||||
category: apiTransaction.category || 'Other',
|
||||
date: apiTransaction.date,
|
||||
note: apiTransaction.notes,
|
||||
});
|
||||
|
||||
// Async thunks
|
||||
export const fetchIncomeSources = createAsyncThunk('cashflow/fetchIncomeSources', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await incomeService.getAll();
|
||||
return response.incomeSources.map(mapApiIncomeToIncome);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch income sources');
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchExpenses = createAsyncThunk('cashflow/fetchExpenses', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await expenseService.getAll();
|
||||
return response.expenses.map(mapApiExpenseToExpense);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch expenses');
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchTransactions = createAsyncThunk('cashflow/fetchTransactions', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await transactionService.getAll();
|
||||
return response.transactions.map(mapApiTransactionToTransaction);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch transactions');
|
||||
}
|
||||
});
|
||||
|
||||
const cashflowSlice = createSlice({
|
||||
name: 'cashflow',
|
||||
initialState,
|
||||
@@ -242,7 +158,50 @@ const cashflowSlice = createSlice({
|
||||
removeTransaction: (state, action: PayloadAction<string>) => {
|
||||
state.transactions = state.transactions.filter(t => t.id !== action.payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
// Fetch income sources
|
||||
builder.addCase(fetchIncomeSources.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchIncomeSources.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.incomeSources = action.payload;
|
||||
});
|
||||
builder.addCase(fetchIncomeSources.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Fetch expenses
|
||||
builder.addCase(fetchExpenses.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchExpenses.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.expenses = action.payload;
|
||||
});
|
||||
builder.addCase(fetchExpenses.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Fetch transactions
|
||||
builder.addCase(fetchTransactions.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchTransactions.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.transactions = action.payload;
|
||||
});
|
||||
builder.addCase(fetchTransactions.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
|
||||
@@ -50,104 +50,10 @@ const defaultCategories: DebtCategory[] = [
|
||||
{id: 'other', name: 'Other', color: '#6b7280', createdAt: new Date().toISOString()}
|
||||
];
|
||||
|
||||
// Mock data for development
|
||||
const mockAccounts: DebtAccount[] = [
|
||||
{
|
||||
id: 'cc1',
|
||||
name: 'Chase Sapphire Preferred',
|
||||
categoryId: 'credit-cards',
|
||||
institution: 'Chase',
|
||||
accountNumber: '4521',
|
||||
originalBalance: 8500,
|
||||
currentBalance: 3200,
|
||||
interestRate: 21.99,
|
||||
minimumPayment: 95,
|
||||
dueDay: 15,
|
||||
createdAt: '2024-01-15',
|
||||
updatedAt: '2024-12-01'
|
||||
},
|
||||
{
|
||||
id: 'cc2',
|
||||
name: 'Amex Blue Cash',
|
||||
categoryId: 'credit-cards',
|
||||
institution: 'American Express',
|
||||
accountNumber: '1008',
|
||||
originalBalance: 4200,
|
||||
currentBalance: 1850,
|
||||
interestRate: 19.24,
|
||||
minimumPayment: 55,
|
||||
dueDay: 22,
|
||||
createdAt: '2024-02-10',
|
||||
updatedAt: '2024-12-01'
|
||||
},
|
||||
{
|
||||
id: 'cc3',
|
||||
name: 'Citi Double Cash',
|
||||
categoryId: 'credit-cards',
|
||||
institution: 'Citibank',
|
||||
accountNumber: '7732',
|
||||
originalBalance: 2800,
|
||||
currentBalance: 950,
|
||||
interestRate: 18.49,
|
||||
minimumPayment: 35,
|
||||
dueDay: 8,
|
||||
createdAt: '2024-03-05',
|
||||
updatedAt: '2024-12-01'
|
||||
},
|
||||
{
|
||||
id: 'al1',
|
||||
name: 'Tesla Model 3 Loan',
|
||||
categoryId: 'auto-loans',
|
||||
institution: 'Tesla Finance',
|
||||
accountNumber: '9901',
|
||||
originalBalance: 42000,
|
||||
currentBalance: 15000,
|
||||
interestRate: 4.99,
|
||||
minimumPayment: 650,
|
||||
dueDay: 1,
|
||||
createdAt: '2021-06-15',
|
||||
updatedAt: '2024-12-01'
|
||||
},
|
||||
{
|
||||
id: 'sl1',
|
||||
name: 'Federal Student Loan',
|
||||
categoryId: 'student-loans',
|
||||
institution: 'Dept of Education',
|
||||
originalBalance: 45000,
|
||||
currentBalance: 28000,
|
||||
interestRate: 5.5,
|
||||
minimumPayment: 320,
|
||||
dueDay: 25,
|
||||
createdAt: '2018-09-01',
|
||||
updatedAt: '2024-12-01'
|
||||
},
|
||||
{
|
||||
id: 'pl1',
|
||||
name: 'Home Improvement Loan',
|
||||
categoryId: 'personal-loans',
|
||||
institution: 'SoFi',
|
||||
accountNumber: '3344',
|
||||
originalBalance: 15000,
|
||||
currentBalance: 8500,
|
||||
interestRate: 8.99,
|
||||
minimumPayment: 285,
|
||||
dueDay: 12,
|
||||
createdAt: '2023-08-20',
|
||||
updatedAt: '2024-12-01'
|
||||
}
|
||||
];
|
||||
|
||||
const mockPayments: DebtPayment[] = [
|
||||
{id: 'p1', accountId: 'cc1', amount: 500, date: '2024-11-15', note: 'Extra payment'},
|
||||
{id: 'p2', accountId: 'cc2', amount: 200, date: '2024-11-22'},
|
||||
{id: 'p3', accountId: 'al1', amount: 650, date: '2024-12-01'},
|
||||
{id: 'p4', accountId: 'sl1', amount: 320, date: '2024-11-25'}
|
||||
];
|
||||
|
||||
const initialState: DebtsState = {
|
||||
categories: defaultCategories,
|
||||
accounts: mockAccounts,
|
||||
payments: mockPayments,
|
||||
accounts: [],
|
||||
payments: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
@@ -42,129 +42,9 @@ export interface InvoicesState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Mock data for development
|
||||
const mockClients: Client[] = [
|
||||
{
|
||||
id: 'c1',
|
||||
name: 'Acme Corp',
|
||||
email: 'billing@acme.com',
|
||||
phone: '555-0100',
|
||||
company: 'Acme Corporation',
|
||||
address: '123 Business Ave, Suite 400, San Francisco, CA 94102',
|
||||
createdAt: '2024-01-10'
|
||||
},
|
||||
{
|
||||
id: 'c2',
|
||||
name: 'TechStart Inc',
|
||||
email: 'accounts@techstart.io',
|
||||
phone: '555-0200',
|
||||
company: 'TechStart Inc',
|
||||
address: '456 Innovation Blvd, Austin, TX 78701',
|
||||
createdAt: '2024-02-15'
|
||||
},
|
||||
{
|
||||
id: 'c3',
|
||||
name: 'Sarah Mitchell',
|
||||
email: 'sarah@mitchell.design',
|
||||
company: 'Mitchell Design Studio',
|
||||
createdAt: '2024-03-22'
|
||||
},
|
||||
{
|
||||
id: 'c4',
|
||||
name: 'Global Media LLC',
|
||||
email: 'finance@globalmedia.com',
|
||||
phone: '555-0400',
|
||||
company: 'Global Media LLC',
|
||||
address: '789 Media Row, New York, NY 10001',
|
||||
createdAt: '2024-04-08'
|
||||
}
|
||||
];
|
||||
|
||||
const mockInvoices: Invoice[] = [
|
||||
{
|
||||
id: 'inv1',
|
||||
invoiceNumber: 'INV-2024-001',
|
||||
clientId: 'c1',
|
||||
status: 'paid',
|
||||
issueDate: '2024-10-01',
|
||||
dueDate: '2024-10-31',
|
||||
lineItems: [
|
||||
{id: 'li1', description: 'Web Development - October', quantity: 80, unitPrice: 150, total: 12000},
|
||||
{id: 'li2', description: 'Hosting & Maintenance', quantity: 1, unitPrice: 500, total: 500}
|
||||
],
|
||||
subtotal: 12500,
|
||||
tax: 0,
|
||||
total: 12500,
|
||||
createdAt: '2024-10-01',
|
||||
updatedAt: '2024-10-15'
|
||||
},
|
||||
{
|
||||
id: 'inv2',
|
||||
invoiceNumber: 'INV-2024-002',
|
||||
clientId: 'c2',
|
||||
status: 'paid',
|
||||
issueDate: '2024-10-15',
|
||||
dueDate: '2024-11-14',
|
||||
lineItems: [{id: 'li3', description: 'Mobile App Development', quantity: 120, unitPrice: 175, total: 21000}],
|
||||
subtotal: 21000,
|
||||
tax: 0,
|
||||
total: 21000,
|
||||
createdAt: '2024-10-15',
|
||||
updatedAt: '2024-11-10'
|
||||
},
|
||||
{
|
||||
id: 'inv3',
|
||||
invoiceNumber: 'INV-2024-003',
|
||||
clientId: 'c1',
|
||||
status: 'sent',
|
||||
issueDate: '2024-11-01',
|
||||
dueDate: '2024-12-01',
|
||||
lineItems: [
|
||||
{id: 'li4', description: 'Web Development - November', quantity: 60, unitPrice: 150, total: 9000},
|
||||
{id: 'li5', description: 'API Integration', quantity: 20, unitPrice: 175, total: 3500}
|
||||
],
|
||||
subtotal: 12500,
|
||||
tax: 0,
|
||||
total: 12500,
|
||||
createdAt: '2024-11-01',
|
||||
updatedAt: '2024-11-01'
|
||||
},
|
||||
{
|
||||
id: 'inv4',
|
||||
invoiceNumber: 'INV-2024-004',
|
||||
clientId: 'c3',
|
||||
status: 'overdue',
|
||||
issueDate: '2024-10-20',
|
||||
dueDate: '2024-11-20',
|
||||
lineItems: [{id: 'li6', description: 'Brand Identity Design', quantity: 1, unitPrice: 4500, total: 4500}],
|
||||
subtotal: 4500,
|
||||
tax: 0,
|
||||
total: 4500,
|
||||
createdAt: '2024-10-20',
|
||||
updatedAt: '2024-10-20'
|
||||
},
|
||||
{
|
||||
id: 'inv5',
|
||||
invoiceNumber: 'INV-2024-005',
|
||||
clientId: 'c4',
|
||||
status: 'draft',
|
||||
issueDate: '2024-12-01',
|
||||
dueDate: '2024-12-31',
|
||||
lineItems: [
|
||||
{id: 'li7', description: 'Video Production', quantity: 5, unitPrice: 2000, total: 10000},
|
||||
{id: 'li8', description: 'Motion Graphics', quantity: 10, unitPrice: 500, total: 5000}
|
||||
],
|
||||
subtotal: 15000,
|
||||
tax: 0,
|
||||
total: 15000,
|
||||
createdAt: '2024-12-01',
|
||||
updatedAt: '2024-12-01'
|
||||
}
|
||||
];
|
||||
|
||||
const initialState: InvoicesState = {
|
||||
clients: mockClients,
|
||||
invoices: mockInvoices,
|
||||
clients: [],
|
||||
invoices: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import {createSlice, type PayloadAction} from '@reduxjs/toolkit';
|
||||
import {createSlice, createAsyncThunk, type PayloadAction} from '@reduxjs/toolkit';
|
||||
import {
|
||||
assetService,
|
||||
liabilityService,
|
||||
snapshotService,
|
||||
type Asset as ApiAsset,
|
||||
type Liability as ApiLiability,
|
||||
type CreateAssetRequest,
|
||||
type UpdateAssetRequest,
|
||||
type CreateLiabilityRequest,
|
||||
type UpdateLiabilityRequest,
|
||||
} from '@/lib/api/networth.service';
|
||||
|
||||
export interface Asset {
|
||||
id: string;
|
||||
@@ -32,39 +43,118 @@ export interface NetWorthState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// Mock data for development
|
||||
const mockAssets: Asset[] = [
|
||||
{id: 'a1', name: 'Chase Checking', type: 'cash', value: 12500, updatedAt: '2024-12-01'},
|
||||
{id: 'a2', name: 'Ally Savings', type: 'cash', value: 35000, updatedAt: '2024-12-01'},
|
||||
{id: 'a3', name: 'Fidelity 401k', type: 'investment', value: 145000, updatedAt: '2024-12-01'},
|
||||
{id: 'a4', name: 'Vanguard Brokerage', type: 'investment', value: 52000, updatedAt: '2024-12-01'},
|
||||
{id: 'a5', name: 'Primary Residence', type: 'property', value: 425000, updatedAt: '2024-12-01'},
|
||||
{id: 'a6', name: '2021 Tesla Model 3', type: 'vehicle', value: 28000, updatedAt: '2024-12-01'}
|
||||
];
|
||||
|
||||
const mockLiabilities: Liability[] = [
|
||||
{id: 'l1', name: 'Mortgage', type: 'mortgage', balance: 320000, updatedAt: '2024-12-01'},
|
||||
{id: 'l2', name: 'Auto Loan', type: 'loan', balance: 15000, updatedAt: '2024-12-01'},
|
||||
{id: 'l3', name: 'Student Loans', type: 'loan', balance: 28000, updatedAt: '2024-12-01'}
|
||||
];
|
||||
|
||||
const mockSnapshots: NetWorthSnapshot[] = [
|
||||
{id: 's1', date: '2024-07-01', totalAssets: 650000, totalLiabilities: 380000, netWorth: 270000},
|
||||
{id: 's2', date: '2024-08-01', totalAssets: 665000, totalLiabilities: 375000, netWorth: 290000},
|
||||
{id: 's3', date: '2024-09-01', totalAssets: 680000, totalLiabilities: 370000, netWorth: 310000},
|
||||
{id: 's4', date: '2024-10-01', totalAssets: 685000, totalLiabilities: 368000, netWorth: 317000},
|
||||
{id: 's5', date: '2024-11-01', totalAssets: 692000, totalLiabilities: 365000, netWorth: 327000},
|
||||
{id: 's6', date: '2024-12-01', totalAssets: 697500, totalLiabilities: 363000, netWorth: 334500}
|
||||
];
|
||||
|
||||
const initialState: NetWorthState = {
|
||||
assets: mockAssets,
|
||||
liabilities: mockLiabilities,
|
||||
snapshots: mockSnapshots,
|
||||
assets: [],
|
||||
liabilities: [],
|
||||
snapshots: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
// Helper functions to map between API and UI types
|
||||
const mapApiAssetToAsset = (apiAsset: ApiAsset): Asset => ({
|
||||
id: apiAsset.id,
|
||||
name: apiAsset.name,
|
||||
type: apiAsset.type.toLowerCase() as Asset['type'],
|
||||
value: apiAsset.value,
|
||||
updatedAt: apiAsset.updatedAt || new Date().toISOString(),
|
||||
});
|
||||
|
||||
const mapApiLiabilityToLiability = (apiLiability: ApiLiability): Liability => ({
|
||||
id: apiLiability.id,
|
||||
name: apiLiability.name,
|
||||
type: apiLiability.type.toLowerCase() as Liability['type'],
|
||||
balance: apiLiability.currentBalance,
|
||||
updatedAt: apiLiability.updatedAt || new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Async thunks for assets
|
||||
export const fetchAssets = createAsyncThunk('netWorth/fetchAssets', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await assetService.getAll();
|
||||
return response.assets.map(mapApiAssetToAsset);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch assets');
|
||||
}
|
||||
});
|
||||
|
||||
export const createAsset = createAsyncThunk('netWorth/createAsset', async (data: CreateAssetRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await assetService.create(data);
|
||||
return mapApiAssetToAsset(response.asset);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to create asset');
|
||||
}
|
||||
});
|
||||
|
||||
export const updateAsset = createAsyncThunk('netWorth/updateAsset', async ({id, data}: {id: string; data: UpdateAssetRequest}, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await assetService.update(id, data);
|
||||
return mapApiAssetToAsset(response.asset);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to update asset');
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteAsset = createAsyncThunk('netWorth/deleteAsset', async (id: string, {rejectWithValue}) => {
|
||||
try {
|
||||
await assetService.delete(id);
|
||||
return id;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to delete asset');
|
||||
}
|
||||
});
|
||||
|
||||
// Async thunks for liabilities
|
||||
export const fetchLiabilities = createAsyncThunk('netWorth/fetchLiabilities', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await liabilityService.getAll();
|
||||
return response.liabilities.map(mapApiLiabilityToLiability);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch liabilities');
|
||||
}
|
||||
});
|
||||
|
||||
export const createLiability = createAsyncThunk('netWorth/createLiability', async (data: CreateLiabilityRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await liabilityService.create(data);
|
||||
return mapApiLiabilityToLiability(response.liability);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to create liability');
|
||||
}
|
||||
});
|
||||
|
||||
export const updateLiability = createAsyncThunk(
|
||||
'netWorth/updateLiability',
|
||||
async ({id, data}: {id: string; data: UpdateLiabilityRequest}, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await liabilityService.update(id, data);
|
||||
return mapApiLiabilityToLiability(response.liability);
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to update liability');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteLiability = createAsyncThunk('netWorth/deleteLiability', async (id: string, {rejectWithValue}) => {
|
||||
try {
|
||||
await liabilityService.delete(id);
|
||||
return id;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to delete liability');
|
||||
}
|
||||
});
|
||||
|
||||
// Async thunks for snapshots
|
||||
export const fetchSnapshots = createAsyncThunk('netWorth/fetchSnapshots', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await snapshotService.getAll();
|
||||
return response.snapshots;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Failed to fetch snapshots');
|
||||
}
|
||||
});
|
||||
|
||||
const netWorthSlice = createSlice({
|
||||
name: 'netWorth',
|
||||
initialState,
|
||||
@@ -75,36 +165,84 @@ const netWorthSlice = createSlice({
|
||||
setError: (state, action: PayloadAction<string | null>) => {
|
||||
state.error = action.payload;
|
||||
},
|
||||
addAsset: (state, action: PayloadAction<Asset>) => {
|
||||
},
|
||||
extraReducers: builder => {
|
||||
// Fetch assets
|
||||
builder.addCase(fetchAssets.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchAssets.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.assets = action.payload;
|
||||
});
|
||||
builder.addCase(fetchAssets.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Create asset
|
||||
builder.addCase(createAsset.fulfilled, (state, action) => {
|
||||
state.assets.push(action.payload);
|
||||
},
|
||||
updateAsset: (state, action: PayloadAction<Asset>) => {
|
||||
});
|
||||
|
||||
// Update asset
|
||||
builder.addCase(updateAsset.fulfilled, (state, action) => {
|
||||
const index = state.assets.findIndex(a => a.id === action.payload.id);
|
||||
if (index !== -1) state.assets[index] = action.payload;
|
||||
},
|
||||
removeAsset: (state, action: PayloadAction<string>) => {
|
||||
});
|
||||
|
||||
// Delete asset
|
||||
builder.addCase(deleteAsset.fulfilled, (state, action) => {
|
||||
state.assets = state.assets.filter(a => a.id !== action.payload);
|
||||
},
|
||||
addLiability: (state, action: PayloadAction<Liability>) => {
|
||||
});
|
||||
|
||||
// Fetch liabilities
|
||||
builder.addCase(fetchLiabilities.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchLiabilities.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.liabilities = action.payload;
|
||||
});
|
||||
builder.addCase(fetchLiabilities.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Create liability
|
||||
builder.addCase(createLiability.fulfilled, (state, action) => {
|
||||
state.liabilities.push(action.payload);
|
||||
},
|
||||
updateLiability: (state, action: PayloadAction<Liability>) => {
|
||||
});
|
||||
|
||||
// Update liability
|
||||
builder.addCase(updateLiability.fulfilled, (state, action) => {
|
||||
const index = state.liabilities.findIndex(l => l.id === action.payload.id);
|
||||
if (index !== -1) state.liabilities[index] = action.payload;
|
||||
},
|
||||
removeLiability: (state, action: PayloadAction<string>) => {
|
||||
});
|
||||
|
||||
// Delete liability
|
||||
builder.addCase(deleteLiability.fulfilled, (state, action) => {
|
||||
state.liabilities = state.liabilities.filter(l => l.id !== action.payload);
|
||||
},
|
||||
addSnapshot: (state, action: PayloadAction<NetWorthSnapshot>) => {
|
||||
state.snapshots.push(action.payload);
|
||||
},
|
||||
setSnapshots: (state, action: PayloadAction<NetWorthSnapshot[]>) => {
|
||||
});
|
||||
|
||||
// Fetch snapshots
|
||||
builder.addCase(fetchSnapshots.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(fetchSnapshots.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.snapshots = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.addCase(fetchSnapshots.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {setLoading, setError, addAsset, updateAsset, removeAsset, addLiability, updateLiability, removeLiability, addSnapshot, setSnapshots} =
|
||||
netWorthSlice.actions;
|
||||
export const {setLoading, setError} = netWorthSlice.actions;
|
||||
|
||||
export default netWorthSlice.reducer;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {createSlice, type PayloadAction} from '@reduxjs/toolkit';
|
||||
import {createSlice, createAsyncThunk, type PayloadAction} from '@reduxjs/toolkit';
|
||||
import {authService, type RegisterRequest, type LoginRequest} from '@/lib/api/auth.service';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
@@ -20,6 +21,40 @@ const initialState: UserState = {
|
||||
error: null
|
||||
};
|
||||
|
||||
// Async thunks
|
||||
export const registerUser = createAsyncThunk('user/register', async (data: RegisterRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await authService.register(data);
|
||||
return response.user;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Registration failed');
|
||||
}
|
||||
});
|
||||
|
||||
export const loginUser = createAsyncThunk('user/login', async (data: LoginRequest, {rejectWithValue}) => {
|
||||
try {
|
||||
const response = await authService.login(data);
|
||||
return response.user;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || 'Login failed');
|
||||
}
|
||||
});
|
||||
|
||||
export const loadUserFromStorage = createAsyncThunk('user/loadFromStorage', async (_, {rejectWithValue}) => {
|
||||
try {
|
||||
const user = authService.getCurrentUser();
|
||||
if (!user) {
|
||||
return rejectWithValue('No user found');
|
||||
}
|
||||
// Verify token is still valid by fetching profile
|
||||
await authService.getProfile();
|
||||
return user;
|
||||
} catch (error: any) {
|
||||
authService.logout();
|
||||
return rejectWithValue(error.message || 'Session expired');
|
||||
}
|
||||
});
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
@@ -33,6 +68,7 @@ const userSlice = createSlice({
|
||||
state.error = null;
|
||||
},
|
||||
clearUser: state => {
|
||||
authService.logout();
|
||||
state.currentUser = null;
|
||||
state.isAuthenticated = false;
|
||||
state.error = null;
|
||||
@@ -41,6 +77,54 @@ const userSlice = createSlice({
|
||||
state.error = action.payload;
|
||||
state.isLoading = false;
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
// Register
|
||||
builder.addCase(registerUser.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(registerUser.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.currentUser = action.payload;
|
||||
state.isAuthenticated = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(registerUser.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Login
|
||||
builder.addCase(loginUser.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(loginUser.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.currentUser = action.payload;
|
||||
state.isAuthenticated = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(loginUser.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Load from storage
|
||||
builder.addCase(loadUserFromStorage.pending, state => {
|
||||
state.isLoading = true;
|
||||
});
|
||||
builder.addCase(loadUserFromStorage.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.currentUser = action.payload;
|
||||
state.isAuthenticated = true;
|
||||
});
|
||||
builder.addCase(loadUserFromStorage.rejected, state => {
|
||||
state.isLoading = false;
|
||||
state.currentUser = null;
|
||||
state.isAuthenticated = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
305
package-lock.json
generated
Normal file
305
package-lock.json
generated
Normal file
@@ -0,0 +1,305 @@
|
||||
{
|
||||
"name": "personal-finances",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "personal-finances",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"concurrently": "^9.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user