diff --git a/COLLABORATIVE_PLANNING.md b/COLLABORATIVE_PLANNING.md deleted file mode 100644 index 500e7263..00000000 --- a/COLLABORATIVE_PLANNING.md +++ /dev/null @@ -1,681 +0,0 @@ -# Collaborative Financial Planning with Shared Goals & Permissions - -## Overview - -The Collaborative Financial Planning feature enables multiple users to manage shared expenses, budgets, and financial goals together. Perfect for families, couples, roommates, and small businesses, this feature provides granular permission controls, expense approval workflows, and comprehensive activity tracking. - -## Features - -### 1. **Shared Spaces** -- Create dedicated financial spaces for different groups (family, couple, roommates, business, friends) -- Invite members via unique invite codes -- Set privacy modes: open, restricted, or private -- Configure approval thresholds for large expenses -- Customize notification preferences per space - -### 2. **Role-Based Permissions** -Four predefined roles with granular permission controls: - -| Role | View | Add | Edit | Delete | Manage Goals | Manage Budgets | Approve | Manage Members | Reports | -|------|------|-----|------|--------|--------------|----------------|---------|----------------|---------| -| **Admin** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| **Manager** | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | -| **Contributor** | ✓ | ✓ | ✓ | ✗ | View Only | View Only | ✗ | ✗ | ✓ | -| **Viewer** | ✓ | ✗ | ✗ | ✗ | View Only | View Only | ✗ | ✗ | ✓ | - -### 3. **Shared Goals** -- Create financial goals with multiple contributors -- Track individual contributions and progress -- Set target amounts and deadlines -- Define allocation rules: equal, proportional, or custom -- Configure milestone alerts (e.g., 25%, 50%, 75% complete) -- Support for various goal categories: savings, investment, purchase, vacation, emergency, education - -### 4. **Expense Approval Workflow** -- Set approval thresholds (e.g., require approval for expenses > ₹10,000) -- Multi-level approval support (require N approvers) -- Priority levels: low, medium, high, urgent -- Automatic expense creation upon approval -- Email notifications for approvers and requesters -- 7-day expiration for pending requests - -### 5. **Privacy Controls** -Members can configure privacy settings: -- Hide personal transactions -- Hide income information -- Hide savings data - -### 6. **Activity Logging** -Comprehensive audit trail for: -- Member additions/removals -- Role changes -- Expense additions/edits/deletions -- Goal creations/updates/completions -- Budget changes -- Approval requests and decisions -- Settings modifications - -### 7. **Reporting & Analytics** -- Consolidated space reports with date ranges -- Expense breakdown by category and member -- Goal progress tracking -- Member contribution summaries -- Recent activity logs - -## Data Models - -### SharedSpace -```javascript -{ - name: String, // Space name - description: String, // Optional description - type: String, // family, couple, roommates, business, friends, other - owner: ObjectId, // Space creator - members: [{ - user: ObjectId, - role: String, // admin, manager, contributor, viewer - permissions: { - view_expenses: Boolean, - add_expenses: Boolean, - edit_expenses: Boolean, - delete_expenses: Boolean, - view_goals: Boolean, - manage_goals: Boolean, - view_budgets: Boolean, - manage_budgets: Boolean, - approve_expenses: Boolean, - manage_members: Boolean, - view_reports: Boolean - }, - privacy_settings: { - hide_personal_transactions: Boolean, - hide_income: Boolean, - hide_savings: Boolean - }, - notification_preferences: { - new_expense: Boolean, - goal_progress: Boolean, - budget_alert: Boolean, - approval_request: Boolean, - member_activity: Boolean - }, - joined_at: Date - }], - settings: { - currency: String, - require_approval_above: Number, - approval_threshold_count: Number, - privacy_mode: String, - enable_notifications: Boolean, - notification_channels: [String] - }, - invite_code: String, - isActive: Boolean -} -``` - -### SharedGoal -```javascript -{ - space: ObjectId, - name: String, - description: String, - target_amount: Number, - current_amount: Number, - currency: String, - deadline: Date, - category: String, // savings, investment, purchase, etc. - contributors: [{ - user: ObjectId, - target_contribution: Number, - current_contribution: Number, - contribution_percentage: Number, - last_contribution_date: Date - }], - contributions: [{ - user: ObjectId, - amount: Number, - date: Date, - note: String, - transaction_id: ObjectId - }], - status: String, // active, completed, paused, cancelled - priority: String, - visibility: String, - auto_allocate: Boolean, - allocation_rule: String, // equal, proportional, custom - milestone_alerts: [{ - percentage: Number, - triggered: Boolean - }], - created_by: ObjectId -} -``` - -### ApprovalRequest -```javascript -{ - space: ObjectId, - requester: ObjectId, - expense_data: { - description: String, - amount: Number, - category: String, - date: Date, - notes: String, - receipt_url: String - }, - status: String, // pending, approved, rejected, cancelled - approvals: [{ - approver: ObjectId, - decision: String, // approved, rejected - comment: String, - decided_at: Date - }], - required_approvals: Number, - priority: String, - expense_id: ObjectId, - expires_at: Date -} -``` - -### SpaceActivity -```javascript -{ - space: ObjectId, - actor: ObjectId, - action: String, // member_added, expense_added, goal_created, etc. - target_type: String, // expense, goal, budget, member, space, approval - target_id: ObjectId, - details: { - old_value: Mixed, - new_value: Mixed, - amount: Number, - description: String - } -} -``` - -## API Endpoints - -### Shared Spaces - -#### Create Shared Space -```http -POST /api/shared-spaces -Authorization: Bearer -Content-Type: application/json - -{ - "name": "Family Budget", - "description": "Our household expenses", - "type": "family", - "settings": { - "currency": "INR", - "require_approval_above": 10000, - "approval_threshold_count": 1, - "privacy_mode": "open" - } -} -``` - -#### Get User's Shared Spaces -```http -GET /api/shared-spaces -Authorization: Bearer -``` - -#### Get Single Shared Space -```http -GET /api/shared-spaces/:id -Authorization: Bearer -``` - -#### Update Shared Space -```http -PUT /api/shared-spaces/:id -Authorization: Bearer -Content-Type: application/json - -{ - "name": "Updated Name", - "settings": { - "require_approval_above": 15000 - } -} -``` - -#### Archive Shared Space -```http -DELETE /api/shared-spaces/:id -Authorization: Bearer -``` - -### Members - -#### Add Member -```http -POST /api/shared-spaces/:id/members -Authorization: Bearer -Content-Type: application/json - -{ - "user_id": "user_id_here", - "role": "contributor" -} -``` - -#### Join with Invite Code -```http -POST /api/shared-spaces/join -Authorization: Bearer -Content-Type: application/json - -{ - "invite_code": "ABC12345" -} -``` - -#### Remove Member -```http -DELETE /api/shared-spaces/:id/members/:userId -Authorization: Bearer -``` - -#### Update Member Role -```http -PUT /api/shared-spaces/:id/members/:userId -Authorization: Bearer -Content-Type: application/json - -{ - "role": "manager", - "permissions": { - "approve_expenses": true - } -} -``` - -#### Regenerate Invite Code -```http -POST /api/shared-spaces/:id/invite-code/regenerate -Authorization: Bearer -``` - -### Goals - -#### Create Shared Goal -```http -POST /api/shared-spaces/:id/goals -Authorization: Bearer -Content-Type: application/json - -{ - "name": "Emergency Fund", - "description": "Build a 6-month emergency fund", - "target_amount": 300000, - "currency": "INR", - "deadline": "2024-12-31", - "category": "emergency", - "allocation_rule": "equal", - "milestone_alerts": [ - { "percentage": 25 }, - { "percentage": 50 }, - { "percentage": 75 } - ] -} -``` - -#### Get Space Goals -```http -GET /api/shared-spaces/:id/goals?status=active -Authorization: Bearer -``` - -#### Get Single Goal -```http -GET /api/shared-spaces/:id/goals/:goalId -Authorization: Bearer -``` - -#### Add Contribution -```http -POST /api/shared-spaces/:id/goals/:goalId/contribute -Authorization: Bearer -Content-Type: application/json - -{ - "amount": 5000, - "note": "Monthly contribution" -} -``` - -#### Update Goal -```http -PUT /api/shared-spaces/:id/goals/:goalId -Authorization: Bearer -Content-Type: application/json - -{ - "target_amount": 350000, - "status": "active" -} -``` - -### Approvals - -#### Create Approval Request -```http -POST /api/shared-spaces/:id/approvals -Authorization: Bearer -Content-Type: application/json - -{ - "expense_data": { - "description": "New laptop for work", - "amount": 75000, - "category": "electronics", - "notes": "MacBook Pro for development" - }, - "priority": "high" -} -``` - -#### Get Pending Approvals -```http -GET /api/shared-spaces/:id/approvals -Authorization: Bearer -``` - -#### Approve/Reject Request -```http -POST /api/shared-spaces/:id/approvals/:requestId/approve -Authorization: Bearer -Content-Type: application/json - -{ - "comment": "Approved for business use" -} - -POST /api/shared-spaces/:id/approvals/:requestId/reject -{ - "comment": "Please wait until next quarter" -} -``` - -#### Cancel Approval Request -```http -DELETE /api/shared-spaces/:id/approvals/:requestId -Authorization: Bearer -``` - -### Reports & Activity - -#### Get Space Report -```http -GET /api/shared-spaces/:id/report?start_date=2024-01-01&end_date=2024-01-31 -Authorization: Bearer -``` - -**Response:** -```json -{ - "success": true, - "data": { - "space": { - "id": "space_id", - "name": "Family Budget", - "type": "family", - "currency": "INR", - "members": 4 - }, - "period": { - "startDate": "2024-01-01", - "endDate": "2024-01-31" - }, - "summary": { - "total_expenses": 125000, - "expense_count": 87, - "active_goals": 3, - "completed_goals": 1 - }, - "expenses": { - "by_category": { - "Food": { "total": 35000, "count": 28 }, - "Transport": { "total": 15000, "count": 12 } - }, - "by_member": { - "user_id_1": { "name": "John", "total": 60000, "count": 40 }, - "user_id_2": { "name": "Jane", "total": 65000, "count": 47 } - } - }, - "goals": [...], - "recent_activity": [...] - } -} -``` - -#### Get Member Contributions -```http -GET /api/shared-spaces/:id/contributions/:userId -Authorization: Bearer -``` - -#### Get Activity Log -```http -GET /api/shared-spaces/:id/activity?limit=50&skip=0 -Authorization: Bearer -``` - -## Usage Examples - -### Example 1: Family Budget Management - -```javascript -// 1. Create family space -const familySpace = await createSpace({ - name: "Smith Family Budget", - type: "family", - settings: { - currency: "INR", - require_approval_above: 10000, - approval_threshold_count: 1 - } -}); - -// 2. Invite spouse with manager role -await addMember(familySpace.id, spouseUserId, "manager"); - -// 3. Create emergency fund goal -const emergencyGoal = await createGoal(familySpace.id, { - name: "Emergency Fund", - target_amount: 300000, - allocation_rule: "equal", - deadline: "2024-12-31" -}); - -// 4. Add monthly contribution -await contributeToGoal(familySpace.id, emergencyGoal.id, { - amount: 10000, - note: "January contribution" -}); -``` - -### Example 2: Roommate Expense Sharing - -```javascript -// 1. Create roommates space -const apartmentSpace = await createSpace({ - name: "Apartment 402", - type: "roommates", - settings: { - require_approval_above: 5000, - privacy_mode: "restricted" - } -}); - -// 2. Share invite code -const inviteCode = apartmentSpace.invite_code; -// Others join: POST /api/shared-spaces/join { invite_code } - -// 3. Create shared utility goal -await createGoal(apartmentSpace.id, { - name: "Monthly Utilities", - target_amount: 8000, - allocation_rule: "equal", - category: "other" -}); -``` - -### Example 3: Business Expense Approval - -```javascript -// 1. Request approval for large expense -const approvalRequest = await createApproval(businessSpace.id, { - expense_data: { - description: "New server hardware", - amount: 150000, - category: "equipment" - }, - priority: "high" -}); - -// 2. Manager approves -await processApproval(businessSpace.id, approvalRequest.id, "approve", { - comment: "Approved - necessary for scaling" -}); - -// 3. Expense automatically created -``` - -## Permission Matrix - -### Detailed Permission Descriptions - -| Permission | Description | -|-----------|-------------| -| `view_expenses` | View all expenses in the space | -| `add_expenses` | Add new expenses to the space | -| `edit_expenses` | Modify existing expenses | -| `delete_expenses` | Delete expenses (usually admin only) | -| `view_goals` | View shared goals and contributions | -| `manage_goals` | Create, update, and manage goals | -| `view_budgets` | View budget information | -| `manage_budgets` | Create and modify budgets | -| `approve_expenses` | Approve expense requests above threshold | -| `manage_members` | Add, remove, and modify member roles | -| `view_reports` | Access consolidated reports and analytics | - -## Best Practices - -### 1. **Space Configuration** -- Set appropriate approval thresholds based on group size and trust level -- Use privacy mode "restricted" for roommates, "open" for families -- Configure notification preferences to avoid alert fatigue - -### 2. **Role Assignment** -- **Owner**: Person who manages the overall space (usually primary account holder) -- **Admin**: Trusted members who can manage everything (spouse, business partner) -- **Manager**: Members who can approve expenses but not manage members -- **Contributor**: Active participants who add expenses and contribute to goals -- **Viewer**: Members who need visibility but not management access - -### 3. **Goal Setting** -- Use equal allocation for shared responsibilities (rent, utilities) -- Use proportional allocation based on income levels -- Use custom allocation for specific agreements -- Set realistic deadlines with buffer time -- Configure milestone alerts at 25%, 50%, 75%, 90% - -### 4. **Approval Workflow** -- Set threshold at a level that balances control and convenience -- Use priority levels to indicate urgency -- Add context in approval requests to help approvers -- Respond to approval requests within 24-48 hours - -### 5. **Privacy** -- Respect member privacy settings in reports -- Use "hide_personal_transactions" for members who want financial privacy -- Separate personal and shared spaces for better organization - -### 6. **Activity Monitoring** -- Review activity logs regularly for transparency -- Use activity logs to track contribution patterns -- Monitor for unusual expense patterns - -## Cron Jobs - -### Cleanup Expired Approval Requests -**Schedule:** Daily at 2:00 AM -```javascript -cron.schedule('0 2 * * *', async () => { - await cleanupExpiredApprovals(); -}); -``` -- Automatically marks approval requests as "cancelled" after 7 days -- Logs activity for transparency -- Prevents clutter in pending requests - -## Error Handling - -### Common Errors - -**403 Forbidden - Insufficient Permissions** -```json -{ - "success": false, - "message": "You do not have permission to manage members" -} -``` - -**404 Not Found - Space Not Found** -```json -{ - "success": false, - "message": "Shared space not found" -} -``` - -**400 Bad Request - Invalid Input** -```json -{ - "success": false, - "message": "Validation error", - "details": "target_amount must be at least 1" -} -``` - -## Security Considerations - -1. **Authentication Required**: All endpoints require valid JWT token -2. **Permission Checks**: Every action validates user permissions -3. **Input Validation**: Joi schemas validate all input data -4. **Audit Trail**: All actions logged in SpaceActivity -5. **Invite Code Security**: 8-character alphanumeric codes, can be regenerated -6. **Privacy Protection**: Member privacy settings enforced in queries - -## Future Enhancements - -- [ ] Recurring contribution schedules -- [ ] Automated goal contributions from expenses -- [ ] Budget integration with shared spaces -- [ ] Mobile app notifications via Firebase -- [ ] Goal templates (e.g., "6-month emergency fund") -- [ ] Expense splitting algorithms -- [ ] Multi-currency support within same space -- [ ] Export reports to PDF/CSV -- [ ] Integration with banking APIs for auto-sync - -## Support - -For issues or questions: -- Check the activity log for audit trail -- Review permission settings for access issues -- Regenerate invite codes if experiencing join problems -- Contact support with space ID and error details - ---- - -**Version:** 1.0.0 -**Last Updated:** January 2024 -**Maintainer:** ExpenseFlow Team diff --git a/GROUP_MANAGEMENT_README.md b/GROUP_MANAGEMENT_README.md deleted file mode 100644 index 2d84db88..00000000 --- a/GROUP_MANAGEMENT_README.md +++ /dev/null @@ -1,330 +0,0 @@ -# Group Expense Management - Implementation Guide - -## Overview -The Group Expense Management feature has been successfully implemented for ExpenseFlow. This feature allows users to create groups, invite members, and manage group expenses collaboratively. - -## Files Created - -### 1. **groups.html** (948 lines) -The main frontend page for group expense management featuring: -- **Create New Group Section**: Form to create groups with name, description, and currency selection -- **Groups List Section**: Display all user's groups with statistics (members, expenses, total amount) -- **Member Management Section**: Add/remove group members with role assignment (Admin/Member) -- **Group Details & Expenses Section**: View group overview and recent group expenses -- **Delete Confirmation Modal**: Safe deletion of groups with confirmation dialog -- **Responsive Design**: Fully responsive layout for desktop, tablet, and mobile devices - -### 2. **groups.js** (400+ lines) -The JavaScript functionality layer providing: -- **GroupManager Class**: Main class handling all group operations - - `init()`: Initialize the system - - `loadGroups()`: Fetch user's groups from API - - `createGroup()`: Create a new group - - `selectGroup()`: Load specific group details - - `addMember()`: Add members to a group - - `removeMember()`: Remove members from a group - - `renderGroupsList()`: Display groups in the UI - - `renderGroupDetails()`: Show group overview stats - - `renderMembers()`: Display group members - - `renderGroupExpenses()`: Show group expenses - -- **Event Handling**: Form submissions, button clicks, navigation -- **API Integration**: RESTful API calls for all operations -- **UI Notifications**: Toast notifications for user feedback -- **Modal Management**: Delete confirmation and other modals - -### 3. **Dashboard Integration** -Modified [index.html](index.html) to: -- Add "Groups" navigation link in the main navbar -- Link to `groups.html` for seamless navigation -- Maintain consistent UI/UX with existing dashboard pages - -## Key Features - -### 1. Create Groups -```javascript -- Group Name (required, max 100 chars) -- Description (optional, max 500 chars) -- Currency Selection (USD, EUR, GBP, INR, JPY, AUD, CAD, etc.) -- Automatic owner assignment to group creator -``` - -### 2. Group Management -```javascript -- View all user's groups in a card-based layout -- Display member count, expense count, and total amount per group -- Edit group details -- Delete groups (with confirmation) -- Switch between groups to view different data -``` - -### 3. Member Management -```javascript -- Add members by email address -- Assign roles (Admin, Member) -- View all group members with their details -- Remove members from groups -- Display member initials and role badges -``` - -### 4. Group Expenses Overview -```javascript -- View group statistics (total members, expenses, amount) -- See currency for each group -- View recent group expenses -- Track who added expenses and when -``` - -## API Endpoints Used - -```javascript -// Backend API Endpoints (expected structure) -GET /api/groups - Fetch user's groups -POST /api/groups - Create new group -GET /api/groups/:id - Get specific group details -PUT /api/groups/:id - Update group -DELETE /api/groups/:id - Delete group -POST /api/groups/:id/members - Add member to group -DELETE /api/groups/:id/members/:userId - Remove member from group -``` - -## UI Design - -### Color Scheme (from expensetracker.css) -- **Primary Gradient**: Linear gradient for buttons and accents -- **Background**: Semi-transparent glass morphism cards -- **Text**: High contrast for readability -- **Accent Color**: Cyan (#40fcd0) for highlights and interactive elements - -### Components - -#### Group Cards -- Glass-morphism design with blur effect -- Hover animations and transitions -- Quick stats display (members, expenses, total) -- Action buttons (edit, delete) - -#### Forms -- Consistent styling with existing dashboard forms -- Input validation and placeholder text -- Two-column grid layout for optimal space usage -- Submit and reset buttons with hover effects - -#### Modal Dialogs -- Centered overlay with backdrop blur -- Slide-up animation on open -- Proper spacing and typography -- Close button and cancel options - -#### Member List -- Avatar with user initials -- Name, email, and role display -- Remove member button -- Animated entrance with stagger effect - -## Responsive Breakpoints - -```css -Desktop (> 1024px): -- Two-column grid for groups section and member management -- Full width components with proper spacing - -Tablet (768px - 1024px): -- Single column layout -- Optimized form fields - -Mobile (< 768px): -- Full width single column layout -- Stacked form rows -- Touch-friendly button sizes -- Reduced padding and font sizes -``` - -## Form Validation - -### Create Group Form -- Group name: Required, max 100 characters -- Description: Optional, max 500 characters -- Currency: Required, predefined list of currencies - -### Add Member Form -- Email: Required, valid email format -- Role: Required, select from Admin/Member -- Validation prevents duplicate emails - -## Error Handling & User Feedback - -### Notification System -- Success notifications (green gradient) -- Error notifications (red/orange gradient) -- Info notifications (purple gradient) -- Auto-dismiss after 3 seconds -- Custom animations for appear/disappear - -### Error States -- Empty states for no groups/members -- Helpful messages guiding users to take action -- Network error handling with user-friendly messages - -## Security Features - -### Data Protection -```javascript -- XSS Prevention: HTML escaping for user input -- CSRF: Bearer token in Authorization header -- Input Validation: Client and server-side -- Email Format Validation -``` - -### Access Control -- Groups accessible only to authorized users -- Members can only be added by group admin -- Delete operations require confirmation -- Role-based access (Admin/Member) - -## Browser Compatibility - -- Chrome/Chromium 90+ -- Firefox 88+ -- Safari 14+ -- Edge 90+ -- Mobile browsers (iOS Safari, Chrome Mobile) - -## Dependencies - -### External Libraries -- Font Awesome 6.4.0 (for icons) -- Google Fonts (Inter font family) -- Fetch API (for HTTP requests) - -### Internal Dependencies -- [expensetracker.css](expensetracker.css) - Shared styles -- [index.html](index.html) - Navigation and header - -## How to Use - -### 1. Accessing the Page -``` -1. Navigate to Dashboard -2. Click "Groups" in the navigation menu -3. Or directly visit: /groups.html -``` - -### 2. Creating a Group -``` -1. Fill in group name (required) -2. Add optional description -3. Select currency for group -4. Click "Create Group" button -5. Success notification confirms creation -``` - -### 3. Managing Members -``` -1. Select a group from the list (click on group card) -2. Scroll to "Member Management" section -3. Enter member email and select role -4. Click "Add Member" button -5. View added members in the list below -6. Click remove button to remove members -``` - -### 4. Viewing Group Details -``` -1. Select a group to view its details -2. See overview cards with key statistics -3. View recent expenses added to group -4. Currency and amounts displayed -``` - -## Performance Optimizations - -1. **Lazy Loading**: Groups loaded on demand -2. **Efficient DOM Updates**: Minimal reflows and repaints -3. **Event Delegation**: Single listeners for multiple elements -4. **CSS Animations**: Hardware-accelerated transforms -5. **Debounced API Calls**: Prevents duplicate requests - -## Future Enhancements - -1. **Group Expense Splitting** - - Add expenses to groups - - Automatic splitting calculation - - Settlement tracking - -2. **Advanced Permissions** - - Custom roles with specific permissions - - Read-only members - - Expense approval workflows - -3. **Notifications** - - Email notifications for member invites - - Real-time expense updates - - Settlement reminders - -4. **Analytics** - - Group spending charts - - Member contribution breakdown - - Spending trends over time - -5. **Bulk Operations** - - Bulk member import via CSV - - Batch expense creation - - Group templates - -6. **Mobile App Integration** - - Native mobile app support - - Offline synchronization - - Push notifications - -## Testing Checklist - -- [ ] Create group with all required fields -- [ ] Create group with only required fields -- [ ] Edit group name and description -- [ ] Delete group (with confirmation) -- [ ] Add member with valid email -- [ ] Add member with invalid email -- [ ] Change member role -- [ ] Remove member -- [ ] View group statistics -- [ ] Switch between groups -- [ ] Test responsive design on mobile -- [ ] Test error notifications -- [ ] Test form validation -- [ ] Test navigation links -- [ ] Test modal dialogs - -## Troubleshooting - -### Groups Not Loading -1. Check browser console for errors -2. Verify API endpoint is accessible -3. Check authentication token -4. Clear browser cache - -### Members Not Appearing -1. Ensure group is selected -2. Check member email validity -3. Verify user exists in system -4. Check API response - -### Styling Issues -1. Verify expensetracker.css is linked -2. Check browser DevTools for CSS errors -3. Clear browser cache -4. Try different browser - -## Support & Contact - -For issues or feature requests: -1. Check the troubleshooting section -2. Review error messages in console -3. Contact development team -4. Submit issue on GitHub - ---- - -**Last Updated**: January 28, 2026 -**Version**: 1.0.0 -**Status**: Production Ready diff --git a/RECEIPT_OCR.md b/RECEIPT_OCR.md deleted file mode 100644 index 2f90d7de..00000000 --- a/RECEIPT_OCR.md +++ /dev/null @@ -1,911 +0,0 @@ -# Smart Receipt OCR & Document Management System - -An intelligent receipt scanning and document management system with OCR (Optical Character Recognition) to automatically extract expense data from receipts and store documents securely. - -## Features - -- 📷 **Smart OCR**: Automatic data extraction from receipt images using Tesseract.js or Google Cloud Vision -- 🔍 **Intelligent Parsing**: Extract merchant name, amount, date, line items, tax, and payment method -- 📁 **Document Management**: Organize receipts in folders with tags and full-text search -- 🔄 **Duplicate Detection**: Perceptual image hashing to identify duplicate receipts -- ✅ **Expense Creation**: Confirm and automatically create expenses from scanned receipts -- ✏️ **Manual Correction**: Edit OCR results with correction history tracking -- 📊 **Confidence Scores**: AI-powered confidence scoring for extracted data -- 🔐 **Secure Storage**: Cloud-based storage with Cloudinary integration - -## Installation - -### Dependencies - -```bash -npm install tesseract.js @google-cloud/vision -``` - -### Optional: Google Cloud Vision Setup - -For enhanced OCR accuracy, configure Google Cloud Vision: - -1. Create a Google Cloud project -2. Enable Cloud Vision API -3. Download service account credentials -4. Set environment variable: - -```bash -GOOGLE_CLOUD_VISION_CREDENTIALS=path/to/credentials.json -``` - -If not configured, the system will fall back to Tesseract.js. - -## Models - -### ReceiptDocument Model - -Stores receipt images and extracted data: - -```javascript -{ - user: ObjectId, - original_image: { - url: String, - public_id: String, - format: String, - size: Number - }, - thumbnail: { - url: String, - public_id: String - }, - processed_text: String, - extracted_data: { - merchant_name: String, - merchant_address: String, - merchant_phone: String, - total_amount: Number, - subtotal: Number, - tax_amount: Number, - tip_amount: Number, - discount_amount: Number, - currency: String, - date: Date, - time: String, - payment_method: String, - card_last_four: String, - transaction_id: String, - invoice_number: String, - category: String, - line_items: [ - { - description: String, - quantity: Number, - unit_price: Number, - total_price: Number - } - ] - }, - confidence_scores: { - overall: Number, - merchant: Number, - amount: Number, - date: Number - }, - status: String, // pending, processing, completed, failed, confirmed - image_hash: String, - is_duplicate: Boolean, - duplicate_of: ObjectId, - expense_created: Boolean, - expense_id: ObjectId, - folder: ObjectId, - tags: [String], - manually_corrected: Boolean, - correction_history: [...] -} -``` - -### DocumentFolder Model - -Hierarchical folder structure for organizing documents: - -```javascript -{ - user: ObjectId, - name: String, - description: String, - color: String, - icon: String, - parent_folder: ObjectId, - path: String, - is_system: Boolean, - metadata: { - document_count: Number, - total_size: Number, - last_updated: Date - } -} -``` - -## API Documentation - -### Upload & Process Receipt - -#### Upload Receipt Image -```http -POST /api/receipts/upload -Authorization: Bearer -Content-Type: multipart/form-data - -{ - "file": , - "folder": "64a1b2c3d4e5f6789abcdef0" // Optional -} -``` - -**Supported Formats:** JPG, PNG, PDF (first page) -**Max Size:** 10MB - -**Response:** -```json -{ - "success": true, - "data": { - "_id": "64a1b2c3d4e5f6789abcdef0", - "status": "processing", - "original_image": { - "url": "https://res.cloudinary.com/...", - "public_id": "receipts/...", - "size": 245678 - }, - "message": "Receipt uploaded successfully. Processing..." - } -} -``` - -The receipt will be automatically processed by OCR in the background. - -### Retrieve Receipts - -#### Get All Receipts -```http -GET /api/receipts -Authorization: Bearer -``` - -**Query Parameters:** -- `status`: pending | processing | completed | failed | confirmed -- `start_date`: Filter by date range start -- `end_date`: Filter by date range end -- `merchant`: Filter by merchant name (partial match) -- `min_amount`: Minimum amount -- `max_amount`: Maximum amount -- `category`: Filter by category -- `tags`: Comma-separated tags -- `folder`: Folder ID -- `limit`: Results per page (default: 50) -- `offset`: Pagination offset (default: 0) - -**Response:** -```json -{ - "success": true, - "count": 15, - "data": [ - { - "_id": "64a1b2c3d4e5f6789abcdef0", - "original_image": { - "url": "https://res.cloudinary.com/..." - }, - "extracted_data": { - "merchant_name": "Starbucks", - "total_amount": 450, - "currency": "INR", - "date": "2024-01-15T00:00:00.000Z", - "category": "food" - }, - "confidence_scores": { - "overall": 92, - "merchant": 95, - "amount": 98, - "date": 85 - }, - "confidence_level": "high", - "status": "completed", - "expense_created": false, - "tags": ["coffee", "personal"], - "createdAt": "2024-01-15T10:30:00.000Z" - } - ] -} -``` - -#### Get Receipt Details -```http -GET /api/receipts/:id -Authorization: Bearer -``` - -**Response:** -```json -{ - "success": true, - "data": { - "_id": "64a1b2c3d4e5f6789abcdef0", - "original_image": { - "url": "https://res.cloudinary.com/...", - "format": "jpg", - "size": 245678 - }, - "thumbnail": { - "url": "https://res.cloudinary.com/..." - }, - "processed_text": "Full OCR text output...", - "extracted_data": { - "merchant_name": "Starbucks Coffee", - "merchant_address": "123 Main St, City", - "merchant_phone": "+91-1234567890", - "total_amount": 450, - "subtotal": 400, - "tax_amount": 50, - "currency": "INR", - "date": "2024-01-15T00:00:00.000Z", - "time": "10:30 AM", - "payment_method": "credit_card", - "card_last_four": "4567", - "transaction_id": "TXN123456789", - "category": "food", - "line_items": [ - { - "description": "Caffe Latte", - "quantity": 2, - "unit_price": 200, - "total_price": 400 - } - ] - }, - "confidence_scores": { - "overall": 92, - "merchant": 95, - "amount": 98, - "date": 85 - }, - "confidence_level": "high", - "status": "completed", - "is_duplicate": false, - "manually_corrected": false, - "tags": ["coffee", "personal"], - "createdAt": "2024-01-15T10:30:00.000Z" - } -} -``` - -### Confirm & Create Expense - -#### Confirm Receipt and Create Expense -```http -POST /api/receipts/:id/confirm -Authorization: Bearer -Content-Type: application/json - -{ - "notes": "Team lunch meeting" -} -``` - -Creates an expense from the receipt data and marks the receipt as confirmed. - -**Response:** -```json -{ - "success": true, - "data": { - "receipt": { - "_id": "64a1b2c3d4e5f6789abcdef0", - "status": "confirmed", - "expense_created": true, - "expense_id": "64a1b2c3d4e5f6789abcdef1" - }, - "expense": { - "_id": "64a1b2c3d4e5f6789abcdef1", - "description": "Starbucks Coffee", - "amount": 450, - "category": "food", - "date": "2024-01-15T00:00:00.000Z" - } - }, - "message": "Expense created successfully from receipt" -} -``` - -### Correct OCR Data - -#### Manually Correct Extracted Data -```http -PUT /api/receipts/:id/correct -Authorization: Bearer -Content-Type: application/json - -{ - "merchant_name": "Starbucks Coffee Co.", - "total_amount": 455, - "date": "2024-01-15", - "category": "food" -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "_id": "64a1b2c3d4e5f6789abcdef0", - "extracted_data": { - "merchant_name": "Starbucks Coffee Co.", - "total_amount": 455, - "date": "2024-01-15T00:00:00.000Z", - "category": "food" - }, - "manually_corrected": true, - "correction_history": [ - { - "field": "total_amount", - "old_value": 450, - "new_value": 455, - "corrected_at": "2024-01-15T11:00:00.000Z" - } - ] - }, - "message": "Receipt data corrected successfully" -} -``` - -### Delete Receipt - -#### Delete Receipt -```http -DELETE /api/receipts/:id -Authorization: Bearer -``` - -**Response:** -```json -{ - "success": true, - "message": "Receipt deleted successfully" -} -``` - -### Search Receipts - -#### Full-Text Search -```http -GET /api/receipts/search?q=starbucks+coffee -Authorization: Bearer -``` - -**Query Parameters:** -- `q`: Search query (required) -- `limit`: Results per page (default: 50) -- `offset`: Pagination offset (default: 0) - -Searches across: -- Merchant name -- Processed OCR text -- Notes -- Tags - -**Response:** -```json -{ - "success": true, - "count": 5, - "data": [ - { - "_id": "64a1b2c3d4e5f6789abcdef0", - "extracted_data": { - "merchant_name": "Starbucks Coffee" - }, - "confidence_scores": { - "overall": 92 - } - } - ] -} -``` - -### Receipt Statistics - -#### Get Receipt Statistics -```http -GET /api/receipts/stats -Authorization: Bearer -``` - -**Response:** -```json -{ - "success": true, - "data": { - "total_receipts": 150, - "by_status": { - "pending": 5, - "processing": 2, - "completed": 120, - "failed": 3, - "confirmed": 20 - }, - "by_category": [ - { - "_id": "food", - "count": 45, - "total_amount": 25000 - }, - { - "_id": "transport", - "count": 30, - "total_amount": 15000 - } - ] - } -} -``` - -### Pending & Unconfirmed Receipts - -#### Get Pending Receipts -```http -GET /api/receipts/pending -Authorization: Bearer -``` - -Returns receipts in 'pending' or 'processing' status. - -#### Get Unconfirmed Receipts -```http -GET /api/receipts/unconfirmed -Authorization: Bearer -``` - -Returns completed receipts that haven't been converted to expenses yet. - -### Folder Management - -#### Create Folder -```http -POST /api/receipts/folders -Authorization: Bearer -Content-Type: application/json - -{ - "name": "Business Receipts", - "description": "All business-related receipts", - "color": "#3498db", - "icon": "briefcase", - "parent_folder": null -} -``` - -**Response:** -```json -{ - "success": true, - "data": { - "_id": "64a1b2c3d4e5f6789abcdef0", - "name": "Business Receipts", - "path": "/Business Receipts", - "color": "#3498db", - "metadata": { - "document_count": 0, - "total_size": 0 - } - } -} -``` - -#### Get Folder Tree -```http -GET /api/receipts/folders/tree -Authorization: Bearer -``` - -**Response:** -```json -{ - "success": true, - "data": [ - { - "_id": "64a1b2c3d4e5f6789abcdef0", - "name": "Business", - "path": "/Business", - "children": [ - { - "_id": "64a1b2c3d4e5f6789abcdef1", - "name": "Travel", - "path": "/Business/Travel", - "children": [] - } - ] - } - ] -} -``` - -#### Move Receipt to Folder -```http -PUT /api/receipts/:id/folder -Authorization: Bearer -Content-Type: application/json - -{ - "folder_id": "64a1b2c3d4e5f6789abcdef0" -} -``` - -### Tag Management - -#### Add Tag to Receipt -```http -POST /api/receipts/:id/tags -Authorization: Bearer -Content-Type: application/json - -{ - "tag": "business" -} -``` - -#### Remove Tag from Receipt -```http -DELETE /api/receipts/:id/tags/:tag -Authorization: Bearer -``` - -#### Get All Tags -```http -GET /api/receipts/tags -Authorization: Bearer -``` - -**Response:** -```json -{ - "success": true, - "data": { - "tags": ["business", "personal", "travel", "food", "transport"], - "tag_counts": { - "business": 45, - "personal": 30, - "travel": 15 - } - } -} -``` - -## Usage Examples - -### 1. Upload and Process Receipt - -```javascript -const formData = new FormData(); -formData.append('file', receiptImage); - -const response = await fetch('/api/receipts/upload', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - }, - body: formData -}); - -const { data } = await response.json(); -console.log('Receipt ID:', data._id); -console.log('Status:', data.status); // 'processing' - -// Poll for completion -const checkStatus = async (receiptId) => { - const statusResponse = await fetch(`/api/receipts/${receiptId}`, { - headers: { 'Authorization': `Bearer ${token}` } - }); - - const { data: receipt } = await statusResponse.json(); - - if (receipt.status === 'completed') { - console.log('Extracted data:', receipt.extracted_data); - return receipt; - } else if (receipt.status === 'failed') { - console.error('OCR failed:', receipt.processing_error); - return null; - } - - // Still processing, check again - setTimeout(() => checkStatus(receiptId), 2000); -}; - -await checkStatus(data._id); -``` - -### 2. Search and Filter Receipts - -```javascript -// Search by merchant -const searchResponse = await fetch('/api/receipts/search?q=starbucks', { - headers: { 'Authorization': `Bearer ${token}` } -}); - -// Filter by date and amount -const filterResponse = await fetch( - '/api/receipts?start_date=2024-01-01&end_date=2024-01-31&min_amount=100&max_amount=1000&category=food', - { headers: { 'Authorization': `Bearer ${token}` } } -); - -const { data: receipts } = await filterResponse.json(); -console.log('Found receipts:', receipts.length); -``` - -### 3. Correct and Confirm Receipt - -```javascript -const receiptId = '64a1b2c3d4e5f6789abcdef0'; - -// Correct any OCR errors -await fetch(`/api/receipts/${receiptId}/correct`, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - total_amount: 455, - merchant_name: 'Starbucks Coffee' - }) -}); - -// Confirm and create expense -const confirmResponse = await fetch(`/api/receipts/${receiptId}/confirm`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - notes: 'Team lunch meeting' - }) -}); - -const { data } = await confirmResponse.json(); -console.log('Created expense:', data.expense._id); -``` - -### 4. Organize with Folders and Tags - -```javascript -// Create folder structure -const businessFolder = await fetch('/api/receipts/folders', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: 'Business', - color: '#3498db' - }) -}); - -const { data: folder } = await businessFolder.json(); - -// Move receipt to folder -await fetch(`/api/receipts/${receiptId}/folder`, { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - folder_id: folder._id - }) -}); - -// Add tags -await fetch(`/api/receipts/${receiptId}/tags`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - tag: 'business' - }) -}); -``` - -## OCR Data Extraction - -### Supported Data Fields - -The OCR service automatically extracts: - -- **Merchant Information**: - - Name - - Address - - Phone number - -- **Transaction Details**: - - Total amount - - Subtotal - - Tax amount - - Tip amount - - Discount amount - - Currency - -- **Date & Time**: - - Transaction date - - Transaction time - -- **Payment Information**: - - Payment method (cash, credit, debit, UPI) - - Last 4 digits of card - - Transaction ID - - Invoice number - -- **Line Items**: - - Item description - - Quantity - - Unit price - - Total price - -- **Category**: - - Auto-inferred from merchant name and items - - Categories: food, transport, shopping, entertainment, utilities, health, education, other - -### Confidence Scores - -Each receipt gets confidence scores: - -- **Overall** (0-100): Weighted average of all scores -- **Merchant** (0-100): Confidence in merchant name extraction -- **Amount** (0-100): Confidence in total amount extraction -- **Date** (0-100): Confidence in date extraction - -**Confidence Levels**: -- High: ≥90% (Green) -- Medium: 70-89% (Yellow) -- Low: <70% (Red) - -Receipts with low confidence scores should be manually reviewed. - -## Duplicate Detection - -The system uses image hashing to detect duplicate receipts: - -1. **Hash Generation**: Each uploaded image gets a perceptual hash -2. **Duplicate Check**: Compares hash against existing receipts -3. **Flagging**: Duplicates are flagged with `is_duplicate: true` -4. **Reference**: `duplicate_of` field points to original receipt - -**Note**: Duplicate receipts can still be processed but are marked for review. - -## Best Practices - -1. **Image Quality**: - - Use clear, well-lit photos - - Ensure receipt is fully visible and in focus - - Avoid shadows and glare - - Supported formats: JPG, PNG, PDF - -2. **Review OCR Results**: - - Always check confidence scores - - Review receipts with confidence < 70% - - Manually correct errors before confirming - -3. **Organization**: - - Create folders for different expense categories - - Use tags for easy filtering - - Move processed receipts to appropriate folders - -4. **Regular Cleanup**: - - Review and confirm pending receipts weekly - - Delete failed or duplicate receipts - - Archive old receipts - -5. **Expense Creation**: - - Confirm receipts to create expenses automatically - - Add notes before confirming for better tracking - - Double-check amounts and categories - -## Troubleshooting - -### OCR Failed - -**Causes**: -- Poor image quality -- Handwritten receipts (not supported) -- Non-English text (if using Tesseract) -- Damaged or faded receipts - -**Solutions**: -- Retake photo with better lighting -- Try Google Cloud Vision (more accurate) -- Manually enter data - -### Low Confidence Scores - -**Solutions**: -- Review extracted data -- Manually correct errors -- Retake photo if possible - -### Duplicate Detection Issues - -**False Positives**: -- Different receipts flagged as duplicates -- Check image hash manually -- Report if persistent - -**False Negatives**: -- Duplicate receipts not detected -- May occur with low-quality images -- Manually mark as duplicate - -### Processing Timeout - -If receipt stays in 'processing' status for >5 minutes: -1. Check server logs -2. Refresh receipt status -3. Re-upload if still stuck - -## Error Handling - -All endpoints return consistent error responses: - -```json -{ - "success": false, - "error": "Error message here" -} -``` - -**Common HTTP Status Codes**: -- `200`: Success -- `201`: Created -- `400`: Bad request / validation error -- `401`: Unauthorized -- `404`: Not found -- `413`: File too large -- `415`: Unsupported media type -- `500`: Server error - -## Security - -- JWT authentication required for all endpoints -- Receipts are private to uploading user -- Images stored securely on Cloudinary -- File size limits enforced (10MB max) -- Supported formats validated -- Malicious file upload prevention - -## Performance - -- **OCR Processing**: 2-10 seconds per receipt -- **Duplicate Detection**: <1 second -- **Image Upload**: Depends on file size and connection -- **Search**: Full-text search indexed for fast results - -## Limitations - -- Maximum file size: 10MB -- Supported formats: JPG, PNG, PDF -- OCR accuracy depends on image quality -- Handwritten receipts not supported -- Best results with printed receipts in English - -## Future Enhancements - -- Multi-language OCR support -- Bulk upload processing -- Advanced duplicate detection (similar amounts/dates) -- Receipt templates for common merchants -- Mobile app integration -- Batch expense creation -- Export receipt data (CSV, PDF) - -## License - -MIT License - see LICENSE file for details diff --git a/models/DepreciationSchedule.js b/models/DepreciationSchedule.js new file mode 100644 index 00000000..33bf5d4f --- /dev/null +++ b/models/DepreciationSchedule.js @@ -0,0 +1,53 @@ +const mongoose = require('mongoose'); + +const depreciationScheduleSchema = new mongoose.Schema({ + assetId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'FixedAsset', + required: true, + index: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + period: { + year: { type: Number, required: true }, + month: { type: Number, required: true } + }, + openingBookValue: { + type: Number, + required: true + }, + depreciationAmount: { + type: Number, + required: true + }, + closingBookValue: { + type: Number, + required: true + }, + isPosted: { + type: Boolean, + default: false + }, + postedDate: Date, + transactionId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Transaction' + }, + methodUsed: String, + metadata: { + daysInPeriod: Number, + fullYearCharge: Number + } +}, { + timestamps: true +}); + +depreciationScheduleSchema.index({ userId: 1, 'period.year': 1, 'period.month': 1 }); +depreciationScheduleSchema.index({ assetId: 1, 'period.year': 1 }); + +module.exports = mongoose.model('DepreciationSchedule', depreciationScheduleSchema); diff --git a/models/FixedAsset.js b/models/FixedAsset.js index 2386435d..a936c973 100644 --- a/models/FixedAsset.js +++ b/models/FixedAsset.js @@ -4,71 +4,82 @@ const fixedAssetSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', + required: true, + index: true + }, + name: { + type: String, + required: true, + trim: true + }, + assetCode: { + type: String, + unique: true, required: true }, - name: { type: String, required: true }, - description: String, category: { type: String, - enum: ['furniture', 'electronics', 'machinery', 'vehicles', 'real_estate', 'software', 'other'], + enum: ['IT Equipment', 'Furniture', 'Machinery', 'Buildings', 'Vehicles', 'Others'], + required: true, + index: true + }, + description: String, + purchaseDate: { + type: Date, + required: true + }, + purchasePrice: { + type: Number, + required: true, + min: 0 + }, + currency: { + type: String, + default: 'INR' + }, + salvageValue: { + type: Number, + default: 0 + }, + usefulLife: { + type: Number, // in years required: true }, - serialNumber: { type: String, unique: true, sparse: true }, - modelNumber: String, - manufacturer: String, - - // Financials - purchaseDate: { type: Date, required: true }, - purchasePrice: { type: Number, required: true }, - currency: { type: String, default: 'INR' }, - salvageValue: { type: Number, default: 0 }, - usefulLifeYears: { type: Number, required: true }, - - // Depreciation Config depreciationMethod: { type: String, - enum: ['SLM', 'DBM'], // SLM: Straight Line, DBM: Declining Balance - default: 'SLM' + enum: ['Straight Line', 'Written Down Value'], + default: 'Straight Line' + }, + depreciationRate: { + type: Number, // for WDV primarily + default: 0 }, - depreciationRate: { type: Number, default: 0 }, // For DBM - - // Status status: { type: String, - enum: ['active', 'disposed', 'maintenance', 'written_off'], - default: 'active' + enum: ['Active', 'Disposed', 'Transferred', 'Written Off'], + default: 'Active' }, location: String, department: String, - assignedTo: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, - - // Links - procurementOrderId: { type: mongoose.Schema.Types.ObjectId, ref: 'ProcurementOrder' }, - - // Current Values - currentBookValue: { type: Number }, - lastDepreciationDate: Date, - - notes: String, - maintenanceHistory: [{ - date: { type: Date, default: Date.now }, - type: { type: String, enum: ['routine', 'repair', 'upgrade'] }, - description: String, - cost: { type: Number, default: 0 }, - performedBy: String, - nextServiceDate: Date - }], - isDeleted: { type: Boolean, default: false } + currentBookValue: { + type: Number, + required: true + }, + accumulatedDepreciation: { + type: Number, + default: 0 + }, + disposalDetails: { + date: Date, + price: Number, + gainLoss: Number, + reason: String + } }, { timestamps: true }); -// Calculate initial book value before saving -fixedAssetSchema.pre('save', function (next) { - if (this.isNew) { - this.currentBookValue = this.purchasePrice; - } - next(); -}); +fixedAssetSchema.index({ assetCode: 1 }); +fixedAssetSchema.index({ userId: 1, status: 1 }); module.exports = mongoose.model('FixedAsset', fixedAssetSchema); diff --git a/public/asset-management.html b/public/asset-management.html new file mode 100644 index 00000000..32308a4e --- /dev/null +++ b/public/asset-management.html @@ -0,0 +1,193 @@ + + + + + + + Fixed Asset Management - ExpenseFlow + + + + + + + + + +
+ + + +
+
+
+
+ Total Assets +

-

+
+
+
+
+
+ Net Book Value +

-

+
+
+
+
+
+ Acc. Depreciation +

-

+
+
+
+ +
+ +
+
+

Value by Category

+
+
+ +
+
+ + +
+
+

Asset Register

+
+ +
+
+
+ + + + + + + + + + + + + + +
Asset DetailsCategoryPurchase PriceRemaining LifeCurrent BVActions
+
+
+
+ + + +
+ + + + + + + + + \ No newline at end of file diff --git a/public/expensetracker.css b/public/expensetracker.css index 84d83d09..457517e5 100644 --- a/public/expensetracker.css +++ b/public/expensetracker.css @@ -10210,67 +10210,38 @@ input:checked + .toggle-slider::before { .summary-row span.loss { color: #ff6b6b; } -/* Intercompany Reconciliation Hub Styles */ -.entity-balance-grid { +/* Asset Management Styles */ +.detail-grid-3 { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 15px; - margin-top: 15px; -} - -.balance-card { - padding: 15px; - display: flex; - flex-direction: column; - gap: 10px; - border: 1px solid rgba(255, 255, 255, 0.05); -} - -.pair-names { - font-size: 0.9rem; - color: #8892b0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - padding-bottom: 8px; -} - -.net-value { - font-size: 1.4rem; - font-weight: 700; + grid-template-columns: 1.5fr 1fr 1fr; + gap: 20px; + margin-top: 20px; } -.net-value span { - display: block; - font-size: 0.75rem; - font-weight: 400; - margin-top: 4px; - color: #8892b0; +.scroll-table { + max-height: 400px; + overflow-y: auto; } -.net-value.pos { color: #64ffda; } -.net-value.neg { color: #ff6b6b; } - -.advice-box { - text-align: center; - padding: 10px; +.detail-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding-bottom: 10px; } -.advice-summary { - margin-bottom: 15px; +.asset-actions-card button { + height: 45px; + font-size: 0.95rem; } -.advice-summary label { - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 1px; - color: #8892b0; -} +.mb-10 { margin-bottom: 10px; } -.advice-details { - display: flex; - justify-content: space-around; - font-size: 0.85rem; - color: #8892b0; - margin-bottom: 20px; +@media (max-width: 1200px) { + .detail-grid-3 { + grid-template-columns: 1fr; + } } .type-tag { diff --git a/public/js/asset-controller.js b/public/js/asset-controller.js index ec395c9e..e166eafa 100644 --- a/public/js/asset-controller.js +++ b/public/js/asset-controller.js @@ -1,203 +1,206 @@ /** - * Asset Lifecycle & Procurement Controller + * Asset Controller + * Handles Asset Lifecycle UI and Analytics */ +let categoryChart = null; +let projectionChart = null; +let currentAssetId = null; + document.addEventListener('DOMContentLoaded', () => { - loadDashboard(); - loadOrders(); - setupPRForm(); + initAssetBox(); }); -async function loadDashboard() { +async function initAssetBox() { + await fetchAssetSummary(); + await loadAssets(); +} + +async function fetchAssetSummary() { try { - const res = await fetch('/api/procurement/assets/dashboard', { + const response = await fetch('/api/assets/summary', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); - const { data } = await res.json(); - - updateStats(data.stats); - renderAssets(data.assets); - initCharts(data.stats); - } catch (err) { - console.error('Failed to load asset dashboard:', err); - } -} + const summary = await response.json(); -function updateStats(stats) { - document.getElementById('total-book-value').textContent = `₹${stats.totalBookValue.toLocaleString()}`; - document.getElementById('accumulated-depreciation').textContent = `₹${stats.totalDepreciation.toLocaleString()}`; -} + document.getElementById('total-asset-count').textContent = summary.totalCount; + document.getElementById('total-book-value').textContent = `₹${summary.totalBookValue.toLocaleString()}`; + document.getElementById('total-accumulated-dep').textContent = `₹${summary.totalAccumulatedDep.toLocaleString()}`; -function renderAssets(assets) { - const grid = document.getElementById('assets-grid'); - if (!assets || assets.length === 0) { - grid.innerHTML = '
No active assets found. Assets are auto-created when procurement items are received.
'; - return; + renderCategoryChart(summary.byCategory); + } catch (err) { + console.error('Error fetching summary:', err); } - - grid.innerHTML = assets.map(asset => ` -
-
- -
-
-

${asset.name}

- ${asset.serialNumber || 'No Serial'} -
- -
₹${asset.currentBookValue.toLocaleString()}
-
-
-
-
-
- ${asset.usefulLifeYears}Y Useful Life -
-
- -
- `).join(''); } -function getCategoryIcon(cat) { - const icons = { - 'electronics': 'fa-laptop', - 'furniture': 'fa-couch', - 'machinery': 'fa-tools', - 'vehicles': 'fa-car', - 'real_estate': 'fa-building' - }; - return icons[cat] || 'fa-box'; -} +function renderCategoryChart(data) { + const ctx = document.getElementById('categoryChart').getContext('2d'); + if (categoryChart) categoryChart.destroy(); -function calculateLifeUsed(asset) { - const purchase = new Date(asset.purchaseDate); - const monthsOwned = (new Date() - purchase) / (1000 * 60 * 60 * 24 * 30); - const totalMonths = asset.usefulLifeYears * 12; - return Math.min(100, (monthsOwned / totalMonths) * 100); + categoryChart = new Chart(ctx, { + type: 'pie', + data: { + labels: Object.keys(data), + datasets: [{ + data: Object.values(data), + backgroundColor: ['#48dbfb', '#64ffda', '#ff9f43', '#ff6b6b', '#8892b0'] + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { position: 'bottom', labels: { color: '#8892b0' } } } + } + }); } -async function loadOrders() { +async function loadAssets() { + const status = document.getElementById('status-filter').value; try { - const res = await fetch('/api/procurement/orders', { + const url = status === 'All' ? '/api/assets' : `/api/assets?status=${status}`; + const response = await fetch(url, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); - const { data } = await res.json(); - - const list = document.getElementById('orders-list'); - list.innerHTML = data.map(order => ` - - ${order.orderNumber} - ${order.title} - ${order.status.replace('_', ' ')} - ₹${order.totalAmount.toLocaleString()} + const assets = await response.json(); + + const tbody = document.getElementById('assets-table-body'); + tbody.innerHTML = ''; + + assets.forEach(a => { + const age = Math.floor((new Date() - new Date(a.purchaseDate)) / (1000 * 60 * 60 * 24 * 365)); + const lifeRemaining = Math.max(0, a.usefulLife - age); + + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${a.name}
${a.assetCode} + ${a.category} + ₹${a.purchasePrice.toLocaleString()} + ${lifeRemaining} Years + ₹${a.currentBookValue.toLocaleString()} - ${order.status === 'ordered' ? `` : ''} - ${order.status === 'draft' ? `` : ''} + - - `).join(''); - - document.getElementById('pending-pr-count').textContent = data.filter(o => o.status === 'pending_approval').length; + `; + tbody.appendChild(tr); + }); } catch (err) { - console.error('Failed to load orders:', err); + console.error('Error loading assets:', err); } } -function switchSection(sec) { - document.querySelectorAll('.inventory-content section').forEach(s => s.classList.add('hidden')); - document.getElementById(`${sec}-section`).classList.remove('hidden'); +async function viewAssetDetails(id) { + currentAssetId = id; + try { + const response = await fetch(`/api/assets/${id}`, { + headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } + }); + const { asset, schedule, projections } = await response.json(); + + document.getElementById('detail-asset-name').textContent = asset.name; + document.getElementById('asset-detail-container').classList.remove('hidden'); + + renderHistoryTable(schedule); + renderProjectionChart(projections); + + window.scrollTo({ top: document.getElementById('asset-detail-container').offsetTop - 50, behavior: 'smooth' }); + } catch (err) { + console.error('Error viewing details:', err); + } +} - document.querySelectorAll('.side-nav .nav-btn').forEach(b => b.classList.remove('active')); - event.currentTarget.classList.add('active'); +function renderHistoryTable(schedule) { + const tbody = document.getElementById('history-table-body'); + tbody.innerHTML = schedule.map(s => ` + + ${s.period.month}/${s.period.year} + ${s.methodUsed} + ₹${s.depreciationAmount.toFixed(0)} + ₹${s.closingBookValue.toFixed(0)} + + `).join(''); } -function initCharts(stats) { - const ctx = document.getElementById('assetCategoryChart').getContext('2d'); - new Chart(ctx, { - type: 'doughnut', +function renderProjectionChart(projections) { + const ctx = document.getElementById('projectionChart').getContext('2d'); + if (projectionChart) projectionChart.destroy(); + + projectionChart = new Chart(ctx, { + type: 'line', data: { - labels: Object.keys(stats.categoryDistribution), + labels: projections.filter((_, i) => i % 12 === 0).map(p => `Year ${p.month / 12}`), datasets: [{ - data: Object.values(stats.categoryDistribution), - backgroundColor: ['#64ffda', '#48dbfb', '#ff9f43', '#ff6b6b', '#54a0ff'], - borderWidth: 0 + label: 'Projected Book Value', + data: projections.filter((_, i) => i % 12 === 0).map(p => p.remainingValue), + borderColor: '#48dbfb', + backgroundColor: 'rgba(72, 219, 251, 0.1)', + fill: true }] }, options: { - plugins: { legend: { position: 'bottom', labels: { color: '#8892b0' } } } + scales: { + y: { ticks: { color: '#8892b0' }, grid: { color: 'rgba(255,255,255,0.05)' } }, + x: { ticks: { color: '#8892b0' }, grid: { color: 'rgba(255,255,255,0.05)' } } + } } }); } -function openPRModal() { - document.getElementById('pr-modal').classList.remove('hidden'); -} +async function runDepreciationCycle() { + if (!confirm('Run batch depreciation for all active assets for the current month?')) return; -function closePRModal() { - document.getElementById('pr-modal').classList.add('hidden'); -} - -function addPRItemRow() { - const div = document.createElement('div'); - div.className = 'item-row'; - div.innerHTML = ` - - - - - `; - document.getElementById('pr-items-list').appendChild(div); -} - -async function runDepreciation() { - if (!confirm('This will calculate and record depreciation for the current month. Proceed?')) return; + const now = new Date(); + try { + const response = await fetch('/api/assets/run-depreciation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ year: now.getFullYear(), month: now.getMonth() + 1 }) + }); - const res = await fetch('/api/procurement/admin/run-depreciation', { - method: 'POST', - headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } - }); - const result = await res.json(); - if (result.success) { - alert(`Successfully processed ${result.processed} assets.`); - loadDashboard(); + if (response.ok) { + alert('Monthly depreciation cycle completed successfully.'); + initAssetBox(); + if (currentAssetId) viewAssetDetails(currentAssetId); + } + } catch (err) { + console.error('Error running dep cycle:', err); } } -function setupPRForm() { - const form = document.getElementById('pr-form'); - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const items = Array.from(document.querySelectorAll('.item-row')).map(row => ({ - name: row.querySelector('.item-name').value, - quantity: parseInt(row.querySelector('.item-qty').value), - unitPrice: parseFloat(row.querySelector('.item-price').value), - totalPrice: parseInt(row.querySelector('.item-qty').value) * parseFloat(row.querySelector('.item-price').value), - category: 'IT' // Default for now - })); - - const prData = { - title: document.getElementById('pr-title').value, - department: document.getElementById('pr-dept').value, - items - }; - - const res = await fetch('/api/procurement/requisition', { +function openAssetModal() { document.getElementById('asset-modal').style.display = 'block'; } +function closeAssetModal() { document.getElementById('asset-modal').style.display = 'none'; } +function closeDetails() { document.getElementById('asset-detail-container').classList.add('hidden'); } + +document.getElementById('asset-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const assetData = { + name: document.getElementById('a-name').value, + assetCode: document.getElementById('a-code').value, + category: document.getElementById('a-cat').value, + purchaseDate: document.getElementById('a-date').value, + purchasePrice: Number(document.getElementById('a-price').value), + salvageValue: Number(document.getElementById('a-salvage').value || 0), + usefulLife: Number(document.getElementById('a-life').value), + depreciationMethod: document.getElementById('a-method').value + }; + + try { + const response = await fetch('/api/assets', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, - body: JSON.stringify(prData) + body: JSON.stringify(assetData) }); - if (res.ok) { - closePRModal(); - loadOrders(); + if (response.ok) { + closeAssetModal(); + initAssetBox(); } - }); -} + } catch (err) { + console.error('Error registering asset:', err); + } +}); diff --git a/routes/assets.js b/routes/assets.js new file mode 100644 index 00000000..202ecf97 --- /dev/null +++ b/routes/assets.js @@ -0,0 +1,67 @@ +const express = require('express'); +const router = express.Router(); +const auth = require('../middleware/auth'); +const assetService = require('../services/assetService'); + +// Register Asset +router.post('/', auth, async (req, res) => { + try { + const asset = await assetService.registerAsset(req.user._id, req.body); + res.status(201).json(asset); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}); + +// List Assets +router.get('/', auth, async (req, res) => { + try { + const assets = await assetService.getAssets(req.user._id, req.query); + res.json(assets); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}); + +// Get Summary +router.get('/summary', auth, async (req, res) => { + try { + const summary = await assetService.getSummary(req.user._id); + res.json(summary); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}); + +// Get Asset Details +router.get('/:id', auth, async (req, res) => { + try { + const details = await assetService.getAssetDetails(req.user._id, req.params.id); + res.json(details); + } catch (err) { + res.status(404).json({ message: err.message }); + } +}); + +// Run Manual Depreciation +router.post('/run-depreciation', auth, async (req, res) => { + try { + const { year, month } = req.body; + const result = await assetService.runDepreciationForUser(req.user._id, year, month); + res.json({ message: 'Depreciation cycle completed', results: result }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}); + +// Dispose Asset +router.post('/:id/dispose', auth, async (req, res) => { + try { + const asset = await assetService.disposeAsset(req.user._id, req.params.id, req.body); + res.json(asset); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}); + +module.exports = router; diff --git a/server.js b/server.js index 93664ea8..a137d13a 100644 --- a/server.js +++ b/server.js @@ -297,7 +297,7 @@ app.use('/api/profile', require('./routes/profile')); // Serve uploaded avatars app.use('/uploads', express.static(require('path').join(__dirname, 'uploads'))); app.use('/api/treasury', require('./routes/treasury')); -app.use('/api/reconciliation', require('./routes/reconciliation')); +app.use('/api/assets', require('./routes/assets')); // Import error handling middleware const { errorHandler, notFoundHandler } = require('./middleware/errorMiddleware'); diff --git a/services/assetService.js b/services/assetService.js index 8bc62604..47e52d41 100644 --- a/services/assetService.js +++ b/services/assetService.js @@ -1,128 +1,81 @@ const FixedAsset = require('../models/FixedAsset'); -const AssetDepreciation = require('../models/AssetDepreciation'); +const depreciationEngine = require('./depreciationEngine'); +const DepreciationSchedule = require('../models/DepreciationSchedule'); class AssetService { - /** - * Calculate and apply monthly depreciation for all active assets - */ - async runBatchDepreciation() { - const assets = await FixedAsset.find({ status: 'active', isDeleted: false }); - const results = []; - - const now = new Date(); - const currentMonth = now.getMonth(); - const currentYear = now.getFullYear(); - - for (const asset of assets) { - // Skip if already depreciated this month - if (asset.lastDepreciationDate && - asset.lastDepreciationDate.getMonth() === currentMonth && - asset.lastDepreciationDate.getFullYear() === currentYear) { - continue; - } - - const depAmount = this.calculateMonthlyDepreciation(asset); - - if (depAmount > 0) { - const openingValue = asset.currentBookValue; - const closingValue = Math.max(asset.salvageValue, asset.currentBookValue - depAmount); - - const entry = new AssetDepreciation({ - assetId: asset._id, - date: now, - depreciationAmount: openingValue - closingValue, - openingBookValue: openingValue, - closingBookValue: closingValue, - method: asset.depreciationMethod, - period: { month: currentMonth + 1, year: currentYear } - }); - - await entry.save(); - - asset.currentBookValue = closingValue; - asset.lastDepreciationDate = now; - await asset.save(); - - results.push({ assetId: asset._id, amount: depAmount }); - } - } - - return results; + async registerAsset(userId, assetData) { + const asset = new FixedAsset({ + ...assetData, + userId, + currentBookValue: assetData.purchasePrice + }); + return await asset.save(); } - calculateMonthlyDepreciation(asset) { - if (asset.currentBookValue <= asset.salvageValue) return 0; - - if (asset.depreciationMethod === 'SLM') { - // Straight Line Method: (Cost - Salvage) / Useful Life - const annualDep = (asset.purchasePrice - asset.salvageValue) / asset.usefulLifeYears; - return annualDep / 12; - } else if (asset.depreciationMethod === 'DBM') { - // Declining Balance Method: Current Value * Rate - const rate = asset.depreciationRate || (2 / asset.usefulLifeYears); // Double Declining default - return (asset.currentBookValue * rate) / 12; - } - - return 0; + async getAssets(userId, filters = {}) { + const query = { userId }; + if (filters.category) query.category = filters.category; + if (filters.status) query.status = filters.status; + return await FixedAsset.find(query).sort({ purchaseDate: -1 }); } - async getAssetDashboard(userId) { - const assets = await FixedAsset.find({ userId, isDeleted: false }); - const totalValue = assets.reduce((sum, a) => sum + a.currentBookValue, 0); - const totalPurchase = assets.reduce((sum, a) => sum + a.purchasePrice, 0); + async getAssetDetails(userId, assetId) { + const asset = await FixedAsset.findOne({ _id: assetId, userId }); + if (!asset) throw new Error('Asset not found'); - const categoryDist = {}; - assets.forEach(a => { - categoryDist[a.category] = (categoryDist[a.category] || 0) + a.currentBookValue; - }); + const schedule = await DepreciationSchedule.find({ assetId }).sort({ 'period.year': -1, 'period.month': -1 }); + const projections = depreciationEngine.generateProjections(asset); - return { - assets, - stats: { - count: assets.length, - totalBookValue: totalValue, - totalDepreciation: totalPurchase - totalValue, - categoryDistribution: categoryDist - } - }; + return { asset, schedule, projections }; } - async getDepreciationHistory(assetId) { - return await AssetDepreciation.find({ assetId }).sort({ date: -1 }); + async runDepreciationForUser(userId, year, month) { + return await depreciationEngine.runMonthlyRoutine(userId, { year, month }); } - /** - * Record maintenance activity for an asset - */ - async recordMaintenance(assetId, maintenanceData) { - const asset = await FixedAsset.findById(assetId); - if (!asset) throw new Error('Asset not find'); + async disposeAsset(userId, assetId, disposalData) { + const asset = await FixedAsset.findOne({ _id: assetId, userId }); + if (!asset) throw new Error('Asset not found'); - asset.maintenanceHistory.push(maintenanceData); + const gainLoss = disposalData.price - asset.currentBookValue; - // If it's an upgrade, we might want to increase the book value or life - if (maintenanceData.type === 'upgrade' && maintenanceData.capitalize) { - asset.currentBookValue += maintenanceData.cost; - } + asset.status = 'Disposed'; + asset.disposalDetails = { + ...disposalData, + gainLoss + }; - await asset.save(); - return asset; + return await asset.save(); } - /** - * Mark an asset as disposed/sold - */ - async disposeAsset(assetId, disposalData) { - const asset = await FixedAsset.findById(assetId); - if (!asset) throw new Error('Asset not found'); + async getSummary(userId) { + const assets = await FixedAsset.find({ userId }); - const gainLoss = disposalData.saleProceeds - asset.currentBookValue; + return { + totalCount: assets.length, + totalBookValue: assets.reduce((sum, a) => sum + a.currentBookValue, 0), + totalAccumulatedDep: assets.reduce((sum, a) => sum + a.accumulatedDepreciation, 0), + byCategory: assets.reduce((acc, a) => { + acc[a.category] = (acc[a.category] || 0) + a.currentBookValue; + return acc; + }, {}) + }; + } - asset.status = 'disposed'; - asset.notes = (asset.notes || '') + `\nDisposed on ${disposalData.date}. Proceeds: ${disposalData.saleProceeds}. Gain/Loss: ${gainLoss}`; + async runBatchDepreciation() { + const users = await require('../models/User').find({ isActive: true }); + const now = new Date(); + const results = []; - await asset.save(); - return { asset, gainLoss }; + for (const user of users) { + try { + const batch = await this.runDepreciationForUser(user._id, now.getFullYear(), now.getMonth() + 1); + results.push(...batch); + } catch (err) { + console.error(`[AssetService] Batch dep failed for user ${user._id}:`, err.message); + } + } + return results; } } diff --git a/services/depreciationEngine.js b/services/depreciationEngine.js new file mode 100644 index 00000000..a33255b6 --- /dev/null +++ b/services/depreciationEngine.js @@ -0,0 +1,151 @@ +const FixedAsset = require('../models/FixedAsset'); +const DepreciationSchedule = require('../models/DepreciationSchedule'); + +class DepreciationEngine { + /** + * Calculate depreciation for a specific asset and period + */ + calculateDepreciation(asset, period) { + const { year, month } = period; + const method = asset.depreciationMethod; + let amount = 0; + + if (method === 'Straight Line') { + // Amount = (Cost - Salvage) / Life / 12 + const annualDep = (asset.purchasePrice - asset.salvageValue) / asset.usefulLife; + amount = annualDep / 12; + } else if (method === 'Written Down Value') { + // Amount = Current Book Value * Rate / 12 (approximate) + // Or precise: Current Book Value * (Rate/100) / 12 + const rate = asset.depreciationRate || (1 / asset.usefulLife) * 2 * 100; // Double declining approx + amount = (asset.currentBookValue * (rate / 100)) / 12; + } + + // Ensure we don't go below salvage value + if (asset.currentBookValue - amount < asset.salvageValue) { + amount = asset.currentBookValue - asset.salvageValue; + } + + return Math.max(0, amount); + } + + /** + * Run monthly depreciation routine for all active assets of a user + */ + async runMonthlyRoutine(userId, period) { + const assets = await FixedAsset.find({ userId, status: 'Active' }); + const results = []; + + for (const asset of assets) { + const amount = this.calculateDepreciation(asset, period); + + if (amount <= 0) continue; + + const schedule = new DepreciationSchedule({ + assetId: asset._id, + userId, + period, + openingBookValue: asset.currentBookValue, + depreciationAmount: amount, + closingBookValue: asset.currentBookValue - amount, + methodUsed: asset.depreciationMethod + }); + + await schedule.save(); + + // Update asset status + asset.currentBookValue -= amount; + asset.accumulatedDepreciation += amount; + + if (asset.currentBookValue <= asset.salvageValue) { + // Asset fully depreciated + } + + await asset.save(); + results.push(schedule); + } + + return results; + } + + /** + * Generate 5-year projection for an asset + */ + generateProjections(asset) { + const projections = []; + let tempValue = asset.currentBookValue; + const method = asset.depreciationMethod; + const salvage = asset.salvageValue; + const rate = asset.depreciationRate || (1 / asset.usefulLife) * 2 * 100; + + for (let i = 1; i <= 60; i++) { // 60 months + let amount = 0; + if (method === 'Straight Line') { + amount = (asset.purchasePrice - salvage) / asset.usefulLife / 12; + } else { + amount = (tempValue * (rate / 100)) / 12; + } + + if (tempValue - amount < salvage) { + amount = tempValue - salvage; + } + + if (amount <= 0) break; + + tempValue -= amount; + projections.push({ + month: i, + amount, + remainingValue: tempValue + }); + } + + return projections; + } + /** + * Calculate depreciation based on tax laws (Income Tax Act - India) + * Takes block of assets into account + */ + calculateTaxDepreciation(blockValue, rate, period) { + // Simplified block-wise calculation + return (blockValue * (rate / 100)) / 12; // Monthly charge + } + + /** + * Handle asset write-off (Total loss) + */ + async writeOffAsset(userId, assetId, reason) { + const asset = await FixedAsset.findOne({ _id: assetId, userId }); + if (!asset) throw new Error('Asset not found'); + + const lossAmount = asset.currentBookValue; + + asset.status = 'Written Off'; + asset.accumulatedDepreciation += lossAmount; + asset.currentBookValue = 0; + asset.disposalDetails = { + date: new Date(), + price: 0, + gainLoss: -lossAmount, + reason: reason || 'Asset written off due to irreparable damage' + }; + + return await asset.save(); + } + + /** + * Revalue an asset (IFRS/GAAP) + */ + async revalueAsset(userId, assetId, newValue) { + const asset = await FixedAsset.findOne({ _id: assetId, userId }); + if (!asset) throw new Error('Asset not found'); + + const adjustment = newValue - asset.currentBookValue; + asset.currentBookValue = newValue; + + // In a real system, this would post to a Revaluation Reserve + return await asset.save(); + } +} + +module.exports = new DepreciationEngine();