diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md
deleted file mode 100644
index c88e0d89..00000000
--- a/DATABASE_SETUP.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# Database Setup Guide
-
-This document provides instructions for setting up and configuring the PostgreSQL database for the inventory management system.
-
-## Environment Variables
-
-The application requires the following PostgreSQL environment variables to be set:
-
-- `DATABASE_URL` - PostgreSQL connection string in the format `postgresql://username:password@host:port/database`
-- `PGHOST` - PostgreSQL server hostname
-- `PGUSER` - PostgreSQL username
-- `PGPASSWORD` - PostgreSQL password
-- `PGDATABASE` - PostgreSQL database name
-- `PGPORT` - PostgreSQL server port (defaults to 5432)
-
-## Deployment Configuration
-
-When deploying the application, you need to configure the database connection in one of two ways:
-
-### Option 1: Using DATABASE_URL (Recommended)
-
-1. Go to your Replit project's "Secrets" tab in the Tools panel
-2. Add a new secret with key `DATABASE_URL` and value in the format:
- ```
- postgresql://username:password@host:port/database
- ```
-3. Save the secret
-
-### Option 2: Using Individual PostgreSQL Parameters
-
-If you prefer to set individual parameters (for better secret management):
-
-1. Go to your Replit project's "Secrets" tab in the Tools panel
-2. Add the following secrets:
- - `PGHOST` - Your PostgreSQL server hostname
- - `PGUSER` - Your PostgreSQL username
- - `PGPASSWORD` - Your PostgreSQL password
- - `PGDATABASE` - Your PostgreSQL database name
- - `PGPORT` - Your PostgreSQL server port (optional, defaults to 5432)
-3. Save all secrets
-
-The application will automatically detect and use these individual parameters if `DATABASE_URL` is not set.
-
-## Automatic Setup
-
-The application will automatically:
-
-1. Check database connection on startup
-2. Create required tables if they don't exist
-3. Set up database schema using Drizzle ORM
-
-If you encounter database connection issues, please verify your environment variables are correctly set.
-
-## Manual Database Initialization
-
-If you need to manually initialize the database:
-
-```bash
-# Run the database setup script
-node setup-db.js
-
-# OR manually use Drizzle to push the schema
-npm run db:push
-```
-
-## Database Schema
-
-The database uses the following schema (defined in `shared/schema.ts`):
-
-- `users` - User accounts and authentication
-- `inventoryItems` - Inventory product information
-- `categories` - Product categories
-- `warehouses` - Warehouse/location information
-- `suppliers` - Supplier information
-- `stockMovements` - Inventory movement history
-- `purchaseRequisitions` - Purchase requisition records
-- `purchaseOrders` - Purchase order records
-- `reorderRequests` - Restock request records
-- `appSettings` - Application configuration settings
-- `customRoles` - Custom user roles
-- `permissions` - Role-based access control permissions
-- `userAccessLogs` - Security and access logging
-
-## Adding Custom Database Seed Data
-
-To add test data to your database, you can run SQL queries using the provided PostgreSQL connection:
-
-```sql
--- Example: Add a test inventory item
-INSERT INTO inventory_items (name, sku, description, price, quantity, category_id)
-VALUES ('Test Product', 'TEST001', 'Test product description', 99.99, 100, 1);
-```
-
-## Troubleshooting
-
-### Connection Issues
-
-If you encounter connection errors:
-
-1. Verify your PostgreSQL server is running
-2. Ensure your environment variables are correctly set
-3. Check network connectivity to the database server
-4. Verify firewall settings allow connections to PostgreSQL port
-
-### Schema Issues
-
-If you encounter schema errors:
-
-1. Run `npm run db:push` to update the schema
-2. Check for any migration errors in the console
-3. Verify the database user has sufficient permissions
-
-### Performance Optimization
-
-For production environments:
-
-1. Enable PostgreSQL connection pooling
-2. Configure appropriate pool size based on expected load
-3. Consider using a managed PostgreSQL service for production deployments
-
-## Advanced Configuration
-
-### Connection Pooling
-
-The application uses connection pooling to improve performance. You can configure the pool by modifying the `db.ts` file:
-
-```typescript
-export const pool = new Pool({
- connectionString: process.env.DATABASE_URL,
- max: 20, // Maximum number of clients in the pool
- idleTimeoutMillis: 30000, // How long a client is allowed to remain idle before being closed
- connectionTimeoutMillis: 2000, // How long to wait for a connection
-});
-```
-
-### SSL Configuration
-
-For secure connections, you can enable SSL by adding the following to your connection options:
-
-```typescript
-export const pool = new Pool({
- connectionString: process.env.DATABASE_URL,
- ssl: {
- rejectUnauthorized: false // Set to true in production with proper certificates
- }
-});
-```
-
-## Setting Up For Development
-
-1. Install PostgreSQL locally or use a Docker container
-2. Create a database for the application
-3. Set up the required environment variables
-4. Run the application to initialize the schema
-
-## Production Considerations
-
-1. Use a managed PostgreSQL service (AWS RDS, Google Cloud SQL, etc.)
-2. Set up proper database backups
-3. Configure high availability and failover
-4. Implement database monitoring
-5. Use secure connection strings and store credentials safely
-
-## Replit Deployment Configuration
-
-When deploying your application on Replit:
-
-1. Navigate to the "Secrets" tab in your Replit project (lock icon in the tools panel)
-2. Add the following secrets:
- - `DATABASE_URL` (if using the connection string approach)
- - Or individual parameters: `PGHOST`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGPORT`
-3. Click "Add new secret" for each entry
-4. Format the DATABASE_URL as: `postgresql://username:password@host:port/database`
-5. Make sure your PostgreSQL server allows connections from Replit's IP ranges
-6. For Neon Database or similar serverless PostgreSQL services:
- - Use the connection string from your database provider dashboard
- - Ensure WebSocket support is enabled (for Neon Database)
- - Set SSL mode appropriately (usually `{ rejectUnauthorized: false }`)
-
-The application will automatically use these environment variables during deployment and runtime.
\ No newline at end of file
diff --git a/DESKTOP_APP_SETUP.md b/DESKTOP_APP_SETUP.md
deleted file mode 100644
index 8f3f2ecd..00000000
--- a/DESKTOP_APP_SETUP.md
+++ /dev/null
@@ -1,195 +0,0 @@
-# Desktop Application Setup Guide
-
-This document provides instructions for setting up and building the Electron desktop version of the inventory management system.
-
-## Overview
-
-The inventory management system can be run as:
-1. A web application hosted on a server
-2. A desktop application using Electron
-
-The desktop version provides:
-- Better performance for local usage
-- Offline capabilities with local data synchronization
-- Native operating system integration
-- Secure local database for sensitive inventory data
-
-## Prerequisites
-
-Before building the desktop application, ensure you have:
-
-- Node.js (v18.x or later) installed
-- npm (v9.x or later) installed
-- Git installed
-- Required build tools for your operating system:
- - Windows: Visual Studio Build Tools
- - macOS: Xcode Command Line Tools
- - Linux: GCC and related build packages
-
-## Development Setup
-
-### 1. Install Dependencies
-
-First, install all required dependencies:
-
-```bash
-npm install
-```
-
-### 2. Run in Development Mode
-
-To run the application in development mode:
-
-```bash
-# Start the development server and Electron app
-npm run electron:dev
-
-# OR use the provided script
-./start-electron-dev.sh
-```
-
-This will start:
-- The Express backend server
-- The Electron application connected to the backend
-
-### 3. Making Changes
-
-When developing the desktop application:
-
-- Web/UI changes: Modify files in the `client/src` directory
-- Electron-specific changes: Modify files in the `electron` directory
-- Backend changes: Modify files in the `server` directory
-
-## Building for Distribution
-
-### 1. Prerequisites for Building
-
-Ensure your environment is set up correctly:
-
-```bash
-# Install required global packages
-npm install -g electron-builder
-```
-
-### 2. Build Process
-
-To build the desktop application:
-
-```bash
-# Build for the current platform
-npm run electron:build
-
-# OR use the provided script
-./build-electron.sh
-```
-
-This will:
-1. Build the React frontend using Vite
-2. Compile the server-side code with esbuild
-3. Package everything with Electron builder
-
-### 3. Platform-Specific Builds
-
-To build for specific platforms:
-
-```bash
-# Windows
-npm run electron:build:win
-
-# macOS
-npm run electron:build:mac
-
-# Linux
-npm run electron:build:linux
-```
-
-The build outputs will be available in the `dist` directory.
-
-## Deployment Configuration
-
-### Local Database Setup
-
-The desktop application uses SQLite for local data storage. This is automatically configured when the application is installed.
-
-### Data Synchronization
-
-Configure synchronization settings in the application:
-
-1. Navigate to Settings > Sync
-2. Enter your server URL
-3. Configure sync frequency and options
-4. Enable offline mode if needed
-
-### Auto Updates
-
-The application supports auto-updates:
-
-1. Host the update files on a server
-2. Configure `electron-builder.json` with the update URL
-3. Build the application with auto-update support enabled
-
-## Troubleshooting
-
-### Common Issues
-
-#### Application Won't Start
-
-- Check logs in `%APPDATA%/inventory-manager/logs` (Windows) or `~/Library/Logs/inventory-manager` (macOS)
-- Verify all dependencies are installed
-- Check permissions on installation directory
-
-#### Database Errors
-
-- Check SQLite database file integrity
-- Verify user has write permissions to the database directory
-- Try resetting the database from Settings > Advanced
-
-#### Sync Problems
-
-- Verify server URL is correct
-- Check network connectivity
-- Ensure server API is compatible with client version
-
-## Advanced Configuration
-
-### Custom Installation
-
-You can customize the installation directory and other options in `electron-builder.json`:
-
-```json
-{
- "appId": "com.yourcompany.inventory-manager",
- "productName": "Inventory Manager",
- "directories": {
- "output": "dist"
- },
- "win": {
- "target": ["nsis"],
- "icon": "build/icon.ico"
- },
- "mac": {
- "target": ["dmg"],
- "icon": "build/icon.icns"
- },
- "linux": {
- "target": ["AppImage", "deb"],
- "icon": "build/icon.png"
- }
-}
-```
-
-### Custom Database Location
-
-To change where data is stored:
-
-1. Modify `electron/db.js` to specify a custom database path
-2. Rebuild the application
-
-### Security Considerations
-
-For enhanced security:
-
-1. Enable data encryption in Settings > Security
-2. Use strong passwords for application access
-3. Regularly backup your database
-4. Keep the application updated to the latest version
\ No newline at end of file
diff --git a/PROJECT_OVERVIEW.txt b/PROJECT_OVERVIEW.txt
new file mode 100644
index 00000000..8b56a052
--- /dev/null
+++ b/PROJECT_OVERVIEW.txt
@@ -0,0 +1,129 @@
+SkillRadius Codebase Overview
+=============================
+
+This repository now contains a web-first freelancer marketplace UI. The legacy
+inventory, billing, reporting, and Electron desktop code has been removed to
+focus on the SkillRadius experience.
+
+Top-level files
+---------------
+- package.json
+ Defines client-only scripts (dev/build/preview/check) for the Vite UI.
+- tsconfig.json / tsconfig.check.json
+ TypeScript configuration; check config scopes typechecking to client/shared.
+- vite.config.ts
+ Vite configuration for the client app and path aliases.
+- tailwind.config.ts / postcss.config.js / theme.json
+ Styling and theme configuration for the UI.
+
+Client application (client/)
+---------------------------
+- client/index.html
+ HTML entry point for the Vite app.
+
+- client/src/main.tsx
+ React entry point; mounts the app and global styles.
+
+- client/src/App.tsx
+ Router + layout composition. Routes include:
+ - /
+ - /discover
+ - /jobs
+ - /messages
+ - /reviews
+ - /profile
+ - /settings
+ - /auth
+
+Pages (client/src/pages/)
+-------------------------
+- home.tsx
+ Marketplace landing page: hero, stats, map placeholder, filters, and
+ freelancer cards.
+
+- discover.tsx
+ Search + filter screen with a map placeholder.
+
+- jobs.tsx
+ Jobs list and βpost a jobβ action.
+
+- messages.tsx
+ Messaging inbox view with conversations.
+
+- reviews.tsx
+ Client feedback list with star ratings.
+
+- profile.tsx
+ Freelancer profile details, verification, reputation, and availability.
+
+- settings.tsx
+ Account preferences for notifications, availability, and safety.
+
+- auth-page.tsx
+ Sign-in screen with basic login inputs and brand messaging.
+
+- not-found.tsx
+ Fallback 404 screen.
+
+Components (client/src/components/)
+----------------------------------
+- sidebar.tsx
+ Navigation for the SkillRadius routes.
+
+- header.tsx
+ Top header layout for the app pages.
+
+- layout/desktop-layout.tsx
+ Main layout wrapper (sidebar + header + content area).
+
+- page-header.tsx
+ Shared header component for sections.
+
+- theme-provider.tsx
+ Theme state and toggles.
+
+- ui/
+ Shadcn UI primitives (buttons, cards, inputs, badges, tabs, etc.).
+
+Contexts (client/src/contexts/)
+-------------------------------
+- tutorial-context.tsx / TutorialContext.tsx
+ Walkthrough/tutorial context; still present but currently UI-only.
+
+Hooks (client/src/hooks/)
+-------------------------
+- use-auth.tsx
+ Auth hooks with login/logout flow used on the auth page and protected routes.
+
+- use-mobile.tsx
+ Responsive helper hook.
+
+- use-toast.ts
+ Toast notification helper.
+
+Library (client/src/lib/)
+-------------------------
+- queryClient.ts
+ React Query client configuration and API helper.
+
+- protected-route.tsx
+ Route guard component for authenticated pages.
+
+- utils.ts
+ Utility helpers (class name merging, formatting, etc.).
+
+- config.ts
+ Feature flag management for web-only behavior.
+
+- document-generator.ts
+ Browser-first PDF/CSV/Excel generation helpers.
+
+Shared (shared/)
+----------------
+- shared/ holds shared types, schemas, and utilities consumed by the client.
+
+Notes
+-----
+- Electron/desktop code, server APIs, inventory/billing/reporting modules,
+ and related assets have been removed.
+- The app is now a client-only, web-first SkillRadius marketplace UI.
diff --git a/attached_assets/Pasted--Step-1-Database-Schema-Updated-with-Profile-Picture-Storage-Your-users-table-should-include-a--1743502078295.txt b/attached_assets/Pasted--Step-1-Database-Schema-Updated-with-Profile-Picture-Storage-Your-users-table-should-include-a--1743502078295.txt
deleted file mode 100644
index 8f059e68..00000000
--- a/attached_assets/Pasted--Step-1-Database-Schema-Updated-with-Profile-Picture-Storage-Your-users-table-should-include-a--1743502078295.txt
+++ /dev/null
@@ -1,167 +0,0 @@
-πΉ Step 1: Database Schema (Updated with Profile Picture Storage)
-Your users table should include a profile picture URL field:
-
-sql
-Copy
-Edit
-CREATE TABLE users (
- id SERIAL PRIMARY KEY,
- username VARCHAR(50) UNIQUE NOT NULL,
- email VARCHAR(100) UNIQUE NOT NULL,
- password_hash TEXT NOT NULL,
- profile_picture TEXT, -- URL of the profile picture (Stored in Cloud)
- role VARCHAR(20) DEFAULT 'user',
- created_at TIMESTAMP DEFAULT NOW()
-);
-πΉ Step 2: Upload Profile Pictures (Backend API)
-Storage Options:
-β
Cloudinary (Recommended)
-β
AWS S3
-β
Firebase Storage
-
-Install Required Libraries (Node.js Example)
-sh
-Copy
-Edit
-npm install multer cloudinary dotenv express
-Backend API for Uploading Profile Pictures
-javascript
-Copy
-Edit
-const express = require("express");
-const multer = require("multer");
-const cloudinary = require("cloudinary").v2;
-const { CloudinaryStorage } = require("multer-storage-cloudinary");
-require("dotenv").config();
-
-const app = express();
-app.use(express.json());
-
-// Configure Cloudinary
-cloudinary.config({
- cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
- api_key: process.env.CLOUDINARY_API_KEY,
- api_secret: process.env.CLOUDINARY_API_SECRET,
-});
-
-// Multer Storage for Cloudinary
-const storage = new CloudinaryStorage({
- cloudinary,
- params: {
- folder: "profile_pictures",
- allowed_formats: ["jpg", "png", "jpeg"],
- },
-});
-const upload = multer({ storage });
-
-// Profile Picture Upload API
-app.post("/upload-profile", upload.single("profile_picture"), async (req, res) => {
- try {
- const imageUrl = req.file.path;
- res.json({ success: true, imageUrl });
- } catch (error) {
- res.status(500).json({ success: false, message: "Upload failed" });
- }
-});
-
-// Start Server
-app.listen(3000, () => console.log("Server running on port 3000"));
-πΉ Step 3: Update Profile Picture URL in the Database
-Modify User Profile API to Store Image URL
-javascript
-Copy
-Edit
-const updateProfile = async (req, res) => {
- const { userId, username, email, profilePicture } = req.body;
-
- const updatedUser = await User.findByIdAndUpdate(
- userId,
- { username, email, profile_picture: profilePicture },
- { new: true }
- );
-
- res.json(updatedUser);
-};
-πΉ Step 4: Frontend (React) Profile Picture Upload
-Install Axios for API Requests
-sh
-Copy
-Edit
-npm install axios
-React Component for Profile Picture Upload
-jsx
-Copy
-Edit
-import { useState } from "react";
-import axios from "axios";
-
-export default function ProfilePictureUpload() {
- const [image, setImage] = useState(null);
- const [uploading, setUploading] = useState(false);
-
- const handleUpload = async (event) => {
- setUploading(true);
- const file = event.target.files[0];
- const formData = new FormData();
- formData.append("profile_picture", file);
-
- try {
- const response = await axios.post("/upload-profile", formData);
- console.log("Uploaded Image URL:", response.data.imageUrl);
- } catch (error) {
- console.error("Upload Failed", error);
- } finally {
- setUploading(false);
- }
- };
-
- return (
-
-
- {uploading &&
Uploading...
}
-
- );
-}
-πΉ Step 5: Profile Management Features
-Users should be able to:
-β
Edit Profile Info (Name, Email, Address)
-β
Change Profile Picture
-β
Set Preferences (Dark Mode, Notifications, etc.)
-β
Reset Password
-β
View Account Activity
-
-Profile Page (React)
-jsx
-Copy
-Edit
-import { useState, useEffect } from "react";
-import axios from "axios";
-
-export default function Profile() {
- const [user, setUser] = useState(null);
-
- useEffect(() => {
- axios.get("/api/profile").then(response => setUser(response.data));
- }, []);
-
- if (!user) return Loading...
;
-
- return (
-
-
{user.username}
-
-
Email: {user.email}
-
- );
-}
-πΉ Step 6: User Settings & Security Features
-β Privacy Settings β Make profile public/private
-β Notification Preferences β Email/SMS alerts
-β Multi-Factor Authentication (MFA) β Extra security for logins
-β Dark Mode & UI Customization
-
-π― Final Steps
-β
Setup Backend API for Profile Management
-β
Enable Profile Picture Upload to Cloudinary
-β
Build React Frontend for User Profiles
-β
Add Security Features (MFA, Privacy Settings, etc.)
\ No newline at end of file
diff --git a/attached_assets/Pasted-1-Advanced-Inventory-Management-Real-Time-Inventory-Sync-Automatically-update-stock-levels-acro-1743321664353.txt b/attached_assets/Pasted-1-Advanced-Inventory-Management-Real-Time-Inventory-Sync-Automatically-update-stock-levels-acro-1743321664353.txt
deleted file mode 100644
index 37c6b3ed..00000000
--- a/attached_assets/Pasted-1-Advanced-Inventory-Management-Real-Time-Inventory-Sync-Automatically-update-stock-levels-acro-1743321664353.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-1. Advanced Inventory Management
-β
Real-Time Inventory Sync β Automatically update stock levels across multiple locations and sales channels.
-β
AI-Powered Stock Optimization β Machine learning to suggest reorder quantities based on sales trends.
-β
Automated Stock Transfers β If one warehouse is low, the system can suggest or automate stock transfers.
-β
Multi-Warehouse and Multi-Location Tracking β Real-time tracking of stock in multiple locations.
-β
Batch & Expiry Tracking β Ideal for perishable goods and medical supplies.
-
-2. AI & Automation Enhancements
-β
Predictive Restocking β AI-driven demand forecasting to prevent stockouts.
-β
Smart Notifications β Get alerts for stock depletion, slow-moving products, or excess stock.
-β
Auto-Generated Purchase Orders β AI can suggest or auto-create POs when stock is low.
-β
Supplier Performance Tracking β Rate and track supplier reliability based on delivery time, defect rate, and cost trends.
-β
Auto-Categorization β AI can classify products based on purchase patterns.
-
-3. Advanced Reporting & Analytics
-β
Smart Dashboards β AI-based trend analysis to provide insights into stock performance.
-β
Custom Reports β Users can generate reports based on SKU, supplier, warehouse, or time period.
-β
Cost Analysis & Profit Tracking β Helps determine which inventory is the most profitable.
-β
Loss Prevention Analytics β Detects potential fraud or shrinkage trends.
-
-4. High-Level Integration Capabilities
-β
ERP & Accounting Software Integration β Seamless sync with SAP, Oracle, Sage, and QuickBooks.
-β
POS & E-commerce Integration β Connect with Shopify, WooCommerce, Amazon, or retail POS systems.
-β
API Access for Custom Integrations β Allows businesses to integrate their own tools.
-β
IoT & RFID Support β Automate tracking using RFID tags or IoT sensors for real-time visibility.
-
-5. Security, Compliance & Backup
-β
Role-Based Access Control (RBAC) β Different user access levels to prevent unauthorized changes.
-β
Automated Data Backups β Daily cloud backups for security.
-β
Audit Trail & Change Logs β Full visibility on who made what changes and when.
-β
Compliance Features β Adhere to industry standards like ISO, GDPR, or FDA regulations (for medical and food inventory).
-
-6. Smart Mobile & Web App Features
-β
Voice-Controlled Inventory Search β Users can speak to the app to find products.
-β
AI Chatbot for Support β An assistant that helps with queries, stock levels, or purchase orders.
-β
Augmented Reality (AR) for Warehouse Navigation β Helps workers find products using AR mapping.
-β
Offline Mode β The app should work even without an internet connection, syncing once online.
-
-7. Blockchain for High Security (Optional but Future-Proofing)
-β
Tamper-Proof Inventory Logs β Blockchain records each inventory movement securely.
-β
Smart Contracts for Procurement β Automated supplier agreements based on predefined conditions.
\ No newline at end of file
diff --git a/attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743434758051.txt b/attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743434758051.txt
deleted file mode 100644
index 78041c4b..00000000
--- a/attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743434758051.txt
+++ /dev/null
@@ -1,149 +0,0 @@
-Core Inventory Management (Database + Backend) π¦
-π‘ Features:
-β
AI Image Recognition for Item Entry
-β
Voice Command Stock Management
-β
Geo-Fencing for Stock Tracking
-
-π οΈ Tech Stack:
-
-Backend: Node.js (Express.js) or Python (FastAPI/Django) for fast processing
-
-Database: PostgreSQL (Structured data) + MongoDB (Unstructured metadata like images)
-
-Storage: AWS S3 or Firebase for storing item images
-
-AI Integration: OpenAI Vision API or Google Cloud Vision for recognizing inventory items
-
-Voice Processing: Google Speech-to-Text API for voice commands
-
-Geo-Fencing: Google Maps API + Firebase Realtime Database for location tracking
-
-πΉ Settings to Include:
-β Custom Item Categories & Tags
-β Configurable Units of Measure (kg, liters, boxes)
-β Adjustable Inventory Thresholds
-
-2οΈβ£ Automation & AI Features (AI + ML Models) π€
-π‘ Features:
-β
AI-Powered Theft & Fraud Detection
-β
Dynamic Pricing Adjustments
-β
Predictive Maintenance Alerts
-
-π οΈ Tech Stack:
-
-AI Models: TensorFlow or PyTorch (for demand forecasting and fraud detection)
-
-Automation Framework: Apache Airflow (for scheduling and automation)
-
-Predictive Analytics: AWS SageMaker or Google Vertex AI
-
-AI Pricing Models: OpenAI API or Reinforcement Learning algorithms
-
-πΉ Settings to Include:
-β AI-Based Reordering Sensitivity (User can adjust AI predictions)
-β Fraud Risk Level (Low, Medium, High)
-β Real-Time Pricing Rules
-
-3οΈβ£ Reporting & Analytics (Real-Time Data Processing) π
-π‘ Features:
-β
Real-Time Inventory Heatmaps
-β
Competitor Inventory Benchmarking
-β
AI-Generated Actionable Insights
-
-π οΈ Tech Stack:
-
-Data Processing: Apache Kafka + Apache Spark (for real-time analytics)
-
-Visualization: Tableau, Power BI, or Grafana (for dashboards)
-
-Big Data Storage: Google BigQuery or AWS Redshift
-
-Competitor Benchmarking: Scrapy or Selenium (for web scraping competitor pricing)
-
-πΉ Settings to Include:
-β Customizable Report Filters (Time Range, Product Category)
-β Data Export Formats (PDF, Excel, CSV)
-β AI Insights Frequency (Daily, Weekly, Monthly)
-
-4οΈβ£ Integration Features (APIs & External Systems) π
-π‘ Features:
-β
Smart ERP Plug-and-Play Modules
-β
IoT-Enabled Inventory Updates
-β
Social Commerce Integration
-
-π οΈ Tech Stack:
-
-API Management: GraphQL or REST API using Apollo Server or FastAPI
-
-ERP Integration: Odoo, SAP, or Oracle NetSuite (via REST APIs)
-
-IoT Connectivity: MQTT Protocol + AWS IoT Core
-
-E-commerce Sync: Shopify/WooCommerce/Amazon APIs
-
-πΉ Settings to Include:
-β Toggle Integrations On/Off (ERP, E-commerce, IoT)
-β API Key Management for Secure External Connections
-β Sync Frequency Settings (Live, 1-Hour, Daily)
-
-5οΈβ£ Security & Access Control (Authentication & Compliance) π
-π‘ Features:
-β
AI-Driven Role-Based Permissions
-β
Biometric Authentication for High-Risk Actions
-β
Smart Device Access Control
-
-π οΈ Tech Stack:
-
-Authentication: Firebase Auth, Auth0, or AWS Cognito (for secure login)
-
-Biometric Security: Android Biometric API, Apple Face ID API
-
-Audit Logs & Security Monitoring: ELK Stack (Elasticsearch, Logstash, Kibana)
-
-Blockchain-Based Ledger: Hyperledger Fabric for tamper-proof inventory records
-
-πΉ Settings to Include:
-β User Role Management (Admin, Manager, Staff, Auditor)
-β MFA Enable/Disable Option
-β Auto-Logout Timer (Adjustable)
-
-6οΈβ£ Mobile & Cloud Features (Cross-Platform Usability) π±β
-π‘ Features:
-β
Offline Mode with Smart Sync
-β
Wearable Integration (Smartwatches & AR Glasses)
-β
AI-Powered Voice Assistant
-
-π οΈ Tech Stack:
-
-Mobile App: Flutter (Dart) or React Native for cross-platform development
-
-Cloud Backend: Firebase Firestore + AWS Lambda (Serverless)
-
-AR Integration: ARKit (iOS) + ARCore (Android)
-
-Wearable Connectivity: Google WearOS + Apple WatchKit
-
-πΉ Settings to Include:
-β Offline Sync Frequency (Immediate, Manual, Scheduled)
-β Voice Command Sensitivity
-β Toggle Wearable Features On/Off
-
-7οΈβ£ Advanced Features (Futuristic & Enterprise-Level) π
-π‘ Features:
-β
Drone-Powered Stock Audits
-β
AI-Driven Inventory Auto-Classification
-β
Digital Twin for Warehouse Simulation
-
-π οΈ Tech Stack:
-
-Drone Integration: DJI SDK or OpenCV for image-based inventory scanning
-
-Digital Twin Simulation: Unity3D + NVIDIA Omniverse
-
-RFID & IoT Sensors: Azure IoT Hub or AWS IoT Greengrass
-
-πΉ Settings to Include:
-β Digital Twin Simulation Speed (Normal, Fast)
-β Drone Audit Frequency
-β RFID Scan Distance Adjustment
-
diff --git a/attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743435690258.txt b/attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743435690258.txt
deleted file mode 100644
index 78041c4b..00000000
--- a/attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743435690258.txt
+++ /dev/null
@@ -1,149 +0,0 @@
-Core Inventory Management (Database + Backend) π¦
-π‘ Features:
-β
AI Image Recognition for Item Entry
-β
Voice Command Stock Management
-β
Geo-Fencing for Stock Tracking
-
-π οΈ Tech Stack:
-
-Backend: Node.js (Express.js) or Python (FastAPI/Django) for fast processing
-
-Database: PostgreSQL (Structured data) + MongoDB (Unstructured metadata like images)
-
-Storage: AWS S3 or Firebase for storing item images
-
-AI Integration: OpenAI Vision API or Google Cloud Vision for recognizing inventory items
-
-Voice Processing: Google Speech-to-Text API for voice commands
-
-Geo-Fencing: Google Maps API + Firebase Realtime Database for location tracking
-
-πΉ Settings to Include:
-β Custom Item Categories & Tags
-β Configurable Units of Measure (kg, liters, boxes)
-β Adjustable Inventory Thresholds
-
-2οΈβ£ Automation & AI Features (AI + ML Models) π€
-π‘ Features:
-β
AI-Powered Theft & Fraud Detection
-β
Dynamic Pricing Adjustments
-β
Predictive Maintenance Alerts
-
-π οΈ Tech Stack:
-
-AI Models: TensorFlow or PyTorch (for demand forecasting and fraud detection)
-
-Automation Framework: Apache Airflow (for scheduling and automation)
-
-Predictive Analytics: AWS SageMaker or Google Vertex AI
-
-AI Pricing Models: OpenAI API or Reinforcement Learning algorithms
-
-πΉ Settings to Include:
-β AI-Based Reordering Sensitivity (User can adjust AI predictions)
-β Fraud Risk Level (Low, Medium, High)
-β Real-Time Pricing Rules
-
-3οΈβ£ Reporting & Analytics (Real-Time Data Processing) π
-π‘ Features:
-β
Real-Time Inventory Heatmaps
-β
Competitor Inventory Benchmarking
-β
AI-Generated Actionable Insights
-
-π οΈ Tech Stack:
-
-Data Processing: Apache Kafka + Apache Spark (for real-time analytics)
-
-Visualization: Tableau, Power BI, or Grafana (for dashboards)
-
-Big Data Storage: Google BigQuery or AWS Redshift
-
-Competitor Benchmarking: Scrapy or Selenium (for web scraping competitor pricing)
-
-πΉ Settings to Include:
-β Customizable Report Filters (Time Range, Product Category)
-β Data Export Formats (PDF, Excel, CSV)
-β AI Insights Frequency (Daily, Weekly, Monthly)
-
-4οΈβ£ Integration Features (APIs & External Systems) π
-π‘ Features:
-β
Smart ERP Plug-and-Play Modules
-β
IoT-Enabled Inventory Updates
-β
Social Commerce Integration
-
-π οΈ Tech Stack:
-
-API Management: GraphQL or REST API using Apollo Server or FastAPI
-
-ERP Integration: Odoo, SAP, or Oracle NetSuite (via REST APIs)
-
-IoT Connectivity: MQTT Protocol + AWS IoT Core
-
-E-commerce Sync: Shopify/WooCommerce/Amazon APIs
-
-πΉ Settings to Include:
-β Toggle Integrations On/Off (ERP, E-commerce, IoT)
-β API Key Management for Secure External Connections
-β Sync Frequency Settings (Live, 1-Hour, Daily)
-
-5οΈβ£ Security & Access Control (Authentication & Compliance) π
-π‘ Features:
-β
AI-Driven Role-Based Permissions
-β
Biometric Authentication for High-Risk Actions
-β
Smart Device Access Control
-
-π οΈ Tech Stack:
-
-Authentication: Firebase Auth, Auth0, or AWS Cognito (for secure login)
-
-Biometric Security: Android Biometric API, Apple Face ID API
-
-Audit Logs & Security Monitoring: ELK Stack (Elasticsearch, Logstash, Kibana)
-
-Blockchain-Based Ledger: Hyperledger Fabric for tamper-proof inventory records
-
-πΉ Settings to Include:
-β User Role Management (Admin, Manager, Staff, Auditor)
-β MFA Enable/Disable Option
-β Auto-Logout Timer (Adjustable)
-
-6οΈβ£ Mobile & Cloud Features (Cross-Platform Usability) π±β
-π‘ Features:
-β
Offline Mode with Smart Sync
-β
Wearable Integration (Smartwatches & AR Glasses)
-β
AI-Powered Voice Assistant
-
-π οΈ Tech Stack:
-
-Mobile App: Flutter (Dart) or React Native for cross-platform development
-
-Cloud Backend: Firebase Firestore + AWS Lambda (Serverless)
-
-AR Integration: ARKit (iOS) + ARCore (Android)
-
-Wearable Connectivity: Google WearOS + Apple WatchKit
-
-πΉ Settings to Include:
-β Offline Sync Frequency (Immediate, Manual, Scheduled)
-β Voice Command Sensitivity
-β Toggle Wearable Features On/Off
-
-7οΈβ£ Advanced Features (Futuristic & Enterprise-Level) π
-π‘ Features:
-β
Drone-Powered Stock Audits
-β
AI-Driven Inventory Auto-Classification
-β
Digital Twin for Warehouse Simulation
-
-π οΈ Tech Stack:
-
-Drone Integration: DJI SDK or OpenCV for image-based inventory scanning
-
-Digital Twin Simulation: Unity3D + NVIDIA Omniverse
-
-RFID & IoT Sensors: Azure IoT Hub or AWS IoT Greengrass
-
-πΉ Settings to Include:
-β Digital Twin Simulation Speed (Normal, Fast)
-β Drone Audit Frequency
-β RFID Scan Distance Adjustment
-
diff --git a/build-electron.sh b/build-electron.sh
deleted file mode 100755
index a17328a9..00000000
--- a/build-electron.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-
-# Build Electron application
-
-# Check if production flag is provided
-if [ "$1" == "--production" ]; then
- echo "Building Electron application for production..."
- node scripts/build-electron.js --production
-else
- echo "Building Electron application for development testing..."
- node scripts/build-electron.js
-fi
-
-echo "Build complete. Check the dist_electron directory for the output."
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 20dc8019..239e6e6b 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,47 +1,28 @@
import React from "react";
import { Switch, Route } from "wouter";
-import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
-import { Toaster } from "@/components/ui/toaster";
-import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
+import { Toaster } from "@/components/ui/toaster";
import { RefreshCw } from "lucide-react";
-import NotFound from "@/pages/not-found";
-import Dashboard from "@/pages/dashboard";
-import Inventory from "@/pages/inventory";
-import InventoryItemDetail from "@/pages/inventory-item";
-import OrdersPage from "@/pages/orders";
-import SuppliersPage from "@/pages/suppliers";
-import Reports from "@/pages/reports";
-import SettingsPage from "@/pages/settings";
-import UserRolesPage from "@/pages/user-roles";
-import Home from "@/pages/home";
-import ReorderPage from "@/pages/reorder";
-import AuthPage from "@/pages/auth-page";
-import BarcodeScannerPage from "@/pages/barcode-scanner-page";
-import RealTimeUpdatesPage from "@/pages/real-time-updates-page";
-import SyncTestPage from "@/pages/sync-test-page";
-import SyncDashboard from "@/pages/sync-dashboard";
-import DownloadPage from "@/pages/download";
-import BillingPage from "@/pages/billing";
-import ProfilePage from "@/pages/profile";
-import ImageRecognitionPage from "@/pages/image-recognition-page";
-import DocumentExtractorPage from "@/pages/document-extractor-page";
-import WarehousesPage from "@/pages/warehouses";
+import { queryClient } from "./lib/queryClient";
import { ThemeProvider } from "@/components/theme-provider";
-import { useState, useEffect } from "react";
import { TutorialProvider } from "@/contexts/tutorial-context";
-import { TutorialSteps } from "@/components/tutorial/tutorial-steps";
import { AuthProvider } from "@/hooks/use-auth";
import { ProtectedRoute } from "@/lib/protected-route";
-import { isElectronEnvironment } from "./lib/electron-bridge";
-import { ElectronProvider } from "./contexts/electron-provider";
-import { TitleBar, UpdateNotification } from "./components/electron";
import { DesktopLayout } from "./components/layout/desktop-layout";
+import NotFound from "@/pages/not-found";
+import Home from "@/pages/home";
+import Discover from "@/pages/discover";
+import JobsPage from "@/pages/jobs";
+import MessagesPage from "@/pages/messages";
+import ReviewsPage from "@/pages/reviews";
+import ProfilePage from "@/pages/profile";
+import SettingsPage from "@/pages/settings";
+import AuthPage from "@/pages/auth-page";
-// Error boundary component
-class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
- constructor(props: {children: React.ReactNode}) {
+class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean; error: Error | null }> {
+ constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
@@ -64,11 +45,7 @@ class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasErr
{this.state.error?.message || "An unexpected error occurred"}
- window.location.reload()}
- className="w-full"
- >
+ window.location.reload()} className="w-full">
Reload Application
@@ -86,25 +63,12 @@ function Router() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
+
@@ -114,60 +78,31 @@ function Router() {
function AppLayout({ children }: { children: React.ReactNode }) {
return (
-
{children}
);
}
-// Function to set up Electron-specific features
-function setupElectronApp() {
- if (isElectronEnvironment()) {
- // Add a class to the HTML element to allow for Electron-specific styling
- document.documentElement.classList.add('electron-app');
-
- // Disable drag and drop file behavior that may interfere with the app
- document.addEventListener('dragover', (e) => e.preventDefault());
- document.addEventListener('drop', (e) => e.preventDefault());
-
- // Override the context menu for custom behavior if needed
- // document.addEventListener('contextmenu', (e) => e.preventDefault());
- }
-}
-
function App() {
- // Set up Electron-specific HTML classes when in Electron environment
- useEffect(() => {
- setupElectronApp();
- }, []);
-
return (
-
+
-
-
-
-
-
-
- {(params) => {
- // Don't wrap non-auth routes with AppLayout
- const pathname = params["*"] || "";
- if (pathname === "auth") return null;
- return (
-
-
-
- );
- }}
-
-
-
-
-
+
+
+
+
+
+ {(params) => {
+ const pathname = params["*"] || "";
+ if (pathname === "auth") return null;
+ return ;
+ }}
+
+
+
diff --git a/client/src/components/analytics/demand-forecast.tsx b/client/src/components/analytics/demand-forecast.tsx
deleted file mode 100644
index da6d95fa..00000000
--- a/client/src/components/analytics/demand-forecast.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import React, { useState } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { format, parseISO, subMonths, addMonths } from 'date-fns';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Skeleton } from '@/components/ui/skeleton';
-import { CalendarIcon, ChevronLeft, ChevronRight, RefreshCw } from 'lucide-react';
-import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
-import { DateRangePicker } from '@/components/ui/date-range-picker';
-import { useDateRangeParams } from '@/hooks/use-date-range-params';
-
-interface DemandForecastPoint {
- date: string;
- itemId: number;
- itemName: string;
- historical: number | null;
- forecast: number | null;
- accuracy: number | null;
-}
-
-interface DemandForecastProps {
- itemId: number;
- itemName: string;
-}
-
-export function DemandForecast({ itemId, itemName }: DemandForecastProps) {
- const { dateRange, updateDateRange, range, updateRange } = useDateRangeParams(90);
- const [view, setView] = useState<'3m' | '6m' | '12m'>('3m');
-
- // Build query parameters
- const queryParams = new URLSearchParams();
-
- if (dateRange?.from) {
- queryParams.append("startDate", format(dateRange.from, "yyyy-MM-dd"));
- }
-
- if (dateRange?.to) {
- queryParams.append("endDate", format(dateRange.to, "yyyy-MM-dd"));
- }
-
- // Fetch forecast data
- const { data, isLoading, error, refetch } = useQuery({
- queryKey: [`/api/analytics/demand-forecast/${itemId}`, queryParams.toString()],
- queryFn: async () => {
- const response = await fetch(`/api/analytics/demand-forecast/${itemId}?${queryParams.toString()}`);
- if (!response.ok) {
- throw new Error('Failed to fetch demand forecast');
- }
- return response.json() as Promise;
- },
- });
-
- const handleViewChange = (newView: '3m' | '6m' | '12m') => {
- setView(newView);
-
- const today = new Date();
- let newFrom: Date;
-
- switch (newView) {
- case '3m':
- newFrom = subMonths(today, 2);
- break;
- case '6m':
- newFrom = subMonths(today, 5);
- break;
- case '12m':
- newFrom = subMonths(today, 11);
- break;
- }
-
- updateDateRange({
- from: newFrom,
- to: addMonths(today, 1)
- });
- };
-
- if (error) {
- return (
-
-
- Demand Forecast
-
-
-
-
Error loading forecast data.
-
-
-
- );
- }
-
- // Calculate average accuracy from forecast data
- const averageAccuracy = data && data.length > 0
- ? data.reduce((acc, point) => {
- return point.accuracy !== null ? acc + point.accuracy : acc;
- }, 0) / data.filter(point => point.accuracy !== null).length
- : null;
-
- return (
-
-
-
-
-
Demand Forecast
-
- Historical and predicted demand for {itemName}
-
-
-
-
- refetch()}
- title="Refresh data"
- >
-
-
-
-
-
-
-
- handleViewChange('3m')}
- >
- 3 Months
-
- handleViewChange('6m')}
- >
- 6 Months
-
- handleViewChange('12m')}
- >
- 12 Months
-
-
-
- {isLoading ? (
-
-
-
- ) : data && data.length > 0 ? (
- <>
-
-
-
Forecast Accuracy
-
- {averageAccuracy !== null
- ? `${Math.round(averageAccuracy * 100)}%`
- : 'N/A'}
-
-
-
-
Next Month Forecast
-
- {data.find(d => d.forecast !== null)?.forecast || 'N/A'}
-
-
-
-
-
-
- ({
- ...d,
- date: format(parseISO(d.date), 'MMM dd')
- }))}
- margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
- >
-
-
-
- [value, 'Units']}
- labelFormatter={(label) => `Date: ${label}`}
- />
-
-
-
-
-
-
-
-
-
-
- Historical: Actual demand based on past stock movements
-
-
-
- Forecast: Predicted future demand based on historical patterns
-
-
- >
- ) : (
-
- No forecast data available for this item.
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/analytics/inventory-value.tsx b/client/src/components/analytics/inventory-value.tsx
deleted file mode 100644
index 6835a2f6..00000000
--- a/client/src/components/analytics/inventory-value.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import React, { useMemo } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Skeleton } from '@/components/ui/skeleton';
-import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
-import { formatCurrency } from '@/lib/utils';
-
-interface InventoryValueItem {
- id: number;
- name: string;
- quantity: number;
- cost: number;
- value: number;
-}
-
-interface InventoryValueData {
- totalValue: number;
- totalItems: number;
- items: InventoryValueItem[];
-}
-
-export function InventoryValue() {
- // Fetch inventory value data
- const { data, isLoading, error } = useQuery({
- queryKey: ['/api/analytics/inventory-value'],
- queryFn: async () => {
- const response = await fetch('/api/analytics/inventory-value');
- if (!response.ok) {
- throw new Error('Failed to fetch inventory value data');
- }
- return response.json() as Promise;
- },
- });
-
- // Prepare data for the chart
- const chartData = useMemo(() => {
- if (!data || !data.items || data.items.length === 0) return [];
-
- // Group smaller items into "Other" category
- const topItems = data.items.slice(0, 6); // Take top 6 items
- const otherItems = data.items.slice(6); // The rest become "Other"
-
- const result = topItems.map(item => ({
- name: item.name,
- value: item.value,
- }));
-
- // Add "Other" category if we have more than 6 items
- if (otherItems.length > 0) {
- const otherValue = otherItems.reduce((sum, item) => sum + item.value, 0);
- result.push({
- name: 'Other',
- value: otherValue,
- });
- }
-
- return result;
- }, [data]);
-
- // Color scale for the chart
- const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d', '#bbb'];
-
- // Custom tooltip for the chart
- const CustomTooltip = ({ active, payload }: any) => {
- if (active && payload && payload.length) {
- return (
-
-
{payload[0].name}
-
- Value: {formatCurrency(payload[0].value)}
-
- {data && (
-
- ({((payload[0].value / data.totalValue) * 100).toFixed(1)}% of total)
-
- )}
-
- );
- }
- return null;
- };
-
- if (error) {
- return (
-
-
- Inventory Value
-
-
-
-
Error loading inventory value data.
-
-
-
- );
- }
-
- return (
-
-
- Inventory Value Distribution
-
-
- {isLoading ? (
-
- ) : data ? (
- <>
-
-
-
Total Value
-
{formatCurrency(data.totalValue)}
-
-
-
Total Items
-
{data.totalItems.toLocaleString()}
-
-
-
- {chartData.length > 0 ? (
-
-
-
-
- {chartData.map((entry, index) => (
- |
- ))}
-
- } />
- {
- // Truncate long names and show tooltip on hover
- return value.length > 10 ? `${value.substring(0, 10)}...` : value;
- }}
- wrapperStyle={{
- paddingTop: '10px',
- width: '100%',
- display: 'flex',
- flexWrap: 'wrap',
- justifyContent: 'center',
- gap: '10px'
- }}
- />
-
-
-
- ) : (
-
- No inventory value data available.
-
- )}
- >
- ) : (
-
- No inventory value data available.
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/analytics/top-items.tsx b/client/src/components/analytics/top-items.tsx
deleted file mode 100644
index 41266344..00000000
--- a/client/src/components/analytics/top-items.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { useLocation } from 'wouter';
-import { format } from 'date-fns';
-import { useDateRangeParams } from '@/hooks/use-date-range-params';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { DateRangePicker } from '@/components/ui/date-range-picker';
-import { Badge } from '@/components/ui/badge';
-import { Skeleton } from '@/components/ui/skeleton';
-import { Button } from '@/components/ui/button';
-import { ExternalLink } from 'lucide-react';
-import { formatCurrency } from '@/lib/utils';
-import { type InventoryItem } from '@shared/schema';
-
-export function TopItems() {
- const [, setLocation] = useLocation();
- const { dateRange, updateDateRange, range, updateRange } = useDateRangeParams(30);
-
- // Build query parameters
- const queryParams = new URLSearchParams();
- queryParams.append("limit", "8"); // Show top 8 items
-
- if (dateRange?.from) {
- queryParams.append("startDate", format(dateRange.from, "yyyy-MM-dd"));
- }
-
- if (dateRange?.to) {
- queryParams.append("endDate", format(dateRange.to, "yyyy-MM-dd"));
- }
-
- // Fetch top items
- const { data, isLoading, error } = useQuery({
- queryKey: ['/api/analytics/top-items', queryParams.toString()],
- queryFn: async () => {
- const response = await fetch(`/api/analytics/top-items?${queryParams.toString()}`);
- if (!response.ok) {
- throw new Error('Failed to fetch top items');
- }
- return response.json() as Promise;
- },
- });
-
- if (error) {
- return (
-
-
- Top Items by Demand
-
-
-
-
Error loading top items data.
-
-
-
- );
- }
-
- return (
-
-
-
-
Top Items by Demand
-
-
-
-
-
-
- {isLoading ? (
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
- ) : data && data.length > 0 ? (
- <>
-
- {data.map((item) => (
-
-
- {item.name.substring(0, 2).toUpperCase()}
-
-
-
{item.name}
-
SKU: {item.sku}
-
-
- {item.quantity} in stock
-
-
setLocation(`/inventory/${item.id}`)}
- >
-
-
-
- ))}
-
-
- setLocation('/inventory')}
- >
- View All Items
-
-
- >
- ) : (
-
- No data available for the selected period.
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/barcode/barcode-generator.tsx b/client/src/components/barcode/barcode-generator.tsx
deleted file mode 100644
index 6524a989..00000000
--- a/client/src/components/barcode/barcode-generator.tsx
+++ /dev/null
@@ -1,318 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { Card, CardContent, CardFooter } from '@/components/ui/card';
-import { Label } from '@/components/ui/label';
-import { Input } from '@/components/ui/input';
-import { Button } from '@/components/ui/button';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { useToast } from '@/hooks/use-toast';
-import { QrCode, Barcode, Printer, Download, Copy } from 'lucide-react';
-
-// Import barcode generator libraries directly to ensure they're available
-import JsBarcode from 'jsbarcode';
-import QRCode from 'qrcode';
-
-type BarcodeFormat = 'CODE128' | 'CODE39' | 'EAN13' | 'EAN8' | 'UPC' | 'ITF14';
-
-interface BarcodeGeneratorProps {
- initialValue?: string;
- onClose?: () => void;
-}
-
-export function BarcodeGenerator({ initialValue = '', onClose }: BarcodeGeneratorProps) {
- const [tab, setTab] = useState('barcode');
- const [value, setValue] = useState(initialValue);
- const [format, setFormat] = useState('CODE128');
- const [size, setSize] = useState('medium');
- const barcodeRef = useRef(null);
- const qrcodeRef = useRef(null);
- const { toast } = useToast();
-
- // No need to load libraries dynamically anymore since we're importing them directly
-
- // Generate barcode/QR code whenever value, format, or size changes
- useEffect(() => {
- if (!value) return;
-
- const generateCode = async () => {
- try {
- if (tab === 'barcode' && barcodeRef.current && JsBarcode) {
- JsBarcode(barcodeRef.current, value, {
- format,
- width: getWidthFromSize(size),
- height: getHeightFromSize(size),
- displayValue: true,
- fontOptions: 'bold',
- fontSize: getSizeFontFromSize(size),
- margin: 10,
- });
- } else if (tab === 'qrcode' && qrcodeRef.current && QRCode) {
- qrcodeRef.current.innerHTML = '';
- await QRCode.toCanvas(
- qrcodeRef.current.appendChild(document.createElement('canvas')),
- value,
- {
- width: getQRSizeFromSize(size),
- margin: 1,
- color: {
- dark: '#000000',
- light: '#ffffff',
- },
- }
- );
- }
- } catch (err) {
- console.error('Error generating code:', err);
- toast({
- title: 'Generation Error',
- description: err instanceof Error ? err.message : String(err),
- variant: 'destructive',
- });
- }
- };
-
- generateCode();
- }, [tab, value, format, size, toast]);
-
- // Helper functions for size conversions
- const getWidthFromSize = (size: string): number => {
- switch (size) {
- case 'small': return 1;
- case 'large': return 3;
- default: return 2;
- }
- };
-
- const getHeightFromSize = (size: string): number => {
- switch (size) {
- case 'small': return 30;
- case 'large': return 80;
- default: return 60;
- }
- };
-
- const getSizeFontFromSize = (size: string): number => {
- switch (size) {
- case 'small': return 12;
- case 'large': return 20;
- default: return 16;
- }
- };
-
- const getQRSizeFromSize = (size: string): number => {
- switch (size) {
- case 'small': return 128;
- case 'large': return 256;
- default: return 200;
- }
- };
-
- // Print the generated code
- const handlePrint = () => {
- const printWindow = window.open('', '_blank');
- if (!printWindow) {
- toast({
- title: 'Print Error',
- description: 'Unable to open print window. Please check your browser settings.',
- variant: 'destructive',
- });
- return;
- }
-
- const codeElement = tab === 'barcode'
- ? barcodeRef.current
- : qrcodeRef.current?.querySelector('canvas');
-
- if (!codeElement) return;
-
- const dataUrl = codeElement.toDataURL('image/png');
-
- printWindow.document.write(`
-
-
-
- Print ${tab === 'barcode' ? 'Barcode' : 'QR Code'}
-
-
-
-
- ${value}
-
-
- `);
-
- printWindow.document.close();
- };
-
- // Download the generated code as a PNG image
- const handleDownload = () => {
- const codeElement = tab === 'barcode'
- ? barcodeRef.current
- : qrcodeRef.current?.querySelector('canvas');
-
- if (!codeElement) return;
-
- const dataUrl = codeElement.toDataURL('image/png');
- const downloadLink = document.createElement('a');
- downloadLink.href = dataUrl;
- downloadLink.download = `${tab}-${value}.png`;
- document.body.appendChild(downloadLink);
- downloadLink.click();
- document.body.removeChild(downloadLink);
-
- toast({
- title: 'Download Complete',
- description: `${tab === 'barcode' ? 'Barcode' : 'QR Code'} has been downloaded.`,
- });
- };
-
- // Copy the generated code to clipboard
- const handleCopy = () => {
- navigator.clipboard.writeText(value).then(
- () => {
- toast({
- title: 'Copied to Clipboard',
- description: `Value "${value}" has been copied to clipboard.`,
- });
- },
- (err) => {
- console.error('Error copying to clipboard:', err);
- toast({
- title: 'Copy Error',
- description: 'Failed to copy to clipboard.',
- variant: 'destructive',
- });
- }
- );
- };
-
- return (
-
-
-
- Generate {tab === 'barcode' ? 'Barcode' : 'QR Code'}
-
-
-
-
-
-
-
- Barcode
-
-
-
- QR Code
-
-
-
-
-
-
- Value
- setValue(e.target.value)}
- />
-
-
- {tab === 'barcode' && (
-
- Format
- setFormat(v as BarcodeFormat)}>
-
-
-
-
- Code 128 (General)
- Code 39 (Alphanumeric)
- EAN-13 (Product)
- EAN-8 (Product)
- UPC (Product)
- ITF-14 (Shipping)
-
-
-
- )}
-
-
- Size
-
-
-
-
-
- Small
- Medium
- Large
-
-
-
-
-
-
- {tab === 'barcode' ? (
-
- {value ? (
-
- ) : (
-
-
-
Enter a value to generate barcode
-
- )}
-
- ) : (
-
- {!value && (
-
-
-
Enter a value to generate QR code
-
- )}
-
- )}
-
-
-
-
-
-
-
-
- Copy
-
-
-
- Download
-
-
-
- Print
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/barcode/barcode-scanner.tsx b/client/src/components/barcode/barcode-scanner.tsx
deleted file mode 100644
index de5f8d5c..00000000
--- a/client/src/components/barcode/barcode-scanner.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Card, CardContent } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { useBarcodeScanner, ScannerType, ScanResult } from '@/hooks/use-barcode-scanner';
-import { Loader2, QrCode, Barcode, Camera, X } from 'lucide-react';
-import { isElectronEnvironment } from '@/lib/electron-bridge';
-
-interface BarcodeScannerProps {
- onScan?: (result: ScanResult) => void;
- onClose?: () => void;
- defaultTab?: ScannerType;
-}
-
-export function BarcodeScanner({
- onScan,
- onClose,
- defaultTab = 'auto'
-}: BarcodeScannerProps) {
- const [tab, setTab] = useState(defaultTab);
- const [scannerElementId] = useState(`scanner-${Math.random().toString(36).substring(2, 11)}`);
- const { isScanning, lastScan, error, startScanning, stopScanning } = useBarcodeScanner();
-
- // Handle scan result
- useEffect(() => {
- if (lastScan && onScan) {
- onScan(lastScan);
- }
- }, [lastScan, onScan]);
-
- // Stop scanner on unmount
- useEffect(() => {
- return () => {
- if (isScanning) {
- stopScanning();
- }
- };
- }, [isScanning, stopScanning]);
-
- // Handle tab change
- const handleTabChange = (value: string) => {
- if (isScanning) {
- stopScanning();
- }
- setTab(value as ScannerType);
- };
-
- // Start scanning with the selected type
- const handleStartScanning = async () => {
- await startScanning(scannerElementId, tab);
- };
-
- return (
-
-
-
Scan Barcode/QR Code
- {onClose && (
-
-
-
- )}
-
-
-
-
- Auto Detect
- Barcode
- QR Code
-
-
-
-
-
- Automatically detect and scan barcodes or QR codes.
-
-
-
-
-
-
-
- Scan linear barcodes: UPC, EAN, Code 128, Code 39, etc.
-
-
-
-
-
-
-
- Scan QR codes only.
-
-
-
-
-
-
- {/* Scanner video container */}
-
- {!isScanning && !isElectronEnvironment() && (
-
-
-
- Click 'Start Scanning' to activate camera
-
-
- )}
-
- {!isScanning && isElectronEnvironment() && (
-
- {tab === 'qrcode' ? (
-
- ) : (
-
- )}
-
- Click 'Start Scanning' to open the scanner
-
-
- )}
-
- {error && (
-
-
-
- {error.message}
-
-
stopScanning()}
- >
- Retry
-
-
-
- )}
-
-
- {/* Last scan result */}
- {lastScan && (
-
-
-
- {lastScan.format.includes('QR') ? (
-
- ) : (
-
- )}
-
-
-
{lastScan.format}
-
{lastScan.text}
-
-
-
- )}
-
- {/* Action buttons */}
-
- stopScanning()}
- disabled={!isScanning}
- >
- Stop
-
-
- {isScanning ? (
- <>
-
- Scanning...
- >
- ) : 'Start Scanning'}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/barcode/index.tsx b/client/src/components/barcode/index.tsx
deleted file mode 100644
index 55e81400..00000000
--- a/client/src/components/barcode/index.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-export { BarcodeScanner } from './barcode-scanner';
-export { BarcodeGenerator } from './barcode-generator';
\ No newline at end of file
diff --git a/client/src/components/billing/invoice-dialog.tsx b/client/src/components/billing/invoice-dialog.tsx
deleted file mode 100644
index 18fb100b..00000000
--- a/client/src/components/billing/invoice-dialog.tsx
+++ /dev/null
@@ -1,927 +0,0 @@
-import { useState, useEffect } from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { useMutation, useQuery } from "@tanstack/react-query";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { useToast } from "@/hooks/use-toast";
-import { apiRequest, queryClient } from "@/lib/queryClient";
-import { format, addDays } from "date-fns";
-import { Calendar } from "@/components/ui/calendar";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { CalendarIcon, FileText, Plus, Trash } from "lucide-react";
-import { cn } from "@/lib/utils";
-import {
- Table,
- TableBody,
- TableCell,
- TableFooter,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- Card,
- CardContent,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-
-// Invoice validation schema
-const invoiceFormSchema = z.object({
- customerId: z.number({
- required_error: "Customer is required",
- }),
- dueDate: z.date({
- required_error: "Due date is required",
- }),
- status: z.enum(["DRAFT", "SENT", "PAID", "PARTIALLY_PAID", "OVERDUE", "CANCELLED", "VOID"], {
- required_error: "Status is required",
- }).default("DRAFT"),
- notes: z.string().nullable().optional(),
- invoiceNumber: z.string().optional(),
- subtotal: z.number().optional(),
- taxAmount: z.number().optional(),
- discountAmount: z.number().optional(),
- total: z.number().optional(),
- amountPaid: z.number().optional(),
- dueAmount: z.number().optional(),
- items: z.array(
- z.object({
- id: z.number().optional(),
- itemId: z.number(),
- description: z.string(),
- quantity: z.number().min(0.01, "Quantity must be greater than 0"),
- unitPrice: z.number().min(0, "Unit price must be 0 or greater"),
- discount: z.number().min(0, "Discount must be 0 or greater").max(100, "Discount cannot exceed 100%").optional().nullable(),
- taxRate: z.number().min(0, "Tax rate must be 0 or greater").max(100, "Tax rate cannot exceed 100%").optional().nullable(),
- taxAmount: z.number().optional().nullable(),
- totalPrice: z.number(),
- })
- ).optional(),
-});
-
-type InvoiceFormValues = z.infer;
-
-// Invoice line item schema
-const invoiceItemSchema = z.object({
- itemId: z.number(),
- description: z.string().min(1, "Description is required"),
- quantity: z.number().min(0.01, "Quantity must be greater than 0"),
- unitPrice: z.number().min(0, "Unit price must be 0 or greater"),
- discount: z.number().min(0, "Discount must be 0 or greater").max(100, "Discount cannot exceed 100%").default(0),
- taxRate: z.number().min(0, "Tax rate must be 0 or greater").max(100, "Tax rate cannot exceed 100%").default(0),
-});
-
-type InvoiceItemValues = z.infer;
-
-export function InvoiceDialog({ open, onClose, invoice }) {
- const { toast } = useToast();
- const [items, setItems] = useState([]);
- const [newItemDialogOpen, setNewItemDialogOpen] = useState(false);
-
- // Calculate totals
- const calculateSubtotal = (items) => {
- return items.reduce((sum, item) => sum + (item.totalPrice || 0), 0);
- };
-
- const calculateTaxTotal = (items) => {
- return items.reduce((sum, item) => sum + (item.taxAmount || 0), 0);
- };
-
- // Calculate item total price
- const calculateItemTotalPrice = (quantity: number, unitPrice: number, discount: number = 0, taxRate: number = 0) => {
- const lineTotal = quantity * unitPrice;
- const discountAmount = (lineTotal * discount) / 100;
- const subtotalAfterDiscount = lineTotal - discountAmount;
- const taxAmount = (subtotalAfterDiscount * taxRate) / 100;
-
- return {
- totalPrice: subtotalAfterDiscount + taxAmount,
- taxAmount
- };
- };
-
- // Default values for the form
- const defaultValues: Partial = {
- customerId: 0,
- dueDate: addDays(new Date(), 30),
- status: "DRAFT",
- notes: "",
- items: [],
- };
-
- // Fetch inventory items query
- const { data: inventoryItems = [] } = useQuery({
- queryKey: ["/api/inventory"],
- queryFn: async () => {
- const response = await apiRequest("GET", "/api/inventory");
- if (!response.ok) {
- throw new Error("Failed to fetch inventory items");
- }
- return response.json();
- },
- enabled: open,
- });
-
- // Fetch customers query
- const { data: customers = [] } = useQuery({
- queryKey: ["/api/customers"],
- queryFn: async () => {
- const response = await apiRequest("GET", "/api/customers");
- if (!response.ok) {
- throw new Error("Failed to fetch customers");
- }
- return response.json();
- },
- enabled: open,
- });
-
- // Set up form
- const form = useForm({
- resolver: zodResolver(invoiceFormSchema),
- defaultValues: invoice ? { ...invoice } : defaultValues,
- });
-
- // Initialize form with invoice data if editing
- useEffect(() => {
- if (invoice) {
- const formattedInvoice = {
- ...invoice,
- dueDate: new Date(invoice.dueDate)
- };
-
- form.reset(formattedInvoice);
- setItems(invoice.items || []);
- } else {
- form.reset(defaultValues);
- setItems([]);
- }
- }, [invoice, form]);
-
- // New item form
- const newItemForm = useForm({
- resolver: zodResolver(invoiceItemSchema),
- defaultValues: {
- itemId: 0,
- description: "",
- quantity: 1,
- unitPrice: 0,
- discount: 0,
- taxRate: 0
- },
- });
-
- // Update item description and unit price when inventory item changes
- const handleInventoryItemChange = (itemId: number) => {
- const inventoryItem = inventoryItems.find(item => item.id === itemId);
- if (inventoryItem) {
- newItemForm.setValue("description", inventoryItem.name || "");
- newItemForm.setValue("unitPrice", inventoryItem.price || 0);
- }
- };
-
- // Calculate item total price when values change
- useEffect(() => {
- const subscription = newItemForm.watch((value) => {
- const quantity = parseFloat(value.quantity?.toString() || "0");
- const unitPrice = parseFloat(value.unitPrice?.toString() || "0");
- const discount = parseFloat(value.discount?.toString() || "0");
- const taxRate = parseFloat(value.taxRate?.toString() || "0");
-
- if (quantity && unitPrice) {
- const { totalPrice, taxAmount } = calculateItemTotalPrice(quantity, unitPrice, discount, taxRate);
- newItemForm.setValue("totalPrice", totalPrice);
- }
- });
-
- return () => subscription.unsubscribe();
- }, [newItemForm]);
-
- // Add new line item
- const handleAddItem = (data: InvoiceItemValues) => {
- const { totalPrice, taxAmount } = calculateItemTotalPrice(
- data.quantity,
- data.unitPrice,
- data.discount,
- data.taxRate
- );
-
- const newItem = {
- ...data,
- totalPrice,
- taxAmount
- };
-
- const updatedItems = [...items, newItem];
- setItems(updatedItems);
-
- // Update form values
- form.setValue("items", updatedItems);
- form.setValue("subtotal", calculateSubtotal(updatedItems));
- form.setValue("taxAmount", calculateTaxTotal(updatedItems));
- form.setValue("total", calculateSubtotal(updatedItems));
-
- // Close dialog and reset form
- setNewItemDialogOpen(false);
- newItemForm.reset({
- itemId: 0,
- description: "",
- quantity: 1,
- unitPrice: 0,
- discount: 0,
- taxRate: 0
- });
- };
-
- // Remove line item
- const handleRemoveItem = (index: number) => {
- const updatedItems = [...items];
- updatedItems.splice(index, 1);
- setItems(updatedItems);
-
- // Update form values
- form.setValue("items", updatedItems);
- form.setValue("subtotal", calculateSubtotal(updatedItems));
- form.setValue("taxAmount", calculateTaxTotal(updatedItems));
- form.setValue("total", calculateSubtotal(updatedItems));
- };
-
- // Create or update invoice mutation
- const invoiceMutation = useMutation({
- mutationFn: async (data: InvoiceFormValues) => {
- // Calculate totals
- const subtotal = calculateSubtotal(data.items || []);
- const taxAmount = calculateTaxTotal(data.items || []);
- const total = subtotal;
- const dueAmount = total - (data.amountPaid || 0);
-
- // Prepare data for submission
- const invoiceData = {
- ...data,
- subtotal,
- taxAmount,
- total,
- dueAmount
- };
-
- let res;
-
- if (invoice?.id) {
- // Update existing invoice
- res = await apiRequest("PATCH", `/api/invoices/${invoice.id}`, invoiceData);
- } else {
- // Create new invoice
- res = await apiRequest("POST", "/api/invoices", invoiceData);
- }
-
- if (!res.ok) throw new Error(invoice?.id ? "Failed to update invoice" : "Failed to create invoice");
-
- return await res.json();
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["/api/invoices"] });
-
- if (invoice?.id) {
- queryClient.invalidateQueries({ queryKey: ["/api/invoices", invoice.id] });
- }
-
- toast({
- title: invoice?.id ? "Invoice updated" : "Invoice created",
- description: invoice?.id ? "Invoice has been updated successfully" : "New invoice has been created successfully",
- });
-
- onClose(true);
- },
- onError: (error) => {
- toast({
- title: "Error",
- description: `Failed to ${invoice?.id ? "update" : "create"} invoice: ${error.message}`,
- variant: "destructive",
- });
- },
- });
-
- // Form submission
- const onSubmit = (data: InvoiceFormValues) => {
- invoiceMutation.mutate(data);
- };
-
- // Format currency
- const formatCurrency = (amount: number) => {
- return `$${amount.toFixed(2)}`;
- };
-
- // Get status badge
- const getStatusBadge = (status: string) => {
- let badgeVariant;
- switch (status) {
- case "PAID":
- badgeVariant = "success";
- break;
- case "PARTIALLY_PAID":
- badgeVariant = "warning";
- break;
- case "OVERDUE":
- badgeVariant = "destructive";
- break;
- case "DRAFT":
- badgeVariant = "outline";
- break;
- case "SENT":
- badgeVariant = "default";
- break;
- case "CANCELLED":
- case "VOID":
- badgeVariant = "secondary";
- break;
- default:
- badgeVariant = "outline";
- }
-
- // Convert status to user-friendly format
- const statusText = status
- .split("_")
- .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
- .join(" ");
-
- return (
-
- {statusText}
-
- );
- };
-
- return (
- !open && onClose(false)}>
-
-
-
-
- {invoice?.id ? "Edit Invoice" : "Create New Invoice"}
-
-
- {invoice?.id ? `Editing invoice #${invoice.invoiceNumber || invoice.id}` : "Enter the details for a new invoice"}
-
-
-
-
-
-
-
- {/* New Item Dialog */}
-
-
-
- Add Invoice Item
-
- Add a new item to this invoice.
-
-
-
-
-
- {/* Item Selection */}
- (
-
- Item
- {
- field.onChange(parseInt(value));
- handleInventoryItemChange(parseInt(value));
- }}
- defaultValue={field.value ? field.value.toString() : undefined}
- >
-
-
-
-
-
-
- {inventoryItems.map((item) => (
-
- {item.name} - {formatCurrency(item.price)}
-
- ))}
-
-
-
-
- )}
- />
-
- {/* Description */}
- (
-
- Description
-
-
-
-
-
- )}
- />
-
-
-
-
- {/* Discount */}
- (
-
- Discount %
-
- field.onChange(parseFloat(e.target.value) || 0)}
- />
-
-
-
- )}
- />
-
- {/* Tax Rate */}
- (
-
- Tax Rate %
-
- field.onChange(parseFloat(e.target.value) || 0)}
- />
-
-
-
- )}
- />
-
-
-
- setNewItemDialogOpen(false)}
- >
- Cancel
-
- Add Item
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/billing/invoices-list.tsx b/client/src/components/billing/invoices-list.tsx
deleted file mode 100644
index ad05a1c7..00000000
--- a/client/src/components/billing/invoices-list.tsx
+++ /dev/null
@@ -1,460 +0,0 @@
-import { useState } from "react";
-import { useMutation } from "@tanstack/react-query";
-import { apiRequest, queryClient } from "@/lib/queryClient";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import {
- MoreHorizontal,
- RefreshCw,
- ArrowDownUp,
- FileText,
- CreditCard,
- Trash2,
- Download,
- Send,
- Printer,
-} from "lucide-react";
-import { useToast } from "@/hooks/use-toast";
-import { format } from "date-fns";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
-
-interface InvoicesListProps {
- invoices: any[];
- onCreateInvoice: () => void;
- onPayInvoice: (invoice: any) => void;
- onEditInvoice: (invoice: any) => void;
- onRefresh: () => void;
-}
-
-export function InvoicesList({
- invoices,
- onCreateInvoice,
- onPayInvoice,
- onEditInvoice,
- onRefresh,
-}: InvoicesListProps) {
- const { toast } = useToast();
- const [sortField, setSortField] = useState("dueDate");
- const [sortDirection, setSortDirection] = useState("desc");
- const [selectedInvoice, setSelectedInvoice] = useState(null);
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
-
- // Sort invoices
- const sortedInvoices = [...invoices].sort((a, b) => {
- let valueA = a[sortField];
- let valueB = b[sortField];
-
- // Handle dates
- if (typeof valueA === 'string' && !isNaN(Date.parse(valueA))) {
- valueA = new Date(valueA).getTime();
- valueB = new Date(valueB).getTime();
- }
-
- // Handle numbers
- if (typeof valueA === "number" && typeof valueB === "number") {
- return sortDirection === "asc" ? valueA - valueB : valueB - valueA;
- }
-
- // Handle strings
- if (typeof valueA === "string" && typeof valueB === "string") {
- return sortDirection === "asc"
- ? valueA.localeCompare(valueB)
- : valueB.localeCompare(valueA);
- }
-
- return 0;
- });
-
- // Toggle sort
- const toggleSort = (field: string) => {
- if (field === sortField) {
- setSortDirection(sortDirection === "asc" ? "desc" : "asc");
- } else {
- setSortField(field);
- setSortDirection("asc");
- }
- };
-
- // Format currency
- const formatCurrency = (amount: number) => {
- return `$${amount.toFixed(2)}`;
- };
-
- // Get status badge
- const getStatusBadge = (status: string) => {
- let badgeVariant;
-
- switch (status) {
- case "PAID":
- badgeVariant = "success";
- break;
- case "PARTIALLY_PAID":
- badgeVariant = "warning";
- break;
- case "OVERDUE":
- badgeVariant = "destructive";
- break;
- case "DRAFT":
- badgeVariant = "outline";
- break;
- case "SENT":
- badgeVariant = "default";
- break;
- case "CANCELLED":
- case "VOID":
- badgeVariant = "secondary";
- break;
- default:
- badgeVariant = "outline";
- }
-
- // Convert status to user-friendly format
- const statusText = status
- .split("_")
- .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
- .join(" ");
-
- return (
-
- {statusText}
-
- );
- };
-
- // Delete invoice mutation
- const deleteInvoiceMutation = useMutation({
- mutationFn: async (id: number) => {
- const res = await apiRequest("DELETE", `/api/invoices/${id}`);
- if (!res.ok) throw new Error("Failed to delete invoice");
- return id;
- },
- onSuccess: (id) => {
- queryClient.invalidateQueries({ queryKey: ["/api/invoices"] });
-
- toast({
- title: "Invoice deleted",
- description: "The invoice has been deleted successfully.",
- });
- setDeleteDialogOpen(false);
- },
- onError: (error) => {
- toast({
- title: "Error",
- description: `Failed to delete invoice: ${error.message}`,
- variant: "destructive",
- });
- },
- });
-
- // Send invoice mutation
- const sendInvoiceMutation = useMutation({
- mutationFn: async (id: number) => {
- const res = await apiRequest("POST", `/api/invoices/${id}/send`);
- if (!res.ok) throw new Error("Failed to send invoice");
- return id;
- },
- onSuccess: (id) => {
- queryClient.invalidateQueries({ queryKey: ["/api/invoices"] });
- queryClient.invalidateQueries({ queryKey: ["/api/invoices", id] });
-
- toast({
- title: "Invoice sent",
- description: "The invoice has been marked as sent and an email would be sent to the customer.",
- });
- },
- onError: (error) => {
- toast({
- title: "Error",
- description: `Failed to send invoice: ${error.message}`,
- variant: "destructive",
- });
- },
- });
-
- // Handle invoice actions
- const handleDeleteClick = (invoice: any) => {
- setSelectedInvoice(invoice);
- setDeleteDialogOpen(true);
- };
-
- const handleSendClick = (invoice: any) => {
- sendInvoiceMutation.mutate(invoice.id);
- };
-
- // Handle document generation/printing/download
- const handlePrintClick = (invoice: any) => {
- toast({
- title: "Print feature",
- description: "Invoice printing is not yet implemented.",
- });
- };
-
- const handleDownloadClick = (invoice: any) => {
- toast({
- title: "Download feature",
- description: "Invoice download is not yet implemented.",
- });
- };
-
- return (
-
-
-
- {invoices.length} invoice{invoices.length !== 1 ? "s" : ""} found
-
-
-
-
- Refresh
-
-
-
- Create Invoice
-
-
-
-
- {invoices.length === 0 ? (
-
-
No invoices found
-
- No invoices have been created yet.
-
-
-
- Create Invoice
-
-
- ) : (
-
-
-
-
-
- toggleSort("invoiceNumber")}
- >
- Invoice #
- {sortField === "invoiceNumber" && (
-
- )}
-
-
-
- toggleSort("customerId")}
- >
- Customer
- {sortField === "customerId" && (
-
- )}
-
-
-
- toggleSort("createdAt")}
- >
- Created
- {sortField === "createdAt" && (
-
- )}
-
-
-
- toggleSort("dueDate")}
- >
- Due Date
- {sortField === "dueDate" && (
-
- )}
-
-
-
- toggleSort("status")}
- >
- Status
- {sortField === "status" && (
-
- )}
-
-
-
- toggleSort("total")}
- >
- Total
- {sortField === "total" && (
-
- )}
-
-
-
- toggleSort("amountPaid")}
- >
- Paid
- {sortField === "amountPaid" && (
-
- )}
-
-
-
- toggleSort("dueAmount")}
- >
- Balance
- {sortField === "dueAmount" && (
-
- )}
-
-
-
- Actions
-
-
-
-
- {sortedInvoices.map((invoice) => (
-
- {invoice.invoiceNumber || `#${invoice.id}`}
- {invoice.customer?.name || `Customer #${invoice.customerId}`}
- {format(new Date(invoice.createdAt), "MMM d, yyyy")}
- {format(new Date(invoice.dueDate), "MMM d, yyyy")}
- {getStatusBadge(invoice.status)}
- {formatCurrency(invoice.total)}
- {formatCurrency(invoice.amountPaid || 0)}
- 0 ? "text-red-600 dark:text-red-400 font-medium" : ""}>
- {formatCurrency(invoice.dueAmount)}
-
-
-
-
-
-
- Actions
-
-
-
- onEditInvoice(invoice)}>
-
- Edit Invoice
-
-
- {(invoice.status === "DRAFT" || invoice.status === "SENT" || invoice.status === "OVERDUE" || invoice.status === "PARTIALLY_PAID") && (
- onPayInvoice(invoice)}>
-
- Record Payment
-
- )}
-
- {invoice.status === "DRAFT" && (
- handleSendClick(invoice)} disabled={sendInvoiceMutation.isPending}>
-
- Mark as Sent
-
- )}
-
- handlePrintClick(invoice)}>
-
- Print
-
-
- handleDownloadClick(invoice)}>
-
- Download PDF
-
-
-
-
- handleDeleteClick(invoice)}
- className="text-red-600 dark:text-red-400"
- >
-
- Delete
-
-
-
-
-
- ))}
-
-
-
- )}
-
- {/* Delete Confirmation Dialog */}
-
-
-
- Are you sure?
-
- This will permanently delete the invoice and all associated line items. This action cannot be undone.
- {selectedInvoice && selectedInvoice.status !== "DRAFT" && " This invoice has already been processed. Deleting it may cause accounting inconsistencies."}
-
-
-
- Cancel
- selectedInvoice && deleteInvoiceMutation.mutate(selectedInvoice.id)}
- disabled={deleteInvoiceMutation.isPending}
- className="bg-red-600 hover:bg-red-700"
- >
- {deleteInvoiceMutation.isPending ? "Deleting..." : "Delete"}
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/billing/payment-dialog.tsx b/client/src/components/billing/payment-dialog.tsx
deleted file mode 100644
index 4513ca36..00000000
--- a/client/src/components/billing/payment-dialog.tsx
+++ /dev/null
@@ -1,459 +0,0 @@
-import { useState } from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { useMutation } from "@tanstack/react-query";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { useToast } from "@/hooks/use-toast";
-import { apiRequest, queryClient } from "@/lib/queryClient";
-import { format } from "date-fns";
-import { Calendar } from "@/components/ui/calendar";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { CalendarIcon, CreditCard } from "lucide-react";
-import { cn } from "@/lib/utils";
-import { Badge } from "@/components/ui/badge";
-
-// Payment validation schema
-const paymentFormSchema = z.object({
- invoiceId: z.number({
- required_error: "Invoice is required",
- }),
- paymentDate: z.date({
- required_error: "Payment date is required",
- }),
- amount: z.number({
- required_error: "Amount is required",
- }).min(0.01, "Amount must be greater than 0"),
- method: z.enum(["CASH", "CREDIT_CARD", "DEBIT_CARD", "BANK_TRANSFER", "CHECK", "PAYPAL", "OTHER"], {
- required_error: "Payment method is required",
- }),
- transactionReference: z.string().nullable().optional(),
- notes: z.string().nullable().optional(),
-});
-
-type PaymentFormValues = z.infer;
-
-export function PaymentDialog({ open, onClose, invoices }) {
- const { toast } = useToast();
- const [selectedInvoice, setSelectedInvoice] = useState(null);
-
- // Default form values
- const defaultValues: Partial = {
- invoiceId: 0,
- paymentDate: new Date(),
- amount: 0,
- method: "CASH",
- transactionReference: "",
- notes: "",
- };
-
- // Setup form
- const form = useForm({
- resolver: zodResolver(paymentFormSchema),
- defaultValues,
- });
-
- // Update the amount when invoice changes
- const handleInvoiceChange = (invoiceId: number) => {
- const invoice = invoices.find(inv => inv.id === invoiceId);
- setSelectedInvoice(invoice);
-
- if (invoice) {
- const dueAmount = invoice.total - (invoice.amountPaid || 0);
- form.setValue("amount", dueAmount);
- }
- };
-
- // Get invoice options with status badges
- const getInvoiceOptions = () => {
- return invoices
- .filter(invoice =>
- invoice.status !== "PAID" &&
- invoice.status !== "CANCELLED" &&
- invoice.status !== "VOID"
- )
- .map(invoice => ({
- id: invoice.id,
- label: `#${invoice.invoiceNumber || invoice.id} - ${format(new Date(invoice.dueDate), "MMM d, yyyy")}`,
- dueAmount: invoice.total - (invoice.amountPaid || 0),
- status: invoice.status,
- customerName: invoice.customer?.name || `Customer #${invoice.customerId}`
- }));
- };
-
- // Get status badge
- const getStatusBadge = (status: string) => {
- let badgeVariant;
- switch (status) {
- case "PARTIALLY_PAID":
- badgeVariant = "warning";
- break;
- case "OVERDUE":
- badgeVariant = "destructive";
- break;
- case "DRAFT":
- badgeVariant = "outline";
- break;
- case "SENT":
- badgeVariant = "default";
- break;
- default:
- badgeVariant = "outline";
- }
-
- // Convert status to user-friendly format
- const statusText = status
- .split("_")
- .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
- .join(" ");
-
- return (
-
- {statusText}
-
- );
- };
-
- // Format currency
- const formatCurrency = (amount: number) => {
- return `$${amount.toFixed(2)}`;
- };
-
- // Create payment mutation
- const paymentMutation = useMutation({
- mutationFn: async (data: PaymentFormValues) => {
- // Add receivedBy (current user ID - would come from auth in a real app)
- const paymentData = {
- ...data,
- receivedBy: 1, // Placeholder, would be the current user ID
- };
-
- const res = await apiRequest("POST", "/api/payments", paymentData);
-
- if (!res.ok) throw new Error("Failed to record payment");
-
- return await res.json();
- },
- onSuccess: (data) => {
- // Invalidate relevant queries
- queryClient.invalidateQueries({ queryKey: ["/api/payments"] });
- queryClient.invalidateQueries({ queryKey: ["/api/invoices", data.invoiceId] });
- queryClient.invalidateQueries({ queryKey: ["/api/invoices"] });
-
- toast({
- title: "Payment recorded",
- description: "The payment has been recorded successfully",
- });
-
- onClose(true);
- },
- onError: (error) => {
- toast({
- title: "Error",
- description: `Failed to record payment: ${error.message}`,
- variant: "destructive",
- });
- },
- });
-
- // Create payment with Stripe
- const processPaymentWithStripe = () => {
- const values = form.getValues();
- const invoice = invoices.find(invoice => invoice.id === values.invoiceId);
-
- // Check if we have Stripe keys configured
- if (!import.meta.env.VITE_STRIPE_PUBLIC_KEY) {
- toast({
- title: "Stripe not configured",
- description: "Stripe payment processing is not configured. Please add your Stripe API keys to process card payments.",
- variant: "destructive",
- });
- return;
- }
-
- toast({
- title: "Stripe integration",
- description: "Stripe payment processing would be initiated here with proper setup. For now, recording as a manual payment.",
- });
-
- // Submit the form with the current values
- form.handleSubmit(onSubmit)();
- };
-
- // Form submission
- const onSubmit = (data: PaymentFormValues) => {
- if (data.method === "CREDIT_CARD" || data.method === "DEBIT_CARD") {
- // If using card payment and Stripe is set up, process with Stripe
- if (import.meta.env.VITE_STRIPE_PUBLIC_KEY) {
- processPaymentWithStripe();
- return;
- }
- }
-
- // Otherwise, just record the payment directly
- paymentMutation.mutate(data);
- };
-
- return (
- !open && onClose(false)}>
-
-
-
-
- Record Payment
-
-
- Record a payment for an invoice. This will update the invoice's payment status.
-
-
-
-
-
- {/* Invoice Selection */}
- (
-
- Invoice
- {
- field.onChange(parseInt(value));
- handleInvoiceChange(parseInt(value));
- }}
- defaultValue={field.value ? field.value.toString() : undefined}
- disabled={paymentMutation.isPending}
- >
-
-
-
-
-
-
- {getInvoiceOptions().map((option) => (
-
-
- {option.label}
- {getStatusBadge(option.status)}
-
-
- {option.customerName} - Due: {formatCurrency(option.dueAmount)}
-
-
- ))}
-
-
-
-
- )}
- />
-
- {/* Payment Amount and Date in a row */}
-
-
- {/* Payment Method */}
- (
-
- Payment Method
-
-
-
-
-
-
-
- Cash
- Credit Card
- Debit Card
- Bank Transfer
- Check
- PayPal
- Other
-
-
- {(form.watch("method") === "CREDIT_CARD" || form.watch("method") === "DEBIT_CARD") && (
-
- Card payments will be processed through Stripe when configured.
-
- )}
-
-
- )}
- />
-
- {/* Transaction Reference */}
- (
-
- Reference/Transaction ID
-
-
-
-
- {form.watch("method") === "CHECK" ? "Check number" :
- form.watch("method") === "BANK_TRANSFER" ? "Transfer reference" :
- form.watch("method") === "CREDIT_CARD" || form.watch("method") === "DEBIT_CARD" ? "Card last 4 digits" :
- "Optional reference information"}
-
-
-
- )}
- />
-
- {/* Notes */}
- (
-
- Notes
-
-
-
-
-
- )}
- />
-
-
- onClose(false)}
- disabled={paymentMutation.isPending}
- >
- Cancel
-
-
- {paymentMutation.isPending ? "Processing..." : "Record Payment"}
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/billing/payments-list.tsx b/client/src/components/billing/payments-list.tsx
deleted file mode 100644
index c6242c84..00000000
--- a/client/src/components/billing/payments-list.tsx
+++ /dev/null
@@ -1,379 +0,0 @@
-import { useState } from "react";
-import { useMutation } from "@tanstack/react-query";
-import { apiRequest, queryClient } from "@/lib/queryClient";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import {
- MoreHorizontal,
- RefreshCw,
- ArrowDownUp,
- CreditCard,
- Trash2,
- FileText,
- Printer,
- Download,
-} from "lucide-react";
-import { useToast } from "@/hooks/use-toast";
-import { format } from "date-fns";
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog";
-
-interface PaymentsListProps {
- payments: any[];
- onCreatePayment: () => void;
- onRefresh: () => void;
-}
-
-export function PaymentsList({
- payments,
- onCreatePayment,
- onRefresh,
-}: PaymentsListProps) {
- const { toast } = useToast();
- const [sortField, setSortField] = useState("paymentDate");
- const [sortDirection, setSortDirection] = useState("desc");
- const [selectedPayment, setSelectedPayment] = useState(null);
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
-
- // Sort payments
- const sortedPayments = [...payments].sort((a, b) => {
- let valueA = a[sortField];
- let valueB = b[sortField];
-
- // Handle dates
- if (typeof valueA === 'string' && !isNaN(Date.parse(valueA))) {
- valueA = new Date(valueA).getTime();
- valueB = new Date(valueB).getTime();
- }
-
- // Handle numbers
- if (typeof valueA === "number" && typeof valueB === "number") {
- return sortDirection === "asc" ? valueA - valueB : valueB - valueA;
- }
-
- // Handle strings
- if (typeof valueA === "string" && typeof valueB === "string") {
- return sortDirection === "asc"
- ? valueA.localeCompare(valueB)
- : valueB.localeCompare(valueA);
- }
-
- return 0;
- });
-
- // Toggle sort
- const toggleSort = (field: string) => {
- if (field === sortField) {
- setSortDirection(sortDirection === "asc" ? "desc" : "asc");
- } else {
- setSortField(field);
- setSortDirection("asc");
- }
- };
-
- // Format currency
- const formatCurrency = (amount: number) => {
- return `$${amount.toFixed(2)}`;
- };
-
- // Get payment method badge
- const getPaymentMethodBadge = (method: string) => {
- let badgeVariant;
-
- switch (method) {
- case "CREDIT_CARD":
- case "DEBIT_CARD":
- badgeVariant = "default";
- break;
- case "CASH":
- badgeVariant = "success";
- break;
- case "BANK_TRANSFER":
- badgeVariant = "secondary";
- break;
- case "CHECK":
- badgeVariant = "outline";
- break;
- case "PAYPAL":
- badgeVariant = "info";
- break;
- default:
- badgeVariant = "outline";
- }
-
- // Convert method to user-friendly format
- const methodText = method
- .split("_")
- .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
- .join(" ");
-
- return (
-
- {methodText}
-
- );
- };
-
- // Delete payment mutation
- const deletePaymentMutation = useMutation({
- mutationFn: async (id: number) => {
- const res = await apiRequest("DELETE", `/api/payments/${id}`);
- if (!res.ok) throw new Error("Failed to delete payment");
- return id;
- },
- onSuccess: (id) => {
- queryClient.invalidateQueries({ queryKey: ["/api/payments"] });
- // Also invalidate the invoice this payment was for to update its status
- if (selectedPayment?.invoiceId) {
- queryClient.invalidateQueries({ queryKey: ["/api/invoices", selectedPayment.invoiceId] });
- queryClient.invalidateQueries({ queryKey: ["/api/invoices"] });
- }
-
- toast({
- title: "Payment deleted",
- description: "The payment has been deleted successfully.",
- });
- setDeleteDialogOpen(false);
- },
- onError: (error) => {
- toast({
- title: "Error",
- description: `Failed to delete payment: ${error.message}`,
- variant: "destructive",
- });
- },
- });
-
- // Handle payment actions
- const handleDeleteClick = (payment: any) => {
- setSelectedPayment(payment);
- setDeleteDialogOpen(true);
- };
-
- // Handle document generation
- const handlePrintClick = (payment: any) => {
- toast({
- title: "Print feature",
- description: "Payment receipt printing is not yet implemented.",
- });
- };
-
- const handleDownloadClick = (payment: any) => {
- toast({
- title: "Download feature",
- description: "Payment receipt download is not yet implemented.",
- });
- };
-
- const handleViewInvoiceClick = (payment: any) => {
- toast({
- title: "View Invoice",
- description: "Viewing associated invoice is not yet implemented.",
- });
- };
-
- return (
-
-
-
- {payments.length} payment{payments.length !== 1 ? "s" : ""} found
-
-
-
-
- Refresh
-
-
-
- Record Payment
-
-
-
-
- {payments.length === 0 ? (
-
-
No payments found
-
- No payments have been recorded yet.
-
-
-
- Record Payment
-
-
- ) : (
-
-
-
-
-
- toggleSort("id")}
- >
- Payment ID
- {sortField === "id" && (
-
- )}
-
-
-
- toggleSort("invoiceId")}
- >
- Invoice #
- {sortField === "invoiceId" && (
-
- )}
-
-
-
- toggleSort("paymentDate")}
- >
- Date
- {sortField === "paymentDate" && (
-
- )}
-
-
-
- toggleSort("method")}
- >
- Method
- {sortField === "method" && (
-
- )}
-
-
-
- toggleSort("amount")}
- >
- Amount
- {sortField === "amount" && (
-
- )}
-
-
-
- Actions
-
-
-
-
- {sortedPayments.map((payment) => (
-
- #{payment.id}
-
- {payment.invoice?.invoiceNumber || `#${payment.invoiceId}`}
-
- {format(new Date(payment.paymentDate), "MMM d, yyyy")}
- {getPaymentMethodBadge(payment.method)}
- {formatCurrency(payment.amount)}
-
-
-
-
-
- Actions
-
-
-
- handleViewInvoiceClick(payment)}>
-
- View Invoice
-
-
- handlePrintClick(payment)}>
-
- Print Receipt
-
-
- handleDownloadClick(payment)}>
-
- Download Receipt
-
-
-
-
- handleDeleteClick(payment)}
- className="text-red-600 dark:text-red-400"
- >
-
- Delete
-
-
-
-
-
- ))}
-
-
-
- )}
-
- {/* Delete Confirmation Dialog */}
-
-
-
- Are you sure?
-
- This will permanently delete this payment record. This action cannot be undone.
- This will also update the payment status of the associated invoice.
-
-
-
- Cancel
- selectedPayment && deletePaymentMutation.mutate(selectedPayment.id)}
- disabled={deletePaymentMutation.isPending}
- className="bg-red-600 hover:bg-red-700"
- >
- {deletePaymentMutation.isPending ? "Deleting..." : "Delete"}
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/electron/electron-provider.tsx b/client/src/components/electron/electron-provider.tsx
deleted file mode 100644
index 5904e863..00000000
--- a/client/src/components/electron/electron-provider.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { ReactNode, createContext, useContext, useEffect, useState } from "react";
-import { appControls, isElectron } from "@/lib/electron-bridge";
-
-interface UpdateInfo {
- version: string;
- releaseDate: string;
- releaseNotes: string;
-}
-
-interface ElectronContextType {
- isElectron: boolean;
- appVersion: string;
- updateAvailable: UpdateInfo | null;
- updateDownloaded: UpdateInfo | null;
- installUpdate: () => void;
-}
-
-const ElectronContext = createContext({
- isElectron: false,
- appVersion: "",
- updateAvailable: null,
- updateDownloaded: null,
- installUpdate: () => {},
-});
-
-export const useElectron = () => useContext(ElectronContext);
-
-interface ElectronProviderProps {
- children: ReactNode;
-}
-
-export const ElectronProvider = ({ children }: ElectronProviderProps) => {
- const [appVersion, setAppVersion] = useState("");
- const [updateAvailable, setUpdateAvailable] = useState(null);
- const [updateDownloaded, setUpdateDownloaded] = useState(null);
-
- useEffect(() => {
- // Get application version
- const getAppVersion = async () => {
- if (isElectron) {
- const version = await appControls.getVersion();
- setAppVersion(version);
- }
- };
-
- getAppVersion();
-
- // Set up update listeners
- if (isElectron) {
- const removeUpdateAvailableListener = appControls.onUpdateAvailable((info) => {
- setUpdateAvailable(info);
- });
-
- const removeUpdateDownloadedListener = appControls.onUpdateDownloaded((info) => {
- setUpdateDownloaded(info);
- });
-
- return () => {
- removeUpdateAvailableListener();
- removeUpdateDownloadedListener();
- };
- }
- }, []);
-
- const installUpdate = () => {
- if (isElectron && updateDownloaded) {
- appControls.installUpdate();
- }
- };
-
- return (
-
- {children}
-
- );
-};
\ No newline at end of file
diff --git a/client/src/components/electron/index.ts b/client/src/components/electron/index.ts
deleted file mode 100644
index d767d482..00000000
--- a/client/src/components/electron/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { ElectronProvider, useElectron } from "./electron-provider";
-export { TitleBar } from "./title-bar";
-export { UpdateNotification } from "./update-notification";
\ No newline at end of file
diff --git a/client/src/components/electron/index.tsx b/client/src/components/electron/index.tsx
deleted file mode 100644
index e26fd1b6..00000000
--- a/client/src/components/electron/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react';
-
-export { TitleBar } from './title-bar';
-export { UpdateNotification } from './update-notification';
-export { OfflineModeIndicator } from './offline-mode-indicator';
-export { LocalDatabaseInfo } from './local-database-info';
\ No newline at end of file
diff --git a/client/src/components/electron/local-database-info.tsx b/client/src/components/electron/local-database-info.tsx
deleted file mode 100644
index d1a4190b..00000000
--- a/client/src/components/electron/local-database-info.tsx
+++ /dev/null
@@ -1,281 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useElectron } from '../../contexts/electron-provider';
-import { Loader2, Database, Save, RefreshCw, FileText, Check, AlertTriangle } from 'lucide-react';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Progress } from '@/components/ui/progress';
-import { Badge } from '@/components/ui/badge';
-import { useToast } from '@/hooks/use-toast';
-
-/**
- * Component that displays local database information
- */
-export const LocalDatabaseInfo: React.FC = () => {
- const { isElectron, bridge } = useElectron();
- const { toast } = useToast();
- const [dbInfo, setDbInfo] = useState<{
- status: 'healthy' | 'degraded' | 'error' | 'unknown';
- size: string;
- location: string;
- lastBackup: string | null;
- dataCount: {
- inventory: number;
- movements: number;
- suppliers: number;
- users: number;
- };
- } | null>(null);
- const [isLoading, setIsLoading] = useState(true);
- const [isBackingUp, setIsBackingUp] = useState(false);
- const [isSyncing, setIsSyncing] = useState(false);
- const [syncProgress, setSyncProgress] = useState(0);
-
- useEffect(() => {
- if (!isElectron || !bridge) return;
-
- const fetchDatabaseInfo = async () => {
- try {
- setIsLoading(true);
- const info = await bridge.getDatabaseInfo();
- setDbInfo(info);
- } catch (error) {
- console.error('Failed to fetch database info:', error);
- toast({
- title: 'Error',
- description: 'Failed to retrieve database information',
- variant: 'destructive',
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchDatabaseInfo();
-
- // Listen for database status updates
- const removeDatabaseStatusListener = bridge.on('database-status-changed', (info) => {
- setDbInfo(info);
- });
-
- // Listen for sync progress
- const removeSyncProgressListener = bridge.on('sync-progress', (progress: number) => {
- setSyncProgress(progress);
- });
-
- return () => {
- removeDatabaseStatusListener();
- removeSyncProgressListener();
- };
- }, [isElectron, bridge, toast]);
-
- const handleCreateBackup = async () => {
- if (!isElectron || !bridge) return;
-
- try {
- setIsBackingUp(true);
- const result = await bridge.createDatabaseBackup();
- if (result.success) {
- toast({
- title: 'Backup Created',
- description: `Backup saved to ${result.path}`,
- variant: 'default',
- });
- // Refresh database info to show updated last backup time
- const info = await bridge.getDatabaseInfo();
- setDbInfo(info);
- } else {
- throw new Error(result.error || 'Unknown error');
- }
- } catch (error) {
- console.error('Failed to create backup:', error);
- toast({
- title: 'Backup Failed',
- description: error instanceof Error ? error.message : 'Unknown error occurred',
- variant: 'destructive',
- });
- } finally {
- setIsBackingUp(false);
- }
- };
-
- const handleSync = async () => {
- if (!isElectron || !bridge) return;
-
- try {
- setIsSyncing(true);
- setSyncProgress(0);
- await bridge.syncDatabase();
- toast({
- title: 'Sync Complete',
- description: 'Database synchronized successfully',
- variant: 'default',
- });
- // Refresh database info
- const info = await bridge.getDatabaseInfo();
- setDbInfo(info);
- } catch (error) {
- console.error('Failed to sync database:', error);
- toast({
- title: 'Sync Failed',
- description: error instanceof Error ? error.message : 'Unknown error occurred',
- variant: 'destructive',
- });
- } finally {
- setIsSyncing(false);
- setSyncProgress(0);
- }
- };
-
- if (!isElectron) {
- return (
-
-
- Local Database
-
- Only available in desktop application
-
-
-
- );
- }
-
- if (isLoading) {
- return (
-
-
- Local Database
-
- Loading database information...
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- Local Database
-
- {dbInfo?.status && (
-
- {dbInfo.status === 'healthy' && }
- {dbInfo.status === 'error' && }
- {dbInfo.status.charAt(0).toUpperCase() + dbInfo.status.slice(1)}
-
- )}
-
-
- Manage your local database and synchronization
-
-
-
-
- {dbInfo && (
- <>
-
-
-
Database Size
-
{dbInfo.size || 'Unknown'}
-
-
-
Location
-
{dbInfo.location || 'Unknown'}
-
-
-
Last Backup
-
{dbInfo.lastBackup || 'Never'}
-
-
-
-
-
Data Records
-
-
-
Inventory
-
{dbInfo.dataCount?.inventory || 0}
-
-
-
Movements
-
{dbInfo.dataCount?.movements || 0}
-
-
-
Suppliers
-
{dbInfo.dataCount?.suppliers || 0}
-
-
-
Users
-
{dbInfo.dataCount?.users || 0}
-
-
-
-
- {isSyncing && (
-
-
-
Syncing data...
-
{syncProgress}%
-
-
-
- )}
- >
- )}
-
-
-
-
- {isBackingUp ? (
- <>
-
- Backing Up...
- >
- ) : (
- <>
-
- Create Backup
- >
- )}
-
-
- {isSyncing ? (
- <>
-
- Syncing...
- >
- ) : (
- <>
-
- Sync Database
- >
- )}
-
-
-
- );
-};
\ No newline at end of file
diff --git a/client/src/components/electron/offline-mode-indicator.tsx b/client/src/components/electron/offline-mode-indicator.tsx
deleted file mode 100644
index a4b2995e..00000000
--- a/client/src/components/electron/offline-mode-indicator.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useElectron } from '../../contexts/electron-provider';
-import { WifiOff } from 'lucide-react';
-import { Alert, AlertDescription } from '@/components/ui/alert';
-
-/**
- * Component that displays when the application is in offline mode
- */
-export const OfflineModeIndicator: React.FC = () => {
- const { isElectron, bridge } = useElectron();
- const [isOffline, setIsOffline] = useState(false);
- const [connectionStatus, setConnectionStatus] = useState<'online' | 'offline' | 'checking'>('checking');
-
- useEffect(() => {
- if (!isElectron || !bridge) return;
-
- const checkNetworkStatus = async () => {
- try {
- setConnectionStatus('checking');
- const status = await bridge.checkNetworkStatus();
- setConnectionStatus(status ? 'online' : 'offline');
- setIsOffline(!status);
- } catch (error) {
- console.error('Failed to check network status:', error);
- setConnectionStatus('offline');
- setIsOffline(true);
- }
- };
-
- // Initial check
- checkNetworkStatus();
-
- // Setup interval for checking network status
- const interval = setInterval(checkNetworkStatus, 30000); // Check every 30 seconds
-
- // Setup online/offline event listeners as a backup
- const handleOnline = () => {
- setConnectionStatus('online');
- setIsOffline(false);
- };
-
- const handleOffline = () => {
- setConnectionStatus('offline');
- setIsOffline(true);
- };
-
- window.addEventListener('online', handleOnline);
- window.addEventListener('offline', handleOffline);
-
- // Set up listener for IPC events related to network status
- const removeNetworkListener = bridge.on('network-status-changed', (status: boolean) => {
- setConnectionStatus(status ? 'online' : 'offline');
- setIsOffline(!status);
- });
-
- return () => {
- clearInterval(interval);
- window.removeEventListener('online', handleOnline);
- window.removeEventListener('offline', handleOffline);
- removeNetworkListener();
- };
- }, [isElectron, bridge]);
-
- if (!isElectron || !isOffline) {
- return null;
- }
-
- return (
-
-
-
- You are currently working offline. Changes will be synchronized when you reconnect.
-
-
- );
-};
\ No newline at end of file
diff --git a/client/src/components/electron/title-bar.tsx b/client/src/components/electron/title-bar.tsx
deleted file mode 100644
index f1ea4cec..00000000
--- a/client/src/components/electron/title-bar.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from 'react';
-import { useElectron } from '../../contexts/electron-provider';
-import { X, Minus, Square } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-
-interface TitleBarProps {
- title?: string;
-}
-
-export const TitleBar: React.FC = ({ title = 'Inventory Management System' }) => {
- const { isElectron, electron } = useElectron();
-
- if (!isElectron) {
- return null;
- }
-
- const handleMinimize = () => {
- if (electron) {
- electron.invoke('window-minimize');
- }
- };
-
- const handleMaximize = () => {
- if (electron) {
- electron.invoke('window-maximize');
- }
- };
-
- const handleClose = () => {
- if (electron) {
- electron.invoke('window-close');
- }
- };
-
- return (
-
-
-
-
-
-
{title}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/client/src/components/electron/update-notification.tsx b/client/src/components/electron/update-notification.tsx
deleted file mode 100644
index a0d5e6b1..00000000
--- a/client/src/components/electron/update-notification.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useElectron } from '../../contexts/electron-provider';
-import { AlertCircle, Download, X } from 'lucide-react';
-import {
- Alert,
- AlertDescription,
- AlertTitle
-} from "@/components/ui/alert";
-import { Button } from "@/components/ui/button";
-
-export const UpdateNotification: React.FC = () => {
- const { isElectron, electron } = useElectron();
- const [updateAvailable, setUpdateAvailable] = useState(false);
- const [updateInfo, setUpdateInfo] = useState<{ version: string; notes: string } | null>(null);
- const [isInstalling, setIsInstalling] = useState(false);
- const [dismissed, setDismissed] = useState(false);
-
- useEffect(() => {
- if (!isElectron || !electron) return;
-
- // Listen for update notifications from the main process
- const removeListener = electron.on('update-available', (info: any) => {
- setUpdateAvailable(true);
- setUpdateInfo({
- version: info.version || 'New Version',
- notes: info.releaseNotes || 'Bug fixes and improvements'
- });
- });
-
- // Listen for update progress events
- electron.on('update-progress', (progress: number) => {
- // Could implement a progress bar here
- console.log(`Update progress: ${progress}%`);
- });
-
- // Listen for update downloaded event
- electron.on('update-downloaded', () => {
- setIsInstalling(false);
- });
-
- // Check for updates when component mounts
- electron.invoke('check-for-updates').catch(console.error);
-
- return () => {
- // Cleanup listeners on unmount
- removeListener();
- };
- }, [isElectron, electron]);
-
- const handleInstallUpdate = () => {
- if (!isElectron || !electron) return;
-
- setIsInstalling(true);
- electron.invoke('install-update').catch((error: unknown) => {
- console.error('Failed to install update:', error);
- setIsInstalling(false);
- });
- };
-
- const handleDismiss = () => {
- setDismissed(true);
- };
-
- if (!isElectron || !updateAvailable || dismissed) {
- return null;
- }
-
- return (
-
-
-
-
- Update Available: {updateInfo?.version}
-
-
- {updateInfo?.notes}
-
-
-
- {isInstalling ? 'Installing...' : 'Install Now'}
- {!isInstalling && }
-
-
- Later
-
-
-
-
- );
-};
\ No newline at end of file
diff --git a/client/src/components/inventory/image-recognition-status.tsx b/client/src/components/inventory/image-recognition-status.tsx
deleted file mode 100644
index 2416ba53..00000000
--- a/client/src/components/inventory/image-recognition-status.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import React from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { AlertCircle, CheckCircle, Info } from 'lucide-react';
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- CardDescription
-} from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
-import { Skeleton } from '@/components/ui/skeleton';
-
-/**
- * Component that displays the status of the image recognition service
- */
-const ImageRecognitionStatus: React.FC = () => {
- // Query the status of the image recognition service
- const { data, isLoading, error } = useQuery({
- queryKey: ['/api/image-recognition/status'],
- staleTime: 5 * 60 * 1000, // 5 minutes
- });
-
- if (isLoading) {
- return (
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
-
- Image Recognition Error
-
- Unable to check image recognition status
-
-
-
- There was an error checking the image recognition service status. This may indicate a connectivity issue.
-
-
-
- );
- }
-
- const status = data?.status || 'unknown';
- const mode = data?.mode || 'Unknown';
- const aiProvider = data?.aiProvider || 'None';
- const message = data?.message || 'Status information not available';
-
- // UI variants based on status
- const variants = {
- operational: {
- icon: ,
- title: 'AI Image Recognition Active',
- badgeVariant: 'success',
- badgeText: 'Operational',
- cardStyles: 'border-green-200 bg-green-50',
- },
- simulation: {
- icon: ,
- title: 'Simulation Mode Active',
- badgeVariant: 'warning',
- badgeText: 'Simulation',
- cardStyles: 'border-amber-200 bg-amber-50',
- },
- error: {
- icon: ,
- title: 'Service Unavailable',
- badgeVariant: 'destructive',
- badgeText: 'Error',
- cardStyles: 'border-red-200 bg-red-50',
- },
- unknown: {
- icon: ,
- title: 'Status Unknown',
- badgeVariant: 'outline',
- badgeText: 'Unknown',
- cardStyles: 'border-gray-200',
- },
- };
-
- // Get the appropriate UI variant based on status
- const variant = variants[status as keyof typeof variants] || variants.unknown;
-
- return (
-
-
-
-
- {variant.icon}
- {variant.title}
-
- {variant.badgeText}
-
-
- Provider: {aiProvider} | Mode: {mode}
-
-
-
-
-
-
- {message}
-
-
- {status === 'simulation' ? (
-
- To enable real AI image recognition, set the OPENAI_API_KEY environment variable.
- Contact your administrator for more information.
-
- ) : (
- {message}
- )}
-
-
-
-
-
- );
-};
-
-export default ImageRecognitionStatus;
\ No newline at end of file
diff --git a/client/src/components/inventory/image-recognition-upload.tsx b/client/src/components/inventory/image-recognition-upload.tsx
deleted file mode 100644
index 5b8f69d7..00000000
--- a/client/src/components/inventory/image-recognition-upload.tsx
+++ /dev/null
@@ -1,533 +0,0 @@
-/**
- * Image Recognition Upload Component
- *
- * This component provides a UI for uploading images of inventory items
- * to be analyzed by the AI image recognition service.
- */
-
-import { useState, useRef } from 'react';
-import { useMutation } from '@tanstack/react-query';
-import { apiRequest } from '@/lib/queryClient';
-import { useToast } from '@/hooks/use-toast';
-import { Loader2, Upload, Camera, Check, X, RefreshCw, Image as ImageIcon } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
-import { Separator } from '@/components/ui/separator';
-import { Switch } from '@/components/ui/switch';
-import { Label } from '@/components/ui/label';
-import { Progress } from '@/components/ui/progress';
-import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
-import { Badge } from '@/components/ui/badge';
-import { Input } from '@/components/ui/input';
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-interface ImageRecognitionUploadProps {
- onItemCreated?: (newItem: any) => void;
- standalone?: boolean;
-}
-
-// Schema for the additional item data form
-const additionalItemDataSchema = z.object({
- price: z.string().optional().transform(val => val ? parseFloat(val) : 0),
- quantity: z.string().optional().transform(val => val ? parseInt(val, 10) : 0),
- lowStockThreshold: z.string().optional().transform(val => val ? parseInt(val, 10) : null),
- location: z.string().optional(),
- categoryId: z.string().optional().transform(val => val ? parseInt(val, 10) : null),
- notes: z.string().optional(),
-});
-
-export function ImageRecognitionUpload({ onItemCreated, standalone = false }: ImageRecognitionUploadProps) {
- const [imageFile, setImageFile] = useState(null);
- const [imagePreview, setImagePreview] = useState(null);
- const [recognizedData, setRecognizedData] = useState(null);
- const [uploadProgress, setUploadProgress] = useState(0);
- const [detectText, setDetectText] = useState(true);
- const [generateDescription, setGenerateDescription] = useState(true);
- const fileInputRef = useRef(null);
- const { toast } = useToast();
-
- // Form for additional item data
- const form = useForm({
- resolver: zodResolver(additionalItemDataSchema),
- defaultValues: {
- price: '',
- quantity: '',
- lowStockThreshold: '',
- location: '',
- categoryId: '',
- notes: '',
- },
- });
-
- // Analyze image mutation
- const analyzeImageMutation = useMutation({
- mutationFn: async (file: File) => {
- const formData = new FormData();
- formData.append('image', file);
- formData.append('detectText', detectText.toString());
- formData.append('generateDescription', generateDescription.toString());
-
- // Simulated progress updates
- const progressUpdates = [25, 50, 75, 99];
- let progressIndex = 0;
- const progressInterval = setInterval(() => {
- if (progressIndex < progressUpdates.length) {
- setUploadProgress(progressUpdates[progressIndex]);
- progressIndex++;
- } else {
- clearInterval(progressInterval);
- }
- }, 500);
-
- try {
- const response = await apiRequest('POST', '/api/inventory/image-recognition/analyze', formData, {
- headers: {
- // Don't set Content-Type header when using FormData
- // Browser will set it automatically with proper boundary
- },
- });
-
- clearInterval(progressInterval);
- setUploadProgress(100);
-
- const data = await response.json();
- return data;
- } catch (error) {
- clearInterval(progressInterval);
- setUploadProgress(0);
- throw error;
- }
- },
- onSuccess: (data) => {
- setRecognizedData(data.recognizedItem);
- toast({
- title: 'Image Analyzed',
- description: 'Image successfully analyzed by AI.',
- });
- },
- onError: (error: Error) => {
- toast({
- title: 'Analysis Failed',
- description: error.message || 'Failed to analyze image',
- variant: 'destructive',
- });
- },
- });
-
- // Create item from recognized data mutation
- const createItemMutation = useMutation({
- mutationFn: async (itemData: any) => {
- const response = await apiRequest('POST', '/api/inventory/image-recognition/create', itemData);
- return await response.json();
- },
- onSuccess: (data) => {
- toast({
- title: 'Item Created',
- description: 'New inventory item created from image.',
- });
-
- // Reset the component state
- setImageFile(null);
- setImagePreview(null);
- setRecognizedData(null);
- setUploadProgress(0);
- form.reset();
-
- // Notify parent component if callback provided
- if (onItemCreated) {
- onItemCreated(data.item);
- }
- },
- onError: (error: Error) => {
- toast({
- title: 'Creation Failed',
- description: error.message || 'Failed to create item',
- variant: 'destructive',
- });
- },
- });
-
- // Handle file selection
- const handleFileChange = (e: React.ChangeEvent) => {
- if (e.target.files && e.target.files[0]) {
- const file = e.target.files[0];
- setImageFile(file);
-
- // Create a preview URL
- const reader = new FileReader();
- reader.onload = (event) => {
- setImagePreview(event.target?.result as string);
- };
- reader.readAsDataURL(file);
-
- // Reset previous recognition data
- setRecognizedData(null);
- }
- };
-
- // Handle camera/file upload button
- const handleUploadClick = () => {
- fileInputRef.current?.click();
- };
-
- // Handle analyze button
- const handleAnalyzeClick = () => {
- if (imageFile) {
- analyzeImageMutation.mutate(imageFile);
- }
- };
-
- // Handle create item button
- const handleCreateItem = form.handleSubmit((additionalData) => {
- if (recognizedData) {
- createItemMutation.mutate({
- recognizedItem: recognizedData,
- additionalData,
- });
- }
- });
-
- // Reset the component
- const handleReset = () => {
- setImageFile(null);
- setImagePreview(null);
- setRecognizedData(null);
- setUploadProgress(0);
- form.reset();
- };
-
- return (
-
-
-
-
- AI Image Recognition
-
-
- Upload an image to automatically identify inventory items
-
-
-
-
- {/* Image upload area */}
-
-
- {imagePreview ? (
-
-
-
-
-
-
- ) : (
-
-
-
-
-
- Upload an image to identify the item
-
-
-
- Upload Image
-
-
-
- )}
-
-
-
-
Recognition Options
-
-
-
-
-
Detect Text
-
- Read text from labels or packaging
-
-
-
-
-
-
-
-
Generate Description
-
- AI creates a detailed description
-
-
-
-
-
-
-
-
-
- {analyzeImageMutation.isPending ? (
-
-
- Analyzing image...
- {uploadProgress}%
-
-
-
- ) : (
-
- {recognizedData ? (
- <>
-
- Analysis Complete
- >
- ) : (
- <>
-
- Analyze Image
- >
- )}
-
- )}
-
-
-
-
- {/* Recognition results */}
- {recognizedData && (
-
-
Recognition Results
-
-
-
-
Identified Item
-
{recognizedData.name}
- {recognizedData.description && (
-
- {recognizedData.description}
-
- )}
-
-
-
-
Attributes
-
- {recognizedData.category && (
- {recognizedData.category}
- )}
- {recognizedData.attributes && Object.entries(recognizedData.attributes).map(([key, value]) => (
-
- {key}: {value as string}
-
- ))}
-
- {Math.round(recognizedData.confidence * 100)}% confidence
-
-
-
- {recognizedData.detectedText && (
-
-
Detected Text
-
- {recognizedData.detectedText}
-
-
- )}
-
-
-
- )}
-
- {/* Additional item data form (shown when item is recognized) */}
- {recognizedData && (
-
-
-
- )}
-
-
-
-
-
- Reset
-
-
-
-
-
- {createItemMutation.isPending ? (
- <>
-
- Creating...
- >
- ) : (
- <>
-
- Create Item
- >
- )}
-
-
-
-
- Item Created Successfully
-
- The following inventory item has been created based on image recognition:
-
-
-
- {/* This would show details of the created item */}
-
-
{recognizedData?.name}
-
{recognizedData?.description}
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/inventory/item-form.tsx b/client/src/components/inventory/item-form.tsx
deleted file mode 100644
index a06a7e08..00000000
--- a/client/src/components/inventory/item-form.tsx
+++ /dev/null
@@ -1,379 +0,0 @@
-import { useState, useEffect } from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle
-} from "@/components/ui/dialog";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
-import { useToast } from "@/hooks/use-toast";
-import { apiRequest } from "@/lib/queryClient";
-import { type Category, type InventoryItem, type InventoryItemForm, inventoryItemFormSchema } from "@shared/schema";
-
-interface ItemFormProps {
- open: boolean;
- setOpen: (open: boolean) => void;
- initialData?: InventoryItem | null;
-}
-
-export default function ItemForm({ open, setOpen, initialData = null }: ItemFormProps) {
- const queryClient = useQueryClient();
- const { toast } = useToast();
- const [loading, setLoading] = useState(false);
-
- // Form setup
- const form = useForm({
- resolver: zodResolver(inventoryItemFormSchema),
- defaultValues: {
- name: initialData?.name || "",
- sku: initialData?.sku || "",
- description: initialData?.description || "",
- categoryId: initialData?.categoryId || undefined,
- price: initialData?.price || 0,
- cost: initialData?.cost || 0,
- quantity: initialData?.quantity || 0,
- lowStockThreshold: initialData?.lowStockThreshold || 10,
- location: initialData?.location || "",
- },
- });
-
- // Update form values when initialData changes
- useEffect(() => {
- if (initialData) {
- Object.keys(initialData).forEach((key) => {
- const k = key as keyof InventoryItem;
- if (k in form.getValues()) {
- form.setValue(k as any, initialData[k] as any);
- }
- });
- } else {
- form.reset({
- name: "",
- sku: "",
- description: "",
- categoryId: undefined,
- price: 0,
- cost: 0,
- quantity: 0,
- lowStockThreshold: 10,
- location: "",
- });
- }
- }, [initialData, form]);
-
- // Fetch categories
- const { data: categories } = useQuery({
- queryKey: ["/api/categories"],
- queryFn: async () => {
- const response = await fetch("/api/categories");
- if (!response.ok) {
- throw new Error("Failed to fetch categories");
- }
- return response.json() as Promise;
- },
- });
-
- // Create or update item mutation
- const mutation = useMutation({
- mutationFn: async (data: InventoryItemForm) => {
- if (initialData) {
- // Update existing item
- return apiRequest("PUT", `/api/inventory/${initialData.id}`, data);
- } else {
- // Create new item
- return apiRequest("POST", "/api/inventory", data);
- }
- },
- onSuccess: async () => {
- // Invalidate and refetch
- await queryClient.invalidateQueries({ queryKey: ["/api/inventory"] });
- await queryClient.invalidateQueries({ queryKey: ["/api/inventory/stats"] });
-
- // Close modal and show success toast
- setOpen(false);
- toast({
- title: initialData ? "Item Updated" : "Item Created",
- description: initialData
- ? `${initialData.name} has been updated successfully.`
- : "New inventory item has been created.",
- variant: "default",
- });
- },
- onError: (error) => {
- toast({
- title: "Error",
- description: error.message || "Failed to save item. Please try again.",
- variant: "destructive",
- });
- },
- });
-
- const onSubmit = async (data: InventoryItemForm) => {
- setLoading(true);
- try {
- await mutation.mutateAsync(data);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
- {initialData ? "Edit Inventory Item" : "Add New Inventory Item"}
-
-
- {initialData
- ? "Update the details of the existing inventory item."
- : "Fill in the details below to add a new item to your inventory."}
-
-
-
-
-
- (
-
- Item Name
-
-
-
-
-
- )}
- />
-
- (
-
- SKU
-
-
-
-
- Stock Keeping Unit - unique identifier for this item
-
-
-
- )}
- />
-
- (
-
- Category
-
- field.onChange(Number(value))}
- defaultValue={field.value ? String(field.value) : undefined}
- >
-
-
-
-
-
-
- {categories?.map((category) => (
-
- {category.name}
-
- ))}
-
-
-
-
-
- )}
- />
-
-
-
-
-
- (
-
- Storage Location (Optional)
-
-
-
-
-
- )}
- />
-
- (
-
- Description (Optional)
-
-
-
-
-
- )}
- />
-
-
- setOpen(false)}
- disabled={loading}
- className="cancel-button"
- >
- Cancel
-
-
- {loading ? "Saving..." : initialData ? "Update Item" : "Add Item"}
-
-
-
-
-
-
- );
-}
diff --git a/client/src/components/inventory/real-time-inventory.tsx b/client/src/components/inventory/real-time-inventory.tsx
deleted file mode 100644
index d4752a6a..00000000
--- a/client/src/components/inventory/real-time-inventory.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useWebSocket } from '@/hooks/use-websocket';
-import { Badge } from '@/components/ui/badge';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
-import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { useQuery } from '@tanstack/react-query';
-import { AlertCircle, CheckCircle2, AlertTriangle, ArrowUpDown, RotateCw, Wifi, Info } from 'lucide-react';
-import { Skeleton } from '@/components/ui/skeleton';
-import { useToast } from '@/hooks/use-toast';
-import { Button } from '@/components/ui/button';
-import { isElectronEnvironment } from '@/lib/electron-bridge';
-import { setFeatureFlag } from '@/lib/config';
-
-export function RealTimeInventory() {
- // Fetch warehouses to allow user to choose which ones to monitor
- const { data: warehouses, isLoading: warehousesLoading } = useQuery({
- queryKey: ['/api/warehouses'],
- enabled: true,
- });
-
- // State for selected warehouses
- const [selectedWarehouses, setSelectedWarehouses] = useState([]);
- const [recentUpdates, setRecentUpdates] = useState([]);
- const [lastUpdateTime, setLastUpdateTime] = useState(null);
- const { toast } = useToast();
-
- // Handle inventory updates from WebSocket
- const handleInventoryUpdate = (payload: any) => {
- // Add to recent updates, keeping only the last 10
- setRecentUpdates(prev => {
- const newUpdates = [payload, ...prev].slice(0, 10);
- return newUpdates;
- });
- setLastUpdateTime(new Date().toLocaleTimeString());
- };
-
- // Handle stock alerts from WebSocket
- const handleStockAlert = (payload: any) => {
- const { item, warehouse, currentLevel, reorderThreshold } = payload;
- toast({
- title: `Low Stock Alert: ${item.name}`,
- description: `Current level: ${currentLevel}, Threshold: ${reorderThreshold} in ${warehouse.name || 'warehouse #' + warehouse.warehouseId}`,
- variant: 'destructive'
- });
- };
-
- // Function to enable WebSockets
- const enableWebSockets = () => {
- setFeatureFlag('enableWebSockets', true);
- // Force page refresh to apply feature flag change
- window.location.reload();
- };
-
- // Connect to WebSocket for real-time updates
- const { isConnected, lastMessage, webSocketsEnabled } = useWebSocket({
- warehouses: selectedWarehouses,
- onInventoryUpdate: handleInventoryUpdate,
- onStockAlert: handleStockAlert,
- onConnectionStatus: (connected) => {
- if (connected) {
- toast({
- title: 'Connected',
- description: 'Real-time inventory synchronization is active',
- variant: 'default'
- });
- }
- }
- });
-
- // Handle warehouse selection change
- const handleWarehouseChange = (value: string) => {
- if (value === 'all') {
- // Monitor all warehouses
- setSelectedWarehouses([]);
- } else {
- setSelectedWarehouses([parseInt(value)]);
- }
- };
-
- // Determine connection status display
- let connectionStatus = (
-
-
- Connecting...
-
- );
-
- if (isConnected) {
- connectionStatus = (
-
-
- Connected
-
- );
- } else if (lastMessage) {
- connectionStatus = (
-
-
- Disconnected
-
- );
- }
-
- return (
-
- {!webSocketsEnabled && !isElectronEnvironment() && (
-
-
-
- Real-time inventory updates are currently disabled in development mode.
-
-
- Enable Real-Time Updates
-
-
-
- )}
-
-
-
- Real-Time Inventory Updates
- Live inventory changes across warehouses
-
- {connectionStatus}
-
-
-
-
-
- Monitor:
- {warehousesLoading ? (
-
- ) : (
-
-
-
-
-
- All Warehouses
- {warehouses && Array.isArray(warehouses) ? warehouses.map((warehouse: any) => (
-
- {warehouse.name}
-
- )) : null}
-
-
- )}
-
-
-
- {recentUpdates.length === 0 ? (
-
-
- No recent updates
-
- Inventory changes will appear here in real-time
-
-
- ) : (
-
- {recentUpdates.map((update, index) => (
-
-
-
{update.item.name}
-
- Quantity: {update.currentQuantity} in {update.warehouseName}
-
-
-
- {update.previousQuantity !== undefined && (
-
update.previousQuantity ? "default" : "destructive"}
- className={update.currentQuantity > update.previousQuantity ? "bg-green-100 text-green-800 hover:bg-green-200" : ""}>
-
- {update.currentQuantity > update.previousQuantity
- ? `+${update.currentQuantity - update.previousQuantity}`
- : `${update.currentQuantity - update.previousQuantity}`
- }
-
- )}
-
-
- ))}
-
- )}
-
- {lastUpdateTime && (
-
- Last updated: {lastUpdateTime}
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/inventory/recent-activity.tsx b/client/src/components/inventory/recent-activity.tsx
deleted file mode 100644
index 6f448101..00000000
--- a/client/src/components/inventory/recent-activity.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import { ExternalLink, ShoppingCart, User, Package } from "lucide-react";
-import { Link } from "wouter";
-import { Skeleton } from "@/components/ui/skeleton";
-import { formatRelativeDate } from "@/lib/utils";
-import { ActivityLog } from "@shared/schema";
-
-export default function RecentActivity() {
- const { data: activityLogs, isLoading } = useQuery({
- queryKey: ["/api/activity-logs"],
- queryFn: async () => {
- const response = await fetch("/api/activity-logs?limit=3");
- if (!response.ok) {
- throw new Error("Failed to fetch activity logs");
- }
- return response.json() as Promise;
- },
- });
-
- if (isLoading) {
- return ;
- }
-
- return (
-
-
- Recent Activity
-
-
-
-
- {activityLogs?.map((log, index) => (
-
-
- {index !== activityLogs.length - 1 && (
-
- )}
-
-
-
- {getActivityIcon(log.action)}
-
-
-
-
-
- {log.action}
-
-
- {log.description}
-
-
-
- {formatRelativeDate(log.timestamp)}
-
-
-
-
-
- ))}
-
-
-
-
-
-
- View all activity
-
-
-
-
-
- );
-}
-
-// Helper function to get icon class based on activity type
-function getActivityIconClass(action: string): string {
- if (action.toLowerCase().includes("order")) {
- return "bg-primary/10 dark:bg-primary/20 text-primary";
- } else if (action.toLowerCase().includes("supplier")) {
- return "bg-secondary/10 dark:bg-secondary/20 text-secondary";
- } else {
- return "bg-success/10 dark:bg-success/20 text-success";
- }
-}
-
-// Helper function to get icon based on activity type
-function getActivityIcon(action: string) {
- if (action.toLowerCase().includes("order")) {
- return ;
- } else if (action.toLowerCase().includes("supplier")) {
- return ;
- } else {
- return ;
- }
-}
-
-function ActivitySkeletons() {
- return (
-
-
- Recent Activity
-
-
-
-
- {[1, 2, 3].map((i, index) => (
-
-
- {index !== 2 && (
-
- )}
-
-
-
- ))}
-
-
-
-
-
-
-
- );
-}
diff --git a/client/src/components/inventory/stats-card.tsx b/client/src/components/inventory/stats-card.tsx
deleted file mode 100644
index 16e29848..00000000
--- a/client/src/components/inventory/stats-card.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Card, CardContent, CardFooter } from "@/components/ui/card";
-import { Link } from "wouter";
-import { ExternalLink } from "lucide-react";
-import { Skeleton } from "@/components/ui/skeleton";
-import { cn } from "@/lib/utils";
-
-interface StatsCardProps {
- title: string;
- value: string | number;
- icon: React.ReactNode;
- iconClassName: string;
- link: {
- href: string;
- label: string;
- };
- loading?: boolean;
-}
-
-export function StatsCard({
- title,
- value,
- icon,
- iconClassName,
- link,
- loading = false,
-}: StatsCardProps) {
- return (
-
-
-
-
- {icon}
-
-
-
-
- {title}
-
-
- {loading ? (
-
- ) : (
-
- {value}
-
- )}
-
-
-
-
-
-
-
-
- {link.label}
-
-
-
-
-
- );
-}
diff --git a/client/src/components/inventory/stock-alerts.tsx b/client/src/components/inventory/stock-alerts.tsx
deleted file mode 100644
index 64eb86fd..00000000
--- a/client/src/components/inventory/stock-alerts.tsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
-import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { Link } from "wouter";
-import { AlertTriangle, ExternalLink, RefreshCw } from "lucide-react";
-import { Skeleton } from "@/components/ui/skeleton";
-import { type InventoryItem } from "@shared/schema";
-import { cn } from "@/lib/utils";
-import { useToast } from "@/hooks/use-toast";
-import { apiRequest } from "@/lib/queryClient";
-
-export default function StockAlerts() {
- const { toast } = useToast();
- const queryClient = useQueryClient();
-
- const { data: lowStockItems, isLoading } = useQuery({
- queryKey: ["/api/inventory/low-stock"],
- queryFn: async () => {
- const response = await fetch("/api/inventory/low-stock");
- if (!response.ok) {
- throw new Error("Failed to fetch low stock items");
- }
- return response.json() as Promise;
- },
- });
-
- const { data: outOfStockItems, isLoading: outOfStockLoading } = useQuery({
- queryKey: ["/api/inventory/out-of-stock"],
- queryFn: async () => {
- const response = await fetch("/api/inventory/out-of-stock");
- if (!response.ok) {
- throw new Error("Failed to fetch out of stock items");
- }
- return response.json() as Promise;
- },
- });
-
- // Reorder item mutation
- const reorderMutation = useMutation({
- mutationFn: async (data: { itemId: number; quantity: number }) => {
- // Clean up the data to match the expected schema
- const validData = {
- itemId: data.itemId,
- quantity: data.quantity
- };
-
- const response = await apiRequest("POST", "/api/reorder-requests", validData);
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.message || "Failed to create reorder request");
- }
-
- return response;
- },
- onSuccess: async () => {
- // Invalidate and refetch reorder requests
- await queryClient.invalidateQueries({ queryKey: ["/api/reorder-requests"] });
-
- toast({
- title: "Reorder Request Created",
- description: "A reorder request has been created successfully.",
- });
- },
- onError: (error: any) => {
- console.error("Reorder error:", error);
- toast({
- title: "Error",
- description: error.message || "Failed to create reorder request",
- variant: "destructive",
- });
- },
- });
-
- if (isLoading || outOfStockLoading) {
- return ;
- }
-
- // Combine and take top 3 most critical items
- const criticalItems = [
- ...(outOfStockItems || []),
- ...(lowStockItems || []),
- ].slice(0, 3);
-
- if (criticalItems.length === 0) {
- return (
-
-
- Stock Alerts
-
-
-
-
-
All Items In Stock
-
- No stock alerts at this time
-
-
-
-
- );
- }
-
- return (
-
-
- Stock Alerts
-
-
- {criticalItems.map((item) => (
-
-
-
-
-
{item.name}
-
- {item.quantity === 0 ? (
- Out of stock:
- ) : (
- Low stock:
- )}{" "}
- {item.quantity} remaining
-
-
-
- {
- const defaultQuantity = item.lowStockThreshold || 10;
- reorderMutation.mutate({
- itemId: item.id,
- quantity: defaultQuantity
- });
- }}
- disabled={reorderMutation.isPending}
- >
- {reorderMutation.isPending ? (
-
- ) : (
-
- )}
- Reorder
-
-
-
-
- ))}
-
-
-
-
- View all alerts
-
-
-
-
-
- Manage reorders
-
-
-
-
-
- );
-}
-
-function StockAlertsSkeletons() {
- return (
-
-
- Stock Alerts
-
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
-
-
-
-
-
- );
-}
diff --git a/client/src/components/inventory/stock-movement-form.tsx b/client/src/components/inventory/stock-movement-form.tsx
deleted file mode 100644
index b3912512..00000000
--- a/client/src/components/inventory/stock-movement-form.tsx
+++ /dev/null
@@ -1,424 +0,0 @@
-import { useState, useEffect } from "react";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-import { useQuery } from "@tanstack/react-query";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import { Button } from "@/components/ui/button";
-import { useToast } from "@/hooks/use-toast";
-import { apiRequest, queryClient } from "@/lib/queryClient";
-
-type StockMovementFormProps = {
- open: boolean;
- onClose: () => void;
- type: "RECEIPT" | "ISSUE";
- itemId?: number;
- warehouseId?: number;
-};
-
-type Warehouse = {
- id: number;
- name: string;
-};
-
-type InventoryItem = {
- id: number;
- name: string;
- sku: string;
-};
-
-export function StockMovementForm({ open, onClose, type, itemId, warehouseId }: StockMovementFormProps) {
- const { toast } = useToast();
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- // Define the form schema based on the movement type
- const formSchema = z.object({
- itemId: z.number().positive({ message: "Item is required" }),
- warehouseId: z.number().positive({ message: "Warehouse is required" }),
- quantity: z.coerce.number().positive({ message: "Quantity must be positive" }),
- notes: z.string().nullable().optional(),
- referenceType: z.string().nullable().optional(),
- referenceId: z.string().nullable().optional(),
- unitCost: z.coerce.number().nullable().optional(),
- });
-
- // Create the form
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- itemId: itemId || 0,
- warehouseId: warehouseId || 0,
- quantity: 0,
- notes: "",
- referenceType: null,
- referenceId: null,
- unitCost: null,
- },
- });
-
- // Update form values when props change
- useEffect(() => {
- form.setValue("itemId", itemId || 0);
- form.setValue("warehouseId", warehouseId || 0);
- }, [form, itemId, warehouseId]);
-
- // Fetch warehouses
- const { data: warehouses } = useQuery({
- queryKey: ["/api/warehouses"],
- queryFn: async () => {
- const response = await fetch("/api/warehouses");
- if (!response.ok) {
- throw new Error("Failed to fetch warehouses");
- }
- return response.json() as Promise;
- },
- });
-
- // Fetch inventory items if itemId is not provided
- const { data: items } = useQuery({
- queryKey: ["/api/inventory"],
- queryFn: async () => {
- const response = await fetch("/api/inventory");
- if (!response.ok) {
- throw new Error("Failed to fetch inventory items");
- }
- return response.json() as Promise;
- },
- enabled: !itemId,
- });
-
- // Handle form submission
- const onSubmit = async (data: z.infer) => {
- setIsSubmitting(true);
-
- try {
- // Determine the endpoint based on the movement type
- const endpoint = type === "RECEIPT"
- ? "/api/stock-movements/receipt"
- : "/api/stock-movements/issue";
-
- // Make the API request
- await apiRequest(
- "POST",
- endpoint,
- {
- itemId: data.itemId,
- warehouseId: data.warehouseId,
- quantity: data.quantity,
- notes: data.notes,
- referenceType: data.referenceType,
- referenceId: data.referenceId ? parseInt(data.referenceId) : null,
- unitCost: data.unitCost,
- }
- );
-
- // Show success toast
- toast({
- title: `Stock ${type === "RECEIPT" ? "Receipt" : "Issue"} Recorded`,
- description: `Inventory has been ${type === "RECEIPT" ? "increased" : "decreased"} successfully.`,
- });
-
- // Invalidate queries to refresh data
- queryClient.invalidateQueries({ queryKey: ["/api/stock-movements"] });
- queryClient.invalidateQueries({ queryKey: [`/api/stock-movements/item/${data.itemId}`] });
- queryClient.invalidateQueries({ queryKey: [`/api/stock-movements/warehouse/${data.warehouseId}`] });
- queryClient.invalidateQueries({ queryKey: ["/api/inventory"] });
- queryClient.invalidateQueries({ queryKey: [`/api/inventory/${data.itemId}`] });
- queryClient.invalidateQueries({ queryKey: ["/api/warehouse-inventory"] });
-
- // Close the dialog
- onClose();
- form.reset();
- } catch (error) {
- console.error("Error recording stock movement:", error);
- toast({
- title: "Error",
- description: `Failed to record stock ${type === "RECEIPT" ? "receipt" : "issue"}. Please try again.`,
- variant: "destructive",
- });
- } finally {
- setIsSubmitting(false);
- }
- };
-
- return (
- !open && onClose()}>
-
-
-
- {type === "RECEIPT" ? "Record Stock Receipt" : "Record Stock Issue"}
-
-
- {type === "RECEIPT"
- ? "Add inventory quantities when receiving new stock."
- : "Reduce inventory quantities when items leave the warehouse."}
-
-
-
-
-
- {/* Item selection - only show if itemId is not provided */}
- {!itemId && (
- (
-
- Item
- field.onChange(parseInt(value))}
- value={field.value ? field.value.toString() : ""}
- >
-
-
-
-
-
-
- {items && items.map((item) => (
-
- {item.name} ({item.sku})
-
- ))}
-
-
-
-
- )}
- />
- )}
-
- {/* Warehouse selection - only show if warehouseId is not provided */}
- {!warehouseId && (
- (
-
- Warehouse
- field.onChange(parseInt(value))}
- value={field.value ? field.value.toString() : ""}
- >
-
-
-
-
-
-
- {warehouses && warehouses.map((warehouse) => (
-
- {warehouse.name}
-
- ))}
-
-
-
-
- )}
- />
- )}
-
- {/* Quantity */}
- (
-
- Quantity
-
- field.onChange(e.target.valueAsNumber)}
- />
-
-
- {type === "RECEIPT"
- ? "Number of items to add to inventory"
- : "Number of items to remove from inventory"}
-
-
-
- )}
- />
-
- {/* Unit Cost - only show for RECEIPT */}
- {type === "RECEIPT" && (
- (
-
- Unit Cost
-
-
- field.onChange(
- e.target.value ? e.target.valueAsNumber : null
- )
- }
- />
-
-
- Cost per unit for inventory valuation (optional)
-
-
-
- )}
- />
- )}
-
- {/* Reference Type */}
- (
-
- Reference Type
-
-
-
-
-
-
-
- None
- {type === "RECEIPT" && (
- <>
- Purchase Order
- Customer Return
- Inventory Adjustment
- >
- )}
- {type === "ISSUE" && (
- <>
- Sale
- Supplier Return
- Inventory Adjustment
- Damaged Goods
- Expired Goods
- >
- )}
-
-
-
- Categorize this stock movement (optional)
-
-
-
- )}
- />
-
- {/* Reference ID */}
- (
-
- Reference ID
-
-
-
-
- ID of related record (order number, etc.)
-
-
-
- )}
- />
-
- {/* Notes */}
- (
-
- Notes
-
-
-
-
-
- )}
- />
-
-
-
- Cancel
-
-
- {isSubmitting ? "Processing..." : type === "RECEIPT" ? "Record Receipt" : "Record Issue"}
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/inventory/stock-movements-list.tsx b/client/src/components/inventory/stock-movements-list.tsx
deleted file mode 100644
index c2d3898f..00000000
--- a/client/src/components/inventory/stock-movements-list.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-import { useState } from "react";
-import { useQuery } from "@tanstack/react-query";
-import { format } from "date-fns";
-import { Plus, ArrowUpDown, ArrowRight, Download, FileDown, FileUp } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import {
- Table,
- TableHeader,
- TableRow,
- TableHead,
- TableBody,
- TableCell,
-} from "@/components/ui/table";
-import { Badge } from "@/components/ui/badge";
-import { Skeleton } from "@/components/ui/skeleton";
-import { StockMovementForm } from "./stock-movement-form";
-
-type StockMovement = {
- id: number;
- itemId: number;
- warehouseId: number | null;
- sourceWarehouseId: number | null;
- destinationWarehouseId: number | null;
- type: string;
- quantity: number;
- notes: string | null;
- userId: number | null;
- unitCost: number | null;
- timestamp: string;
- item: {
- id: number;
- name: string;
- sku: string;
- };
- sourceWarehouse?: {
- id: number;
- name: string;
- } | null;
- destinationWarehouse?: {
- id: number;
- name: string;
- } | null;
- warehouse?: {
- id: number;
- name: string;
- } | null;
- user?: {
- id: number;
- name: string;
- } | null;
-};
-
-type Warehouse = {
- id: number;
- name: string;
-};
-
-type StockMovementsListProps = {
- itemId?: number;
- warehouseId?: number;
- limit?: number;
-};
-
-export function StockMovementsList({ itemId, warehouseId, limit }: StockMovementsListProps) {
- const [showReceiptForm, setShowReceiptForm] = useState(false);
- const [showIssueForm, setShowIssueForm] = useState(false);
-
- // Determine API endpoint based on props
- let apiUrl = "/api/stock-movements";
- if (itemId) {
- apiUrl = `/api/stock-movements/item/${itemId}`;
- } else if (warehouseId) {
- apiUrl = `/api/stock-movements/warehouse/${warehouseId}`;
- }
-
- // If limit is provided, add limit parameter
- if (limit) {
- apiUrl += `?limit=${limit}`;
- }
-
- // Fetch stock movements
- const { data: movements, isLoading } = useQuery({
- queryKey: [apiUrl],
- queryFn: async () => {
- const response = await fetch(apiUrl);
- if (!response.ok) {
- throw new Error("Failed to fetch stock movements");
- }
- return response.json() as Promise;
- },
- });
-
- // Get movement type badge color
- const getMovementTypeColor = (type: string) => {
- switch (type) {
- case "RECEIPT":
- return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300";
- case "ISSUE":
- return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300";
- case "TRANSFER":
- return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300";
- case "ADJUSTMENT":
- return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300";
- case "RECOUNT":
- return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300";
- case "PURCHASE":
- return "bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-300";
- case "SALE":
- return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300";
- case "RETURN":
- return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300";
- case "DAMAGE":
- return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300";
- case "EXPIRE":
- return "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300";
- default:
- return "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300";
- }
- };
-
- // Get movement type icon
- const getMovementTypeIcon = (type: string) => {
- switch (type) {
- case "RECEIPT":
- return ;
- case "ISSUE":
- return ;
- case "TRANSFER":
- return ;
- case "ADJUSTMENT":
- return ;
- default:
- return null;
- }
- };
-
- // Format date
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- return format(date, "MMM d, yyyy h:mm a");
- };
-
- // Handle movement type display
- const getMovementDisplay = (movement: StockMovement) => {
- switch (movement.type) {
- case "TRANSFER":
- return (
-
- {movement.sourceWarehouse?.name || "Unknown"}
-
- {movement.destinationWarehouse?.name || "Unknown"}
-
- );
- case "RECEIPT":
- case "ISSUE":
- return movement.warehouse?.name || "General Warehouse";
- default:
- return "β";
- }
- };
-
- if (isLoading) {
- return (
-
-
-
-
-
-
-
- {Array.from({ length: 5 }).map((_, i) => (
-
- ))}
-
-
-
- );
- }
-
- return (
-
-
-
- Stock Movements
-
- Track inventory receipts, issues, and transfers
-
-
-
-
-
setShowReceiptForm(true)}
- >
-
- Receipt
-
-
setShowIssueForm(true)}
- >
-
- Issue
-
-
-
-
-
-
-
-
- Date
- {!itemId && Item }
- Type
- Warehouse
- Quantity
- Notes
-
-
-
- {movements && movements.length > 0 ? (
- movements.map((movement) => (
-
-
- {formatDate(movement.timestamp)}
-
- {!itemId && (
-
- {movement.item?.name || "Unknown Item"}
-
- )}
-
-
- {getMovementTypeIcon(movement.type)}
- {movement.type}
-
-
-
- {getMovementDisplay(movement)}
-
- 0 ? "text-green-600" : "text-red-600"}>
- {movement.quantity > 0 ? "+" : ""}{movement.quantity}
-
-
- {movement.notes || "β"}
-
-
- ))
- ) : (
-
-
- No stock movements found
-
-
- )}
-
-
-
-
- {limit && movements && movements.length >= limit && (
-
-
- View All Movements
-
-
- )}
-
-
- {/* Receipt Form Dialog */}
- setShowReceiptForm(false)}
- type="RECEIPT"
- itemId={itemId}
- warehouseId={warehouseId}
- />
-
- {/* Issue Form Dialog */}
- setShowIssueForm(false)}
- type="ISSUE"
- itemId={itemId}
- warehouseId={warehouseId}
- />
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/layout/desktop-layout.tsx b/client/src/components/layout/desktop-layout.tsx
index 9d14594d..fd1f3022 100644
--- a/client/src/components/layout/desktop-layout.tsx
+++ b/client/src/components/layout/desktop-layout.tsx
@@ -1,7 +1,4 @@
import React, { useState } from 'react';
-import { useElectron } from '../../contexts/electron-provider';
-import { TitleBar } from '../electron';
-import { OfflineModeIndicator } from '../electron/offline-mode-indicator';
import Sidebar from '../sidebar';
import { Menu } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -17,13 +14,10 @@ export const DesktopLayout: React.FC = ({
children,
title
}) => {
- const { isElectron } = useElectron();
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
- {isElectron &&
}
-
{/* Sidebar */}
@@ -45,8 +39,6 @@ export const DesktopLayout: React.FC = ({
- {isElectron &&
}
-
{children}
@@ -59,4 +51,4 @@ export const DesktopLayout: React.FC
= ({
);
-};
\ No newline at end of file
+};
diff --git a/client/src/components/layout/title-bar.tsx b/client/src/components/layout/title-bar.tsx
deleted file mode 100644
index f64b16d9..00000000
--- a/client/src/components/layout/title-bar.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-import { Minus, Square, X } from 'lucide-react';
-import { useElectron } from '@/contexts/electron-provider';
-
-export function TitleBar() {
- const { isElectron, isMaximized, toggleMaximize, minimizeWindow, closeWindow } = useElectron();
-
- // Only show the custom title bar in Electron environment
- if (!isElectron) return null;
-
- return (
-
-
InvTrack - Inventory Management System
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/real-time-updates.tsx b/client/src/components/real-time-updates.tsx
deleted file mode 100644
index 9f8f7e32..00000000
--- a/client/src/components/real-time-updates.tsx
+++ /dev/null
@@ -1,331 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useWebSocket, WebSocketMessage } from '@/hooks/use-websocket';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
-import { Button } from '@/components/ui/button';
-import { ScrollArea } from '@/components/ui/scroll-area';
-import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
-import { useToast } from '@/hooks/use-toast';
-import { Activity, AlertTriangle, BarChart2, Info, PackageOpen, RefreshCw, Zap, Wifi } from 'lucide-react';
-import { isElectronEnvironment } from '@/lib/electron-bridge';
-import { setFeatureFlag } from '@/lib/config';
-
-type UpdateItem = {
- id: string;
- type: 'inventory_update' | 'stock_alert' | 'stock_transfer';
- title: string;
- description: string;
- timestamp: Date;
- details?: any;
-};
-
-export function RealTimeUpdates() {
- const [isListening, setIsListening] = useState(true);
- const [updates, setUpdates] = useState([]);
- const [alerts, setAlerts] = useState([]);
- const [activeTab, setActiveTab] = useState('updates');
- const [connectionStatus, setConnectionStatus] = useState<'connected' | 'disconnected' | 'connecting'>('connecting');
- const { toast } = useToast();
-
- // Handle inventory updates
- const handleInventoryUpdate = (payload: any) => {
- const newUpdate: UpdateItem = {
- id: `inv-${Date.now()}`,
- type: 'inventory_update',
- title: `${payload.item?.name || 'Item'} Updated`,
- description: `Quantity is now ${payload.quantity} in ${payload.warehouse?.name || 'warehouse #' + payload.warehouseId}`,
- timestamp: new Date(),
- details: payload
- };
-
- setUpdates(prev => [newUpdate, ...prev].slice(0, 50)); // Keep last 50 updates
- };
-
- // Handle stock alerts
- const handleStockAlert = (payload: any) => {
- const newAlert: UpdateItem = {
- id: `alert-${Date.now()}`,
- type: 'stock_alert',
- title: `Low Stock: ${payload.item?.name || 'Item'}`,
- description: `Current quantity (${payload.currentQuantity}) is below threshold (${payload.threshold})`,
- timestamp: new Date(),
- details: payload
- };
-
- setAlerts(prev => [newAlert, ...prev].slice(0, 50)); // Keep last 50 alerts
-
- toast({
- title: 'Low Stock Alert',
- description: `${payload.item?.name || 'An item'} is running low on stock.`,
- variant: 'destructive',
- });
- };
-
- // Handle stock transfers
- const handleStockTransfer = (payload: any) => {
- const newUpdate: UpdateItem = {
- id: `transfer-${Date.now()}`,
- type: 'stock_transfer',
- title: `Stock Transfer: ${payload.item?.name || 'Item'}`,
- description: `${payload.quantity} units transferred from ${payload.sourceWarehouse?.name || 'warehouse #' + payload.sourceWarehouseId} to ${payload.destinationWarehouse?.name || 'warehouse #' + payload.destinationWarehouseId}`,
- timestamp: new Date(),
- details: payload
- };
-
- setUpdates(prev => [newUpdate, ...prev].slice(0, 50)); // Keep last 50 updates
- };
-
- // Handle connection status changes
- const handleConnectionStatus = (connected: boolean) => {
- setConnectionStatus(connected ? 'connected' : 'disconnected');
-
- if (connected) {
- toast({
- title: 'Real-Time Connected',
- description: 'You are now receiving live inventory updates',
- variant: 'default',
- });
- }
- };
-
- // Function to enable WebSockets
- const enableWebSockets = () => {
- setFeatureFlag('enableWebSockets', true);
- // Force page refresh to apply feature flag change
- window.location.reload();
- };
-
- // Connect to WebSocket
- const { isConnected, sendMessage, connect, disconnect, webSocketsEnabled } = useWebSocket({
- warehouses: [], // Subscribe to all warehouses
- onInventoryUpdate: isListening ? handleInventoryUpdate : undefined,
- onStockAlert: isListening ? handleStockAlert : undefined,
- onStockTransfer: isListening ? handleStockTransfer : undefined,
- onConnectionStatus: handleConnectionStatus,
- });
-
- // Toggle listening on/off
- const toggleListening = () => {
- setIsListening(!isListening);
- if (!isListening) {
- toast({
- title: 'Real-Time Updates Enabled',
- description: 'You will now receive inventory notifications',
- });
- } else {
- toast({
- title: 'Real-Time Updates Paused',
- description: 'You will not receive inventory notifications',
- });
- }
- };
-
- // Reconnect manually
- const handleReconnect = () => {
- if (!isConnected) {
- setConnectionStatus('connecting');
- connect();
- }
- };
-
- // Clear all updates
- const clearUpdates = () => {
- if (activeTab === 'updates') {
- setUpdates([]);
- } else {
- setAlerts([]);
- }
- };
-
- return (
-
- {!webSocketsEnabled && !isElectronEnvironment() && (
-
-
-
- Real-time activity updates are currently disabled in development mode.
-
-
- Enable Real-Time Activity
-
-
-
- )}
-
-
-
-
-
- Real-Time Activity
-
-
- Live inventory updates and alerts
-
-
-
-
- {connectionStatus === 'connected' && }
- {connectionStatus === 'connecting' && }
- {connectionStatus === 'disconnected' && }
- {connectionStatus === 'connected' ? 'Live' : connectionStatus === 'connecting' ? 'Connecting...' : 'Disconnected'}
-
-
- {isListening ? 'Pause' : 'Resume'}
-
-
-
-
-
-
-
-
-
-
- Updates
- {updates.length > 0 && (
- {updates.length}
- )}
-
-
-
- Alerts
- {alerts.length > 0 && (
- {alerts.length}
- )}
-
-
-
-
-
-
- {updates.length === 0 ? (
-
-
-
No Updates Yet
-
- Real-time inventory updates will appear here as they happen
-
-
- ) : (
-
-
- {updates.map(update => (
-
- ))}
-
-
- )}
-
-
-
- {alerts.length === 0 ? (
-
-
-
No Alerts
-
- Stock alerts will appear here when items fall below thresholds
-
-
- ) : (
-
-
- {alerts.map(alert => (
-
-
-
{alert.title}
-
- {alert.timestamp.toLocaleTimeString()}
-
-
-
- {alert.description}
-
-
- ))}
-
-
- )}
-
-
-
-
-
-
- Clear {activeTab}
-
-
- {!isConnected && (
-
-
- Reconnect
-
- )}
-
-
- );
-}
-
-// Individual update card component
-function UpdateCard({ update }: { update: UpdateItem }) {
- // Determine icon based on update type
- const getIcon = () => {
- switch (update.type) {
- case 'inventory_update':
- return ;
- case 'stock_transfer':
- return ;
- case 'stock_alert':
- return ;
- default:
- return ;
- }
- };
-
- return (
-
-
-
-
- {getIcon()}
-
-
-
{update.title}
-
{update.description}
-
-
-
- {update.timestamp.toLocaleTimeString()}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/reports/report-filters.tsx b/client/src/components/reports/report-filters.tsx
deleted file mode 100644
index a979923e..00000000
--- a/client/src/components/reports/report-filters.tsx
+++ /dev/null
@@ -1,402 +0,0 @@
-import { useState, useEffect } from "react";
-import { Button } from "@/components/ui/button";
-import { Calendar } from "@/components/ui/calendar";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
-import { Badge } from "@/components/ui/badge";
-import { CalendarIcon } from "lucide-react";
-import { cn } from "@/lib/utils";
-import { format, addDays, startOfMonth, endOfMonth, startOfYear, endOfYear } from "date-fns";
-import { type ReportFilter, type Category, type Warehouse, type Supplier, type ReportType } from "@shared/schema";
-
-interface ReportFiltersProps {
- filter: ReportFilter;
- setFilter: (filter: ReportFilter) => void;
- categories?: Category[];
- warehouses?: Warehouse[];
- suppliers?: Supplier[];
- reportType: ReportType;
-}
-
-export function ReportFilters({
- filter,
- setFilter,
- categories,
- warehouses,
- suppliers,
- reportType
-}: ReportFiltersProps) {
- const [dateRange, setDateRange] = useState<{
- from: Date | undefined;
- to: Date | undefined;
- }>({
- from: filter.startDate,
- to: filter.endDate
- });
-
- // Update parent filter when date range changes
- useEffect(() => {
- if (dateRange.from && dateRange.to) {
- setFilter({
- ...filter,
- startDate: dateRange.from,
- endDate: dateRange.to,
- });
- }
- }, [dateRange]);
-
- // Update date range control when filter changes externally
- useEffect(() => {
- setDateRange({
- from: filter.startDate,
- to: filter.endDate
- });
- }, [filter.startDate, filter.endDate]);
-
- // Helper to update date range
- const updateDateRange = (range: { from: Date | undefined; to: Date | undefined }) => {
- setDateRange(range);
- };
-
- // Date range presets
- const dateRangePresets = [
- {
- label: "Last 7 days",
- onClick: () => updateDateRange({
- from: addDays(new Date(), -7),
- to: new Date()
- })
- },
- {
- label: "Last 30 days",
- onClick: () => updateDateRange({
- from: addDays(new Date(), -30),
- to: new Date()
- })
- },
- {
- label: "This month",
- onClick: () => updateDateRange({
- from: startOfMonth(new Date()),
- to: endOfMonth(new Date())
- })
- },
- {
- label: "This year",
- onClick: () => updateDateRange({
- from: startOfYear(new Date()),
- to: endOfYear(new Date())
- })
- },
- ];
-
- // Clear all filters
- const clearFilters = () => {
- setFilter({});
- setDateRange({ from: undefined, to: undefined });
- };
-
- return (
-
-
-
Report Filters
- {(filter.startDate || filter.categoryId || filter.warehouseId || filter.supplierId || filter.status || (filter.tags && filter.tags.length > 0)) && (
-
- Clear all
-
- )}
-
-
-
- {/* Date range filter */}
-
-
Date Range
-
-
-
-
- {dateRange.from ? (
- dateRange.to ? (
- <>
- {format(dateRange.from, "LLL dd, y")} - {format(dateRange.to, "LLL dd, y")}
- >
- ) : (
- format(dateRange.from, "LLL dd, y")
- )
- ) : (
- "Select date range"
- )}
-
-
-
-
-
- {dateRangePresets.map((preset, index) => (
-
- {preset.label}
-
- ))}
-
-
- {
- if (range) {
- updateDateRange({
- from: range.from,
- to: range.to || range.from // Make sure "to" is always defined when "from" is present
- });
- } else {
- updateDateRange({ from: undefined, to: undefined });
- }
- }}
- numberOfMonths={2}
- defaultMonth={dateRange.from}
- />
-
-
-
-
- {/* Category filter - for inventory reports */}
- {(reportType === "inventory" || reportType === "low-stock" || reportType === "value") && categories && categories.length > 0 && (
-
-
Category
-
{
- setFilter({
- ...filter,
- categoryId: value ? parseInt(value) : undefined
- });
- }}
- >
-
-
-
-
- All categories
- {categories.map((category) => (
-
- {category.name}
-
- ))}
-
-
-
- )}
-
- {/* Warehouse filter - for inventory and reorder reports */}
- {(reportType === "inventory" || reportType === "reorder-requests") && warehouses && warehouses.length > 0 && (
-
-
Warehouse
-
{
- setFilter({
- ...filter,
- warehouseId: value ? parseInt(value) : undefined
- });
- }}
- >
-
-
-
-
- All warehouses
- {warehouses.map((warehouse) => (
-
- {warehouse.name}
-
- ))}
-
-
-
- )}
-
- {/* Supplier filter - for orders, requisitions, and reorder requests */}
- {(reportType === "purchase-orders" || reportType === "purchase-requisitions" || reportType === "reorder-requests") &&
- suppliers && suppliers.length > 0 && (
-
-
Supplier
-
{
- setFilter({
- ...filter,
- supplierId: value ? parseInt(value) : undefined
- });
- }}
- >
-
-
-
-
- All suppliers
- {suppliers.map((supplier) => (
-
- {supplier.name}
-
- ))}
-
-
-
- )}
-
- {/* Status filter - for orders, requisitions, and reorder requests */}
- {(reportType === "purchase-orders" || reportType === "purchase-requisitions" || reportType === "reorder-requests") && (
-
-
Status
-
{
- setFilter({
- ...filter,
- status: value || undefined
- });
- }}
- >
-
-
-
-
- All statuses
- {reportType === "purchase-orders" && (
- <>
- Draft
- Sent
- Acknowledged
- Partially Received
- Received
- Completed
- Cancelled
- >
- )}
- {reportType === "purchase-requisitions" && (
- <>
- Draft
- Pending
- Approved
- Rejected
- Converted
- >
- )}
- {reportType === "reorder-requests" && (
- <>
- Pending
- Approved
- Rejected
- Converted
- >
- )}
-
-
-
- )}
-
-
- {/* Active filters display */}
- {(filter.startDate || filter.categoryId || filter.warehouseId || filter.supplierId || filter.status || (filter.tags && filter.tags.length > 0)) && (
-
- {filter.startDate && filter.endDate && (
-
- Date: {format(filter.startDate, "MMM d")} - {format(filter.endDate, "MMM d, yyyy")}
- setFilter({ ...filter, startDate: undefined, endDate: undefined })}
- >
- Γ
-
-
- )}
-
- {filter.categoryId && categories && (
-
- Category: {categories.find(c => c.id === filter.categoryId)?.name}
- setFilter({ ...filter, categoryId: undefined })}
- >
- Γ
-
-
- )}
-
- {filter.warehouseId && warehouses && (
-
- Warehouse: {warehouses.find(w => w.id === filter.warehouseId)?.name}
- setFilter({ ...filter, warehouseId: undefined })}
- >
- Γ
-
-
- )}
-
- {filter.supplierId && suppliers && (
-
- Supplier: {suppliers.find(s => s.id === filter.supplierId)?.name}
- setFilter({ ...filter, supplierId: undefined })}
- >
- Γ
-
-
- )}
-
- {filter.status && (
-
- Status: {filter.status}
- setFilter({ ...filter, status: undefined })}
- >
- Γ
-
-
- )}
-
- {filter.tags && filter.tags.length > 0 && filter.tags.map(tag => (
-
- Tag: {tag}
- setFilter({
- ...filter,
- tags: filter.tags?.filter(t => t !== tag)
- })}
- >
- Γ
-
-
- ))}
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/billing-settings-form.tsx b/client/src/components/settings/billing-settings-form.tsx
deleted file mode 100644
index 5f77ab72..00000000
--- a/client/src/components/settings/billing-settings-form.tsx
+++ /dev/null
@@ -1,1089 +0,0 @@
-import { useState } from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { useMutation, useQuery } from "@tanstack/react-query";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage
-} from "@/components/ui/form";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle
-} from "@/components/ui/card";
-import {
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger
-} from "@/components/ui/tabs";
-import { Switch } from "@/components/ui/switch";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue
-} from "@/components/ui/select";
-import { Textarea } from "@/components/ui/textarea";
-import { getQueryFn, apiRequest, queryClient } from "@/lib/queryClient";
-import { useToast } from "@/hooks/use-toast";
-import {
- AlertCircle,
- CreditCard,
- DollarSign,
- Receipt,
- FileText,
- Save,
- AlertTriangle,
- ArrowRight,
- Globe,
- Lock,
- Wallet
-} from "lucide-react";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "@/components/ui/accordion";
-import { Badge } from "@/components/ui/badge";
-import { Separator } from "@/components/ui/separator";
-
-// Validation schema for billing settings
-const billingSettingsSchema = z.object({
- // Payment processor settings
- paymentProcessorEnabled: z.boolean().default(false),
- paymentProcessor: z.string().optional(),
- stripePublicKey: z.string().optional(),
- stripeSecretKey: z.string().optional(),
- paypalClientId: z.string().optional(),
- paypalClientSecret: z.string().optional(),
-
- // Invoice settings
- invoicePrefix: z.string().max(5).optional(),
- invoiceNumberFormat: z.string().optional(),
- defaultDueDays: z.number().int().min(0).max(90).default(30),
- defaultTerms: z.string().optional(),
- defaultNotes: z.string().optional(),
- companyInfo: z.string().optional(),
-
- // Payment settings
- allowedPaymentMethods: z.array(z.string()).default(["CASH", "CREDIT_CARD", "BANK_TRANSFER"]),
- allowPartialPayments: z.boolean().default(true),
- autoSendReceipts: z.boolean().default(true),
- requirePaymentReference: z.boolean().default(false),
-
- // Email settings
- emailNotificationsEnabled: z.boolean().default(true),
- invoiceEmailSubject: z.string().optional(),
- invoiceEmailTemplate: z.string().optional(),
- receiptEmailSubject: z.string().optional(),
- receiptEmailTemplate: z.string().optional(),
- reminderEnabled: z.boolean().default(true),
- firstReminderDays: z.number().int().min(1).max(30).default(3),
- secondReminderDays: z.number().int().min(1).max(60).default(7),
- thirdReminderDays: z.number().int().min(1).max(90).default(14),
-});
-
-type BillingSettingsValues = z.infer;
-
-export function BillingSettingsForm() {
- const { toast } = useToast();
- const [activeTab, setActiveTab] = useState("payment-processors");
-
- // Fetch settings
- const {
- data: settings,
- isLoading: isLoadingSettings,
- } = useQuery({
- queryKey: ["/api/settings/billing"],
- queryFn: getQueryFn({ on401: "throw" }),
- enabled: false, // Disabled until API endpoint is ready
- });
-
- // Setup form with default values
- const form = useForm({
- resolver: zodResolver(billingSettingsSchema),
- defaultValues: {
- // Payment processor settings
- paymentProcessorEnabled: false,
- paymentProcessor: "stripe",
- stripePublicKey: "",
- stripeSecretKey: "",
- paypalClientId: "",
- paypalClientSecret: "",
-
- // Invoice settings
- invoicePrefix: "INV",
- invoiceNumberFormat: "{PREFIX}-{YEAR}{MONTH}{NUMBER}",
- defaultDueDays: 30,
- defaultTerms: "Payment is due within 30 days of invoice date.",
- defaultNotes: "Thank you for your business!",
- companyInfo: "Your Company Name\nAddress Line 1\nCity, State, Zip\nPhone: (123) 456-7890\nEmail: billing@example.com",
-
- // Payment settings
- allowedPaymentMethods: ["CASH", "CREDIT_CARD", "BANK_TRANSFER", "CHECK"],
- allowPartialPayments: true,
- autoSendReceipts: true,
- requirePaymentReference: false,
-
- // Email settings
- emailNotificationsEnabled: true,
- invoiceEmailSubject: "Invoice #{INVOICE_NUMBER} from {COMPANY_NAME}",
- invoiceEmailTemplate: "Dear {CUSTOMER_NAME},\n\nPlease find attached invoice #{INVOICE_NUMBER} in the amount of {AMOUNT}.\n\nThank you for your business!\n\n{COMPANY_NAME}",
- receiptEmailSubject: "Payment Receipt for Invoice #{INVOICE_NUMBER}",
- receiptEmailTemplate: "Dear {CUSTOMER_NAME},\n\nThank you for your payment of {AMOUNT} for invoice #{INVOICE_NUMBER}.\n\nPlease find attached your receipt.\n\n{COMPANY_NAME}",
- reminderEnabled: true,
- firstReminderDays: 3,
- secondReminderDays: 7,
- thirdReminderDays: 14,
- },
- // Merge with fetched settings when available
- values: settings || undefined,
- });
-
- // Update payment processor fields when payment processor changes
- const selectedPaymentProcessor = form.watch("paymentProcessor");
- const paymentProcessorEnabled = form.watch("paymentProcessorEnabled");
-
- // Save settings mutation
- const saveSettingsMutation = useMutation({
- mutationFn: async (data: BillingSettingsValues) => {
- const res = await apiRequest("POST", "/api/settings/billing", data);
- if (!res.ok) throw new Error("Failed to save settings");
- return data;
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["/api/settings/billing"] });
- toast({
- title: "Settings saved",
- description: "Billing settings have been updated successfully.",
- });
- },
- onError: (error) => {
- toast({
- title: "Failed to save settings",
- description: error.message,
- variant: "destructive",
- });
- },
- });
-
- // Form submission
- const onSubmit = (data: BillingSettingsValues) => {
- saveSettingsMutation.mutate(data);
- };
-
- // Check for Stripe keys in environment
- const hasStripePublicKey = Boolean(import.meta.env.VITE_STRIPE_PUBLIC_KEY);
- const hasStripeSecretKey = false; // This would be checked on the server
-
- return (
-
-
-
Billing Settings
-
- Configure how invoices, payments, and billing notifications work in your system.
-
-
-
-
-
-
-
- Payment Processors
-
-
-
- Invoice Settings
-
-
-
- Payment Settings
-
-
-
- Notifications
-
-
-
-
-
-
-
-
-
-
- Payment Processors
-
-
- Configure payment processors to accept online payments.
-
-
-
- (
-
-
- Enable Payment Processor
-
- Allow customers to pay invoices online.
-
-
-
-
-
-
- )}
- />
-
- {paymentProcessorEnabled && (
- <>
- (
-
- Payment Processor
-
-
-
-
-
-
-
-
- Stripe {hasStripePublicKey && Configured }
-
- PayPal
- Other (Manual Configuration)
-
-
-
- Select the payment processor you want to use.
-
-
-
- )}
- />
-
- {selectedPaymentProcessor === "stripe" && (
-
-
-
- Stripe Integration
-
- To use Stripe, you need to add your API keys to the environment variables.
- The public key will be used by the client to create payment forms, while the
- secret key will be used by the server to process payments.
-
-
-
-
-
-
-
- Important Security Note
-
- The Stripe secret key should ideally be added to your server environment
- variables and not stored in these settings. This field is provided for
- development and testing purposes only.
-
-
-
-
-
{
- window.open('https://dashboard.stripe.com/apikeys', '_blank');
- }}
- >
- Open Stripe Dashboard
-
-
-
-
- )}
-
- {selectedPaymentProcessor === "paypal" && (
-
-
-
- PayPal Integration
-
- To use PayPal, you need to add your API credentials to the environment variables.
-
-
-
-
-
-
-
- Important Security Note
-
- The PayPal client secret should ideally be added to your server environment
- variables and not stored in these settings. This field is provided for
- development and testing purposes only.
-
-
-
-
-
{
- window.open('https://developer.paypal.com/developer/applications/', '_blank');
- }}
- >
- Open PayPal Developer Dashboard
-
-
-
-
- )}
- >
- )}
-
-
-
-
-
-
-
-
-
- Invoice Settings
-
-
- Configure invoice settings, including numbering, default terms, and company information.
-
-
-
-
- (
-
- Invoice Prefix
-
-
-
-
- Short prefix for invoice numbers (e.g., INV, BILL).
-
-
-
- )}
- />
-
- (
-
- Invoice Number Format
-
-
-
-
- Format for invoice numbers. Available tags: {"{PREFIX}"}, {"{YEAR}"}, {"{MONTH}"}, {"{DAY}"}, {"{NUMBER}"}
-
-
-
- )}
- />
-
-
- (
-
- Default Payment Terms (days)
-
- field.onChange(Number(e.target.value))}
- value={field.value || 30}
- />
-
-
- Default number of days until payment is due.
-
-
-
- )}
- />
-
- (
-
- Default Terms & Conditions
-
-
-
-
- Default terms and conditions for all invoices.
-
-
-
- )}
- />
-
- (
-
- Default Notes
-
-
-
-
- Default notes to include on all invoices.
-
-
-
- )}
- />
-
- (
-
- Company Information
-
-
-
-
- Company information to include on all invoices.
-
-
-
- )}
- />
-
-
-
-
-
-
-
-
-
- Payment Settings
-
-
- Configure payment methods and other payment-related settings.
-
-
-
-
-
(
-
-
- Allow Partial Payments
-
- Allow customers to pay a portion of an invoice.
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Automatically Send Receipts
-
- Send receipts automatically when payments are recorded.
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Require Payment Reference
-
- Require a reference number for all payments.
-
-
-
-
-
-
- )}
- />
-
-
-
-
- (
-
- Allowed Payment Methods
-
- Select the payment methods that you want to accept.
-
-
-
-
- {
- const newValue = e.target.checked
- ? [...(field.value || []), "CASH"]
- : (field.value || []).filter((v) => v !== "CASH");
- field.onChange(newValue);
- }}
- />
-
- Cash
-
-
-
-
- {
- const newValue = e.target.checked
- ? [...(field.value || []), "CREDIT_CARD"]
- : (field.value || []).filter((v) => v !== "CREDIT_CARD");
- field.onChange(newValue);
- }}
- />
-
- Credit Card
-
-
-
-
- {
- const newValue = e.target.checked
- ? [...(field.value || []), "DEBIT_CARD"]
- : (field.value || []).filter((v) => v !== "DEBIT_CARD");
- field.onChange(newValue);
- }}
- />
-
- Debit Card
-
-
-
-
- {
- const newValue = e.target.checked
- ? [...(field.value || []), "BANK_TRANSFER"]
- : (field.value || []).filter((v) => v !== "BANK_TRANSFER");
- field.onChange(newValue);
- }}
- />
-
- Bank Transfer
-
-
-
-
- {
- const newValue = e.target.checked
- ? [...(field.value || []), "CHECK"]
- : (field.value || []).filter((v) => v !== "CHECK");
- field.onChange(newValue);
- }}
- />
-
- Check
-
-
-
-
- {
- const newValue = e.target.checked
- ? [...(field.value || []), "PAYPAL"]
- : (field.value || []).filter((v) => v !== "PAYPAL");
- field.onChange(newValue);
- }}
- />
-
- PayPal
-
-
-
-
- {
- const newValue = e.target.checked
- ? [...(field.value || []), "OTHER"]
- : (field.value || []).filter((v) => v !== "OTHER");
- field.onChange(newValue);
- }}
- />
-
- Other
-
-
-
-
- )}
- />
-
-
-
-
-
-
-
-
-
- Notification Settings
-
-
- Configure email notifications and reminders for invoices and payments.
-
-
-
- (
-
-
- Enable Email Notifications
-
- Send email notifications for invoices and payments.
-
-
-
-
-
-
- )}
- />
-
- {form.watch("emailNotificationsEnabled") && (
- <>
-
-
- Invoice Email Templates
-
- (
-
- Invoice Email Subject
-
-
-
-
- Subject line for invoice emails. Available tags: {"{INVOICE_NUMBER}"}, {"{COMPANY_NAME}"}, {"{CUSTOMER_NAME}"}, {"{AMOUNT}"}, {"{DUE_DATE}"}
-
-
-
- )}
- />
-
- (
-
- Invoice Email Template
-
-
-
-
- Email template for invoices. Available tags: {"{INVOICE_NUMBER}"}, {"{COMPANY_NAME}"}, {"{CUSTOMER_NAME}"}, {"{AMOUNT}"}, {"{DUE_DATE}"}, {"{TERMS}"}
-
-
-
- )}
- />
-
-
-
-
- Receipt Email Templates
-
- (
-
- Receipt Email Subject
-
-
-
-
- Subject line for receipt emails. Available tags: {"{INVOICE_NUMBER}"}, {"{COMPANY_NAME}"}, {"{CUSTOMER_NAME}"}, {"{AMOUNT}"}, {"{PAYMENT_DATE}"}, {"{PAYMENT_METHOD}"}
-
-
-
- )}
- />
-
- (
-
- Receipt Email Template
-
-
-
-
- Email template for receipts. Available tags: {"{INVOICE_NUMBER}"}, {"{COMPANY_NAME}"}, {"{CUSTOMER_NAME}"}, {"{AMOUNT}"}, {"{PAYMENT_DATE}"}, {"{PAYMENT_METHOD}"}
-
-
-
- )}
- />
-
-
-
-
- (
-
-
- Enable Payment Reminders
-
- Send automated reminders for overdue invoices.
-
-
-
-
-
-
- )}
- />
-
- {form.watch("reminderEnabled") && (
-
- (
-
- First Reminder (days after due date)
-
- field.onChange(Number(e.target.value))}
- value={field.value || 3}
- />
-
-
-
- )}
- />
-
- (
-
- Second Reminder (days after due date)
-
- field.onChange(Number(e.target.value))}
- value={field.value || 7}
- />
-
-
-
- )}
- />
-
- (
-
- Third Reminder (days after due date)
-
- field.onChange(Number(e.target.value))}
- value={field.value || 14}
- />
-
-
-
- )}
- />
-
- )}
- >
- )}
-
-
-
-
-
-
-
- {saveSettingsMutation.isPending ? "Saving..." : "Save Settings"}
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/database-settings-form.tsx b/client/src/components/settings/database-settings-form.tsx
deleted file mode 100644
index ca8c4fea..00000000
--- a/client/src/components/settings/database-settings-form.tsx
+++ /dev/null
@@ -1,614 +0,0 @@
-import React, { useState, useEffect, useMemo, useCallback } from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import * as z from "zod";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { Switch } from "@/components/ui/switch";
-import { Loader2, Database, RotateCw, CheckCircle, XCircle, CloudCog, Download, Upload } from "lucide-react";
-import { useToast } from "@/hooks/use-toast";
-import { useSettings, DatabaseSettings } from "@/hooks/use-settings";
-import { isElectronEnvironment, ElectronBridge, DatabaseInfo, BackupResult } from "@/lib/electron-bridge";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-
-// Define schema for synchronization settings
-const syncSettingsSchema = z.object({
- useLocalDB: z.boolean().default(true),
- autoConnect: z.boolean().default(true),
- syncInterval: z.coerce.number().int().min(1, "Min sync interval is 1 minute").max(1440, "Max sync interval is 24 hours (1440 minutes)").optional(),
- offlineMode: z.boolean().default(false).optional(),
- syncOnStartup: z.boolean().default(true).optional(),
- maxOfflineDays: z.coerce.number().int().min(1, "Min is 1 day").max(30, "Max is 30 days").optional(),
- compressionEnabled: z.boolean().default(true).optional(),
-});
-
-// Define schema for connection settings
-const connectionSettingsSchema = z.object({
- host: z.string().min(1, "Host is required").optional(),
- port: z.string().min(1, "Port is required").optional(),
- username: z.string().min(1, "Username is required").optional(),
- password: z.string().optional(),
- database: z.string().min(1, "Database name is required").optional(),
-});
-
-// Combined database settings schema
-const databaseSettingsSchema = z.object({
- ...syncSettingsSchema.shape,
- ...connectionSettingsSchema.shape
-});
-
-// Type for form data
-type DatabaseSettingsFormType = z.infer;
-
-export function DatabaseSettingsForm() {
- const { toast } = useToast();
- const [isElectron, setIsElectron] = useState(isElectronEnvironment());
- const [dbStatus, setDbStatus] = useState<'checking' | 'connected' | 'disconnected'>('checking');
- const { settings, updateSettings } = useSettings();
- const electronBridge = useMemo(() => new ElectronBridge(), []);
- const [activeTab, setActiveTab] = useState("sync");
-
- // Create form with default values
- const form = useForm({
- resolver: zodResolver(databaseSettingsSchema),
- defaultValues: {
- // Sync settings defaults
- useLocalDB: true,
- autoConnect: true,
- syncInterval: 15,
- offlineMode: false,
- syncOnStartup: true,
- maxOfflineDays: 7,
- compressionEnabled: true,
-
- // Connection settings defaults
- host: "",
- port: "5432",
- username: "",
- password: "",
- database: "",
- },
- });
-
- // Update form with settings if available
- useEffect(() => {
- if (settings && settings.databaseSettings) {
- const dbSettings = settings.databaseSettings as DatabaseSettings;
- form.reset({
- // Sync settings
- useLocalDB: dbSettings.useLocalDB ?? true,
- autoConnect: dbSettings.autoConnect ?? true,
- syncInterval: dbSettings.syncInterval ?? 15,
- offlineMode: dbSettings.offlineMode ?? false,
- syncOnStartup: dbSettings.syncOnStartup ?? true,
- maxOfflineDays: dbSettings.maxOfflineDays ?? 7,
- compressionEnabled: dbSettings.compressionEnabled ?? true,
-
- // Connection settings
- host: dbSettings.host ?? "",
- port: dbSettings.port ?? "5432",
- username: dbSettings.username ?? "",
- password: dbSettings.password ?? "",
- database: dbSettings.database ?? "",
- });
- }
- }, [settings, form]);
-
- // Check database connection
- const checkDatabaseConnection = useCallback(async () => {
- setDbStatus('checking');
-
- if (isElectron) {
- try {
- // Use the ElectronBridge to check database status
- const dbInfo = await electronBridge.getDatabaseInfo().catch(() => null);
-
- if (dbInfo && dbInfo.status === 'healthy') {
- setDbStatus('connected');
- toast({
- title: "Database Connected",
- description: `Successfully connected to ${dbInfo.database || 'database'}`,
- });
- } else {
- setDbStatus('disconnected');
- toast({
- title: "Database Disconnected",
- description: dbInfo?.error || "Unable to connect to database",
- variant: "destructive",
- });
- }
- } catch (error) {
- console.error('Error checking database connection:', error);
- setDbStatus('disconnected');
- toast({
- title: "Connection Error",
- description: error instanceof Error ? error.message : "Failed to check database connection",
- variant: "destructive",
- });
- }
- } else {
- // If not in Electron, always show as disconnected
- setDbStatus('disconnected');
- toast({
- title: "Not Available",
- description: "Database connections are only available in the desktop application",
- variant: "destructive",
- });
- }
- }, [isElectron, electronBridge, setDbStatus, toast]);
-
- // Manual database synchronization function
- const [isSyncing, setIsSyncing] = useState(false);
-
- const handleManualSync = useCallback(async () => {
- if (!isElectron) {
- toast({
- title: "Not Available",
- description: "Database synchronization is only available in the desktop application",
- variant: "destructive",
- });
- return;
- }
-
- setIsSyncing(true);
-
- try {
- await electronBridge.syncDatabase();
- toast({
- title: "Synchronization Complete",
- description: "Database has been successfully synchronized with the server",
- });
-
- // Re-check the connection after sync
- await checkDatabaseConnection();
- } catch (error) {
- console.error('Error synchronizing database:', error);
- toast({
- title: "Synchronization Failed",
- description: error instanceof Error ? error.message : "Failed to synchronize database",
- variant: "destructive",
- });
- } finally {
- setIsSyncing(false);
- }
- }, [isElectron, electronBridge, toast, checkDatabaseConnection]);
-
- // Check database connection when component mounts
- useEffect(() => {
- if (isElectron) {
- checkDatabaseConnection();
- } else {
- setDbStatus('disconnected');
- }
- }, [isElectron, checkDatabaseConnection]);
-
- // Submit handler
- const onSubmit = useCallback((data: DatabaseSettingsFormType) => {
- if (isElectron) {
- // Use the window.electron API via the bridge class
- window.electron?.invoke('update-database-settings', data)
- .then(() => {
- // Also update the settings in the application
- if (settings) {
- updateSettings.mutate({
- ...settings,
- databaseSettings: data
- });
- }
-
- toast({
- title: "Database settings updated",
- description: "Database settings have been saved successfully.",
- });
-
- // Re-check the connection status after settings are updated
- setTimeout(() => {
- checkDatabaseConnection();
- }, 1000);
- })
- .catch((error: Error) => {
- toast({
- title: "Error updating database settings",
- description: error.message || "Failed to update database settings",
- variant: "destructive",
- });
- });
- } else {
- // Not in Electron environment
- toast({
- title: "Settings not applied",
- description: "Local database is only available in the desktop application",
- variant: "destructive",
- });
- }
- }, [isElectron, toast, settings, updateSettings, checkDatabaseConnection]);
-
- return (
-
-
-
-
-
-
- Database Settings
-
-
- Configure database connection and synchronization settings
-
-
-
- {/* Database status indicator */}
-
-
Database Status
-
- {dbStatus === 'checking' && (
- <> Checking...>
- )}
- {dbStatus === 'connected' && (
- <> Connected>
- )}
- {dbStatus === 'disconnected' && (
- <> Disconnected>
- )}
-
-
-
- {!isElectron && (
-
-
Desktop Application Required
-
- Database configuration is only available in the desktop application.
- These settings will have no effect in the web browser.
-
-
- )}
-
-
-
- Synchronization
- Connection
-
-
- {/* Sync Settings Tab */}
-
- (
-
-
- Use Local Database
-
- Store inventory data locally for offline access
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Auto-Connect
-
- Automatically connect to database on startup
-
-
-
-
-
-
- )}
- />
-
- (
-
- Sync Interval (minutes)
-
-
-
-
- How often to synchronize local database with the server
-
-
-
- )}
- />
-
- (
-
- Max Offline Days
-
-
-
-
- Maximum number of days to retain offline data
-
-
-
- )}
- />
-
-
-
(
-
-
- Sync on Startup
-
- Synchronize when application starts
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Data Compression
-
- Compress local data to save space
-
-
-
-
-
-
- )}
- />
-
-
- (
-
-
- Force Offline Mode
-
- Work offline even when internet is available (for testing)
-
-
-
-
-
-
- )}
- />
-
-
- {/* Connection Settings Tab */}
-
- (
-
- Database Host
-
-
-
-
- The hostname or IP address of your PostgreSQL server
-
-
-
- )}
- />
-
- (
-
- Port
-
-
-
-
- The port your PostgreSQL server is running on (default: 5432)
-
-
-
- )}
- />
-
-
- (
-
- Username
-
-
-
-
-
- )}
- />
-
- (
-
- Password
-
-
-
-
-
- )}
- />
-
-
- (
-
- Database Name
-
-
-
-
- The name of your PostgreSQL database
-
-
-
- )}
- />
-
-
-
Database Connection Security
-
- Your database credentials are stored securely within the application and
- are never transmitted to external servers. All connections are made directly
- from your device to the database server.
-
-
-
-
-
-
-
About Offline Functionality
-
- The local database allows you to work offline and synchronizes
- automatically when an internet connection is available. Changes made
- offline will be uploaded during the next synchronization.
-
-
-
-
-
-
- Test Connection
-
-
-
- {isSyncing ? (
- <>
-
- Synchronizing...
- >
- ) : (
- <>
-
- Sync Now
- >
- )}
-
-
-
-
- {updateSettings.isPending && }
- Save Settings
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/forecasting-settings-form.tsx b/client/src/components/settings/forecasting-settings-form.tsx
deleted file mode 100644
index 0ef383fe..00000000
--- a/client/src/components/settings/forecasting-settings-form.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import React from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import * as z from "zod";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { Switch } from "@/components/ui/switch";
-import { Loader2, BarChart3 } from "lucide-react";
-import { useSettings } from "@/hooks/use-settings";
-
-// Define schema for form validation
-const forecastingSettingsSchema = z.object({
- forecastingEnabled: z.boolean().default(true),
- forecastDays: z.coerce.number().int().min(1, "Forecast period must be at least 1 day").max(365, "Forecast period cannot exceed 365 days"),
- seasonalAdjustmentEnabled: z.boolean().default(true),
-});
-
-// Type for form data
-type ForecastingSettingsFormType = z.infer;
-
-export function ForecastingSettingsForm() {
- const { settings, updateSettings } = useSettings();
-
- // Create form
- const form = useForm({
- resolver: zodResolver(forecastingSettingsSchema),
- defaultValues: {
- forecastingEnabled: settings.forecastingEnabled ?? true,
- forecastDays: settings.forecastDays ?? 30,
- seasonalAdjustmentEnabled: settings.seasonalAdjustmentEnabled ?? true,
- },
- });
-
- // Submit handler
- function onSubmit(data: ForecastingSettingsFormType) {
- updateSettings.mutate(data);
- }
-
- return (
-
-
-
-
-
-
- Forecasting Settings
-
-
- Configure how the system predicts future inventory needs
-
-
-
- (
-
-
- Enable Demand Forecasting
-
- Use historical data to predict future inventory needs
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Seasonal Adjustment
-
- Account for seasonal variations in demand forecasting
-
-
-
-
-
-
- )}
- />
-
- (
-
- Forecast Period (Days)
-
-
-
-
- How many days in advance to forecast inventory needs
-
-
-
- )}
- />
-
-
-
About AI-Powered Forecasting
-
- The forecasting engine uses machine learning to analyze historical sales data,
- seasonal trends, and market conditions to predict future inventory needs.
- This helps prevent stockouts and optimize inventory levels.
-
-
-
-
-
- {updateSettings.isPending && }
- Save Changes
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/general-settings-form.tsx b/client/src/components/settings/general-settings-form.tsx
deleted file mode 100644
index 7851ed49..00000000
--- a/client/src/components/settings/general-settings-form.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-import React from "react";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
-import { useSettings, type AppSettings } from "@/hooks/use-settings";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import { Loader2 } from "lucide-react";
-
-// Define form schema
-const generalSettingsSchema = z.object({
- companyName: z.string().min(1, "Company name is required"),
- companyLogo: z.string().nullable().optional(),
- primaryColor: z.string().regex(/^#[0-9A-F]{6}$/i, "Must be a valid hex color"),
- dateFormat: z.string().min(1, "Date format is required"),
- timeFormat: z.string().min(1, "Time format is required"),
- currencySymbol: z.string().min(1, "Currency symbol is required"),
-});
-
-export function GeneralSettingsForm() {
- const { settings, updateSettings } = useSettings();
-
- // Create form
- const form = useForm>({
- resolver: zodResolver(generalSettingsSchema),
- defaultValues: {
- companyName: settings.companyName || '',
- companyLogo: settings.companyLogo,
- primaryColor: settings.primaryColor || '#0f766e',
- dateFormat: settings.dateFormat || 'YYYY-MM-DD',
- timeFormat: settings.timeFormat || 'HH:mm',
- currencySymbol: settings.currencySymbol || '$',
- },
- });
-
- // Submit handler
- function onSubmit(data: z.infer) {
- if (settings) {
- updateSettings.mutate({
- ...settings,
- companyName: data.companyName,
- companyLogo: data.companyLogo,
- primaryColor: data.primaryColor,
- dateFormat: data.dateFormat,
- timeFormat: data.timeFormat,
- currencySymbol: data.currencySymbol,
- });
- }
- }
-
- return (
-
-
- General Information
-
- Basic information about your company and display preferences
-
-
-
-
-
- (
-
- Company Name
-
-
-
-
- This will be displayed throughout the application
-
-
-
- )}
- />
-
- (
-
- Company Logo URL
-
-
-
-
- URL to your company logo (Optional)
-
-
-
- )}
- />
-
-
-
-
- (
-
- Date Format
-
-
-
-
- Format for displaying dates
-
-
-
- )}
- />
-
- (
-
- Time Format
-
-
-
-
- Format for displaying times
-
-
-
- )}
- />
-
-
-
-
- {updateSettings.isPending && }
- Save Changes
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/inventory-settings-form.tsx b/client/src/components/settings/inventory-settings-form.tsx
deleted file mode 100644
index d56475fc..00000000
--- a/client/src/components/settings/inventory-settings-form.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import React from "react";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useSettings } from "@/hooks/use-settings";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Switch } from "@/components/ui/switch";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import { Loader2 } from "lucide-react";
-import { z } from "zod";
-
-// Define form schema
-const inventorySettingsSchema = z.object({
- lowStockDefaultThreshold: z.number().int().min(1, "Threshold must be at least 1"),
- allowNegativeInventory: z.boolean(),
- requireLocationForItems: z.boolean(),
- allowTransfersBetweenWarehouses: z.boolean(),
-});
-
-type InventorySettingsFormType = z.infer;
-
-export function InventorySettingsForm() {
- const { settings, updateSettings } = useSettings();
-
- // Create form
- const form = useForm({
- resolver: zodResolver(inventorySettingsSchema),
- defaultValues: {
- lowStockDefaultThreshold: settings.lowStockDefaultThreshold ?? 10,
- allowNegativeInventory: settings.allowNegativeInventory ?? false,
- requireLocationForItems: settings.requireLocationForItems ?? true,
- allowTransfersBetweenWarehouses: settings.allowTransfersBetweenWarehouses ?? true,
- },
- });
-
- // Submit handler
- function onSubmit(data: InventorySettingsFormType) {
- updateSettings.mutate(data);
- }
-
- return (
-
-
- Inventory Management
-
- Configure settings for inventory management and tracking
-
-
-
-
-
- (
-
- Low Stock Threshold
-
- field.onChange(Number(e.target.value))}
- min={1}
- />
-
-
- Default quantity threshold for low stock alerts
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Allow Negative Inventory
-
- Allow quantity to go below zero when stock is insufficient
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Require Location for Items
-
- Require a storage location to be specified for all inventory items
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Allow Transfers Between Warehouses
-
- Allow moving inventory items between different warehouse locations
-
-
-
- )}
- />
-
-
-
- {updateSettings.isPending && }
- Save Changes
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/realtime-settings-form.tsx b/client/src/components/settings/realtime-settings-form.tsx
deleted file mode 100644
index 00fce657..00000000
--- a/client/src/components/settings/realtime-settings-form.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import React from "react";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useSettings } from "@/hooks/use-settings";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Switch } from "@/components/ui/switch";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import { Loader2 } from "lucide-react";
-import { z } from "zod";
-
-// Define form schema
-const realtimeSettingsSchema = z.object({
- realTimeUpdatesEnabled: z.boolean(),
- lowStockAlertFrequency: z.number().int().min(1, "Alert frequency must be at least 1 minute"),
- autoReorderEnabled: z.boolean(),
-});
-
-type RealtimeSettingsFormType = z.infer;
-
-export function RealtimeSettingsForm() {
- const { settings, updateSettings } = useSettings();
-
- // Create form
- const form = useForm({
- resolver: zodResolver(realtimeSettingsSchema),
- defaultValues: {
- realTimeUpdatesEnabled: settings.realTimeUpdatesEnabled ?? true,
- lowStockAlertFrequency: settings.lowStockAlertFrequency ?? 30,
- autoReorderEnabled: settings.autoReorderEnabled ?? false,
- },
- });
-
- // Submit handler
- function onSubmit(data: RealtimeSettingsFormType) {
- updateSettings.mutate(data);
- }
-
- return (
-
-
- Real-Time Updates
-
- Configure real-time synchronization settings for inventory changes
-
-
-
-
-
- (
-
-
-
-
-
- Enable Real-Time Updates
-
- Receive instant notifications for inventory changes
-
-
-
- )}
- />
-
- (
-
- Low Stock Alert Frequency (minutes)
-
- field.onChange(Number(e.target.value))}
- min={1}
- max={1440} // 24 hours in minutes
- disabled={!form.watch("realTimeUpdatesEnabled")}
- />
-
-
- How often to check and send low stock alerts (in minutes)
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Automatic Reorder Requests
-
- Automatically generate reorder requests when stock falls below threshold
-
-
-
- )}
- />
-
-
-
About WebSocket Connection
-
- Real-time updates use WebSocket technology to deliver instant notifications.
- You can monitor connection status on the Real-Time Updates page.
-
-
-
-
-
- {updateSettings.isPending && }
- Save Changes
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/security-settings-form.tsx b/client/src/components/settings/security-settings-form.tsx
deleted file mode 100644
index 7ba04858..00000000
--- a/client/src/components/settings/security-settings-form.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-import React from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import * as z from "zod";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { Switch } from "@/components/ui/switch";
-import { Loader2, Shield, Lock, Key, Clock } from "lucide-react";
-import { useToast } from "@/hooks/use-toast";
-import { useSettings } from "@/hooks/use-settings";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-// Define schema for form validation
-const securitySettingsSchema = z.object({
- enableTwoFactor: z.boolean().default(false),
- passwordExpiryDays: z.coerce.number().int().min(0, "Minimum is 0 (never expire)").max(365, "Maximum is 365 days"),
- maxLoginAttempts: z.coerce.number().int().min(1, "Minimum is 1 attempt").max(10, "Maximum is 10 attempts"),
- sessionTimeoutMinutes: z.coerce.number().int().min(5, "Minimum is 5 minutes").max(1440, "Maximum is 24 hours (1440 minutes)"),
- requireStrongPasswords: z.boolean().default(true),
- logActivityEnabled: z.boolean().default(true),
- accessTokenExpiryHours: z.coerce.number().int().min(1, "Minimum is 1 hour").max(720, "Maximum is 30 days (720 hours)"),
-});
-
-// Type for form data
-type SecuritySettingsFormType = z.infer;
-
-export function SecuritySettingsForm() {
- const { toast } = useToast();
- const { settings, updateSettings } = useSettings();
-
- // Create form with default values
- const form = useForm({
- resolver: zodResolver(securitySettingsSchema),
- defaultValues: {
- enableTwoFactor: false,
- passwordExpiryDays: 90,
- maxLoginAttempts: 5,
- sessionTimeoutMinutes: 60,
- requireStrongPasswords: true,
- logActivityEnabled: true,
- accessTokenExpiryHours: 24,
- },
- });
-
- // Submit handler
- function onSubmit(data: SecuritySettingsFormType) {
- // In a real implementation, this would integrate with the settings API
- console.log("Security settings submitted:", data);
-
- toast({
- title: "Security settings updated",
- description: "Your security settings have been saved successfully.",
- });
- }
-
- return (
-
-
-
-
-
-
- Security Settings
-
-
- Configure authentication and security preferences
-
-
-
- (
-
-
-
-
-
- Enable Two-Factor Authentication
-
-
-
- Require 2FA for all users
-
-
-
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Require Strong Passwords
-
-
-
- Enforce password complexity requirements
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Activity Logging
-
- Log user actions for security auditing
-
-
-
-
-
-
- )}
- />
-
-
- (
-
- Password Expiry (Days)
-
-
-
-
- Days until passwords expire (0 = never)
-
-
-
- )}
- />
-
- (
-
- Max Login Attempts
-
-
-
-
- Maximum failed login attempts before lockout
-
-
-
- )}
- />
-
-
-
-
-
-
About Security Features
-
- Security settings help protect your data and ensure compliance with
- industry standards. Two-factor authentication adds an extra layer of
- security by requiring a verification code in addition to passwords.
-
-
-
-
-
- {updateSettings.isPending && }
- Save Changes
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/tax-settings-form.tsx b/client/src/components/settings/tax-settings-form.tsx
deleted file mode 100644
index 2cbf6fc2..00000000
--- a/client/src/components/settings/tax-settings-form.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import React from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import * as z from "zod";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { Button } from "@/components/ui/button";
-import { Switch } from "@/components/ui/switch";
-import { Loader2, Receipt } from "lucide-react";
-import { useSettings } from "@/hooks/use-settings";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-// Define schema for form validation
-const taxSettingsSchema = z.object({
- enableVat: z.boolean().default(false),
- defaultVatCountry: z.string().min(2, "Country code must be valid").optional(),
- showPricesWithVat: z.boolean().default(true),
-});
-
-// Type for form data
-type TaxSettingsFormType = z.infer;
-
-// Country options
-const countryCodes = [
- { value: "US", label: "United States" },
- { value: "GB", label: "United Kingdom" },
- { value: "CA", label: "Canada" },
- { value: "AU", label: "Australia" },
- { value: "DE", label: "Germany" },
- { value: "FR", label: "France" },
- { value: "IT", label: "Italy" },
- { value: "ES", label: "Spain" },
- { value: "JP", label: "Japan" },
- { value: "CN", label: "China" },
- { value: "IN", label: "India" },
- { value: "BR", label: "Brazil" },
- { value: "MX", label: "Mexico" },
- { value: "ZA", label: "South Africa" },
- { value: "SG", label: "Singapore" },
- { value: "AE", label: "United Arab Emirates" },
-];
-
-export function TaxSettingsForm() {
- const { settings, updateSettings } = useSettings();
-
- // Create form
- const form = useForm({
- resolver: zodResolver(taxSettingsSchema),
- defaultValues: {
- enableVat: settings.enableVat ?? false,
- defaultVatCountry: settings.defaultVatCountry || "US",
- showPricesWithVat: settings.showPricesWithVat ?? true,
- },
- });
-
- // Submit handler
- function onSubmit(data: TaxSettingsFormType) {
- updateSettings.mutate(data);
- }
-
- return (
-
-
-
-
-
-
- Tax Settings
-
-
- Configure VAT and tax calculation preferences
-
-
-
- (
-
-
- Enable VAT/Sales Tax
-
- Calculate and track VAT or sales tax for inventory items
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Show Prices with VAT
-
- Display prices including VAT/tax by default
-
-
-
-
-
-
- )}
- />
-
- (
-
- Default Tax Region
-
-
-
-
-
-
-
- {countryCodes.map((country) => (
-
- {country.label}
-
- ))}
-
-
-
- Default country or region for tax calculations
-
-
-
- )}
- />
-
-
-
About Tax Configuration
-
- The tax system supports multiple tax zones and rates. For detailed tax rate
- configuration, use the VAT Rates section in the System Administration
- area. Tax rates are automatically updated for supported regions.
-
-
-
-
-
- {updateSettings.isPending && }
- Save Changes
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/settings/warehouse-settings-form.tsx b/client/src/components/settings/warehouse-settings-form.tsx
deleted file mode 100644
index 81856eea..00000000
--- a/client/src/components/settings/warehouse-settings-form.tsx
+++ /dev/null
@@ -1,471 +0,0 @@
-import React from "react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import * as z from "zod";
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Switch } from "@/components/ui/switch";
-import { Separator } from "@/components/ui/separator";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import {
- Loader2,
- Building,
- Map,
- ArrowLeftRight,
- LayoutGrid,
- ShieldAlert,
- TruckIcon,
- FileWarning,
- InfoIcon
-} from "lucide-react";
-import { useSettings } from "@/hooks/use-settings";
-import { useQuery } from "@tanstack/react-query";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { Link } from "wouter";
-
-// Define schema for form validation
-const warehouseSettingsSchema = z.object({
- defaultWarehouseId: z.coerce.number().int().positive().nullable().optional(),
- requireLocationForItems: z.boolean().default(true),
- allowTransfersBetweenWarehouses: z.boolean().default(true),
- autoUpdateStockLevels: z.boolean().default(true),
- trackInventoryByLocation: z.boolean().default(false),
- enableBinLocations: z.boolean().default(false),
- requireApprovalForTransfers: z.boolean().default(false),
- lowStockNotificationsEnabled: z.boolean().default(true),
- transferNotificationsEnabled: z.boolean().default(true),
- autoGenerateReorderRequests: z.boolean().default(false),
- defaultBinNamingConvention: z.string().optional(),
- warehouseCodePrefix: z.string().min(0).max(5).optional(),
-});
-
-// Type for form data
-type WarehouseSettingsFormType = z.infer;
-
-// Warehouse interface for type safety
-interface Warehouse {
- id: number;
- name: string;
-}
-
-export function WarehouseSettingsForm() {
- const { settings, updateSettings } = useSettings();
-
- // Fetch warehouses
- const { data: warehouses = [] } = useQuery({
- queryKey: ["/api/warehouses"],
- });
-
- // Create form
- const form = useForm({
- resolver: zodResolver(warehouseSettingsSchema),
- defaultValues: {
- defaultWarehouseId: settings.defaultWarehouseId,
- requireLocationForItems: settings.requireLocationForItems ?? true,
- allowTransfersBetweenWarehouses: settings.allowTransfersBetweenWarehouses ?? true,
- autoUpdateStockLevels: true,
- trackInventoryByLocation: false,
- enableBinLocations: false,
- requireApprovalForTransfers: false,
- lowStockNotificationsEnabled: true,
- transferNotificationsEnabled: true,
- autoGenerateReorderRequests: settings.autoReorderEnabled ?? false,
- defaultBinNamingConvention: "ZONE-AISLE-SHELF-BIN",
- warehouseCodePrefix: "WH",
- },
- });
-
- // Submit handler
- function onSubmit(data: WarehouseSettingsFormType) {
- updateSettings.mutate({
- ...data,
- // Map any special cases from our form to the settings
- autoReorderEnabled: data.autoGenerateReorderRequests
- });
- }
-
- const hasWarehouses = warehouses.length > 0;
-
- return (
-
-
-
-
-
-
- Warehouse Settings
-
-
- Configure warehouse and location management preferences for your inventory system
-
-
-
- {!hasWarehouses && (
-
-
- No warehouses found
-
- You need to create at least one warehouse before configuring warehouse settings.
-
- Go to Warehouses
-
-
-
- )}
-
-
-
- General
- Transfers
- Notifications
-
-
-
- (
-
- Default Warehouse
-
- field.onChange(value ? parseInt(value) : null)
- }
- value={field.value?.toString() || ""}
- >
-
-
-
-
-
-
- No Default
- {warehouses.map((warehouse) => (
-
- {warehouse.name}
-
- ))}
-
-
-
- The warehouse to use by default for new inventory items
-
-
-
- )}
- />
-
- (
-
- Warehouse Code Prefix
-
-
-
-
- Used when generating warehouse codes (up to 5 characters)
-
-
-
- )}
- />
-
-
-
- (
-
-
-
-
-
- Require Item Locations
-
-
-
- Require location information when adding new inventory items
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Track Inventory by Location
-
- Track stock quantities separately for each location within warehouses
-
-
-
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Enable Bin Locations
-
-
-
- Enable detailed bin location tracking within warehouses
-
-
-
-
-
-
- )}
- />
-
- {form.watch("enableBinLocations") && (
- (
-
- Default Bin Naming Convention
-
-
-
-
- Standard format for naming bin locations (e.g., ZONE-AISLE-SHELF-BIN)
-
-
-
- )}
- />
- )}
-
-
-
- (
-
-
-
-
-
- Allow Warehouse Transfers
-
-
-
- Enable movement of inventory between warehouses
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Auto-Update Stock Levels
-
- Automatically update stock levels when transfers are completed
-
-
-
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Require Approval for Transfers
-
-
-
- Require manager approval for stock transfers between warehouses
-
-
-
-
-
-
- )}
- />
-
-
-
- (
-
-
- Low Stock Notifications
-
- Enable notifications when items reach low stock levels in warehouses
-
-
-
-
-
-
- )}
- />
-
- (
-
-
- Transfer Notifications
-
- Enable notifications when inventory is transferred between warehouses
-
-
-
-
-
-
- )}
- />
-
- (
-
-
-
-
-
- Auto-Generate Reorder Requests
-
-
-
- Automatically generate reorder requests when items reach low stock thresholds
-
-
-
-
-
-
- )}
- />
-
-
-
-
-
-
-
About Multi-Warehouse Management
-
-
- Multi-warehouse functionality allows tracking and managing inventory across
- different physical locations. The system can automatically suggest stock
- transfers between warehouses to optimize inventory levels.
-
-
- For detailed bin location tracking, enable both the "Track Inventory by Location"
- and "Enable Bin Locations" settings. You can then define specific bin locations
- within each warehouse.
-
-
-
-
-
- {updateSettings.isPending && }
- Save Changes
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx
index 3b9bd236..66a10292 100644
--- a/client/src/components/sidebar.tsx
+++ b/client/src/components/sidebar.tsx
@@ -2,7 +2,18 @@ import { Link, useLocation } from "wouter";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useTheme } from "@/components/theme-provider";
-import { Archive, BarChart3, Building, ChevronRight, FileText, Home, Moon, Settings, ShoppingCart, Sun, Users, X, LayoutDashboard, RefreshCw, QrCode, Activity, Zap, FileUp, Camera } from "lucide-react";
+import {
+ Briefcase,
+ Home,
+ Map,
+ MessageSquare,
+ Moon,
+ Settings,
+ Star,
+ Sun,
+ User,
+ X,
+} from "lucide-react";
interface SidebarProps {
open: boolean;
@@ -17,7 +28,7 @@ export default function Sidebar({ open, setOpen }: SidebarProps) {
return location === path;
};
- const NavItem = ({ path, icon, children }: { path: string, icon: React.ReactNode, children: React.ReactNode }) => {
+ const NavItem = ({ path, icon, children }: { path: string; icon: React.ReactNode; children: React.ReactNode }) => {
return (
{icon}
@@ -37,99 +48,63 @@ export default function Sidebar({ open, setOpen }: SidebarProps) {
return (
<>
- {/* Mobile backdrop */}
- {open && (
-
setOpen(false)}
- />
- )}
+ {open &&
setOpen(false)} />}
- {/* Sidebar */}