-
-
-
-
-
-
- {getInitials(user.fullName || user.username)}
-
-
{user.fullName || user.username}
-
{user.email}
-
{user.role}
-
-
-
-
- setActiveTab("profile")}
- >
-
- Profile
-
- setActiveTab("security")}
- >
-
- Security
-
-
-
-
-
-
- {/* Account Activity Card */}
-
-
- Account Activity
-
-
-
-
Last login
-
Today, 8:26 AM
-
-
-
Login location
-
San Francisco, USA
-
-
-
Account created
-
January 5, 2024
-
-
-
-
-
-
- {activeTab === "profile" && (
-
-
-
- Profile Information
-
- Update your personal information and profile picture
-
-
-
-
-
-
-
-
- {/* Additional Contact Information */}
-
-
- Contact Information
-
- Add additional contact details to your profile
-
-
-
-
-
- Cancel
- Save Details
-
-
-
-
- )}
-
- {activeTab === "security" && (
-
-
-
- Password
-
- Change your password to keep your account secure
-
-
-
-
-
-
-
-
-
-
- Security Preferences
-
- Configure your account security settings
-
-
-
-
-
-
-
+
+
+
+
+
+ Public details
+ Visible to clients when they browse your profile.
+
+
+
+ Display name
+
+
+
+ Hourly rate
+
+
+
+ Primary location
+
+
+
+ Languages
+
+
+
+ Short bio
+
+
+
+
+
+
+
+
+
+
+ Verification status
+
+ Upload certificates to boost client trust.
+
+
+ Identity verified
+ Trade certificate pending review
+ Upload certificate
+
+
+
+
+
+
+ Rating snapshot
+
+ Your reputation at a glance.
+
+
+ 4.8 ★
+
+ 64 completed jobs
+ 92% repeat clients
- )}
-
+
+
+
+
+
+ Skills & services
+ Add the services you want to be hired for.
+
+
+ {skills.map((skill) => (
+ {skill}
+ ))}
+ + Add skill
+
+
);
}
-
-// Custom form description removed to fix duplicate declaration
\ No newline at end of file
diff --git a/client/src/pages/reviews.tsx b/client/src/pages/reviews.tsx
new file mode 100644
index 00000000..12e17545
--- /dev/null
+++ b/client/src/pages/reviews.tsx
@@ -0,0 +1,48 @@
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Star } from "lucide-react";
+
+const reviews = [
+ {
+ freelancer: "Mina Okafor",
+ rating: 5,
+ summary: "Delivered stunning event photos and was right on time.",
+ job: "Corporate headshots",
+ },
+ {
+ freelancer: "Amira Bello",
+ rating: 4.5,
+ summary: "Great collaboration and clear updates throughout the project.",
+ job: "Product design sprint",
+ },
+];
+
+export default function ReviewsPage() {
+ return (
+
+
+
+
+ {reviews.map((review) => (
+
+
+
+ {review.freelancer}
+
+
+ {review.rating}
+
+
+ {review.job}
+
+ {review.summary}
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx
index 1a3adc67..971d7646 100644
--- a/client/src/pages/settings.tsx
+++ b/client/src/pages/settings.tsx
@@ -1,105 +1,72 @@
-import React from "react";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { GeneralSettingsForm } from "@/components/settings/general-settings-form";
-import { InventorySettingsForm } from "@/components/settings/inventory-settings-form";
-import { RealtimeSettingsForm } from "@/components/settings/realtime-settings-form";
-import { DatabaseSettingsForm } from "@/components/settings/database-settings-form";
-import { WarehouseSettingsForm } from "@/components/settings/warehouse-settings-form";
-import { SecuritySettingsForm } from "@/components/settings/security-settings-form";
-import { ForecastingSettingsForm } from "@/components/settings/forecasting-settings-form";
-import { TaxSettingsForm } from "@/components/settings/tax-settings-form";
-import { BillingSettingsForm } from "@/components/settings/billing-settings-form";
-import { Settings, UserCircle, Package, Activity, Database, CreditCard, Building, Shield, BarChart3, Receipt } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
export default function SettingsPage() {
return (
-
-
-
-
-
Settings
-
-
- Configure application settings to match your business needs
-
-
+
+
-
-
-
-
- General
-
-
-
- Inventory
-
-
-
- Real-Time
-
-
-
- Database
-
-
-
- Forecasting
-
-
-
- Tax
-
-
-
- Billing
-
-
-
- Warehouses
-
-
-
- Security
-
-
+
+
+ Notifications
+ Choose how you want to receive updates.
+
+
+
+
+
New job requests
+
Alerts when a client wants to book you.
+
+
+
+
+
+
Message notifications
+
Stay in sync with ongoing chats.
+
+
+
+
+
-
-
-
+
+
+ Availability
+ Set working hours and travel radius.
+
+
+
+ Weekly availability
+
+
+
+ Service radius
+
+
+
+ Emergency availability
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Safety
+ Keep your account protected and trusted.
+
+
+ Update password
+ Enable 2-factor authentication
+
+
);
-}
\ No newline at end of file
+}
From 564ee39503d0d56d1e0b1ea01a392f250cb5058b Mon Sep 17 00:00:00 2001
From: Mqhele-dot <89026892+Mqhele-dot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 20:41:27 +0200
Subject: [PATCH 3/9] Fix auth flow and electron cleanup
---
client/src/App.tsx | 23 ++++++++++++-----
client/src/pages/auth-page.tsx | 47 +++++++++++++++++++++++++++++-----
2 files changed, 57 insertions(+), 13 deletions(-)
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 66dadae3..4b216238 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -88,21 +88,30 @@ function AppLayout({ children }: { children: React.ReactNode }) {
}
function setupElectronApp() {
- if (isElectronEnvironment()) {
- document.documentElement.classList.add("electron-app");
- document.addEventListener("dragover", (e) => e.preventDefault());
- document.addEventListener("drop", (e) => e.preventDefault());
- }
+ if (!isElectronEnvironment()) return () => {};
+
+ document.documentElement.classList.add("electron-app");
+
+ const prevent = (e: DragEvent) => e.preventDefault();
+ document.addEventListener("dragover", prevent);
+ document.addEventListener("drop", prevent);
+
+ return () => {
+ document.documentElement.classList.remove("electron-app");
+ document.removeEventListener("dragover", prevent);
+ document.removeEventListener("drop", prevent);
+ };
}
function App() {
useEffect(() => {
- setupElectronApp();
+ const cleanup = setupElectronApp();
+ return cleanup;
}, []);
return (
-
+
diff --git a/client/src/pages/auth-page.tsx b/client/src/pages/auth-page.tsx
index 1a17af69..7002a429 100644
--- a/client/src/pages/auth-page.tsx
+++ b/client/src/pages/auth-page.tsx
@@ -1,15 +1,18 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useLocation } from "wouter";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
-import { MapPin, ShieldCheck, Star, Users } from "lucide-react";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Loader2, MapPin, ShieldCheck, Star, Users } from "lucide-react";
export default function AuthPage() {
- const { user } = useAuth();
+ const { user, loginMutation } = useAuth();
const [, navigate] = useLocation();
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
useEffect(() => {
if (user) {
@@ -64,15 +67,47 @@ export default function AuthPage() {
Sign in to manage jobs, requests, and bookings.
+ {loginMutation.error ? (
+
+ Sign-in failed
+
+ {(loginMutation.error as Error)?.message || "Please check your details and try again."}
+
+
+ ) : null}
Email
-
+ setEmail(event.target.value)}
+ autoComplete="username"
+ />
Password
-
+ setPassword(event.target.value)}
+ autoComplete="current-password"
+ />
- Continue
+ loginMutation.mutate({ username: email, password })}
+ >
+ {loginMutation.isPending ? (
+ <>
+
+ Signing in...
+ >
+ ) : (
+ "Continue"
+ )}
+
New to SkillRadius? Create an account
From bfdc9d1fc2c43cdd682bcee73f91d1c546628d91 Mon Sep 17 00:00:00 2001
From: Mqhele-dot <89026892+Mqhele-dot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 20:48:34 +0200
Subject: [PATCH 4/9] Polish profile and reviews pages
---
client/src/pages/profile.tsx | 64 ++++++++++++++++++++++++++++--------
client/src/pages/reviews.tsx | 60 ++++++++++++++++++++++-----------
2 files changed, 91 insertions(+), 33 deletions(-)
diff --git a/client/src/pages/profile.tsx b/client/src/pages/profile.tsx
index 6086c478..55c5bc6b 100644
--- a/client/src/pages/profile.tsx
+++ b/client/src/pages/profile.tsx
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
-import { Globe, MapPin, ShieldCheck, Star } from "lucide-react";
+import { Briefcase, CheckCircle2, Globe, MapPin, ShieldCheck, Star } from "lucide-react";
const skills = ["Plumbing", "Electrical", "Appliance Repair", "Home Maintenance"];
@@ -42,6 +42,14 @@ export default function ProfilePage() {
Short bio
+
+
Top skills
+
+ {skills.map((skill) => (
+ {skill}
+ ))}
+
+
@@ -55,9 +63,17 @@ export default function ProfilePage() {
Upload certificates to boost client trust.
- Identity verified
- Trade certificate pending review
- Upload certificate
+
+
+ Identity verified
+
+
+
+ Trade certificate pending review
+
+
+ Upload certificate
+
@@ -70,24 +86,44 @@ export default function ProfilePage() {
4.8 ★
-
-
64 completed jobs
-
92% repeat clients
+
+
+ 203 reviews
+
+
+ 98% response rate
+
+
+ Top-rated this month
+
+
View public profile
- Skills & services
- Add the services you want to be hired for.
+ Availability
+ Tell clients when you can start.
-
- {skills.map((skill) => (
- {skill}
- ))}
- + Add skill
+
+
+ Earliest start
+
+
+
+ Typical hours
+
+
+
+ Service area
+
+
+
+ Cancel
+ Save profile
+
diff --git a/client/src/pages/reviews.tsx b/client/src/pages/reviews.tsx
index 12e17545..4f93231c 100644
--- a/client/src/pages/reviews.tsx
+++ b/client/src/pages/reviews.tsx
@@ -4,42 +4,64 @@ import { Star } from "lucide-react";
const reviews = [
{
- freelancer: "Mina Okafor",
+ client: "Samantha K.",
rating: 5,
- summary: "Delivered stunning event photos and was right on time.",
- job: "Corporate headshots",
+ service: "Furniture Assembly",
+ note: "Arrived on time and finished faster than expected. Clean work.",
+ date: "2 days ago",
},
{
- freelancer: "Amira Bello",
- rating: 4.5,
- summary: "Great collaboration and clear updates throughout the project.",
- job: "Product design sprint",
+ client: "David M.",
+ rating: 5,
+ service: "Drywall Repair",
+ note: "Great communication and the repair looks perfect after paint.",
+ date: "Last week",
+ },
+ {
+ client: "Nadia R.",
+ rating: 4,
+ service: "Plumbing",
+ note: "Solved the leak quickly. Would book again.",
+ date: "2 weeks ago",
},
];
+function Stars({ value }: { value: number }) {
+ return (
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
export default function ReviewsPage() {
return (
Reviews
- Verified feedback
- Only completed jobs can be reviewed to keep ratings trustworthy.
+ Reputation & feedback
+ Only completed jobs can be reviewed.
{reviews.map((review) => (
-
-
-
- {review.freelancer}
-
-
- {review.rating}
-
+
+
+
+ {review.client}
+ {review.date}
- {review.job}
+ {review.service}
- {review.summary}
+
+
+ {review.note}
+
))}
From 347f3531849c76c5bbaea8e38ea58d799ac68feb Mon Sep 17 00:00:00 2001
From: Mqhele-dot <89026892+Mqhele-dot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 21:00:16 +0200
Subject: [PATCH 5/9] Finalize profile and reviews UI
---
client/src/pages/profile.tsx | 80 ++++++++++++++++++------------------
client/src/pages/reviews.tsx | 46 ++++++---------------
2 files changed, 52 insertions(+), 74 deletions(-)
diff --git a/client/src/pages/profile.tsx b/client/src/pages/profile.tsx
index 55c5bc6b..da17258d 100644
--- a/client/src/pages/profile.tsx
+++ b/client/src/pages/profile.tsx
@@ -11,7 +11,9 @@ export default function ProfilePage() {
return (
@@ -42,14 +44,22 @@ export default function ProfilePage() {
Short bio
+
Top skills
{skills.map((skill) => (
- {skill}
+
+ {skill}
+
))}
+
+
+ Save changes
+ Preview profile
+
@@ -71,61 +81,51 @@ export default function ProfilePage() {
Trade certificate pending review
-
-
Upload certificate
+
+
+ Service radius verified
+
+ Upload verification docs
+
+
- Rating snapshot
+ Reputation
- Your reputation at a glance.
+ Clients only review completed jobs.
- 4.8 ★
+
+
+
Average rating
+
Based on recent reviews
+
+
+
+ 4.7
+
+
+
-
- 203 reviews
-
-
- 98% response rate
+
+ English, Hindi
-
- Top-rated this month
+
+ 4.8 mi coverage
- View public profile
+
+
+ View all reviews
+
-
-
-
- Availability
- Tell clients when you can start.
-
-
-
- Earliest start
-
-
-
- Typical hours
-
-
-
- Service area
-
-
-
- Cancel
- Save profile
-
-
-
);
}
diff --git a/client/src/pages/reviews.tsx b/client/src/pages/reviews.tsx
index 4f93231c..167ba9a2 100644
--- a/client/src/pages/reviews.tsx
+++ b/client/src/pages/reviews.tsx
@@ -3,37 +3,16 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Star } from "lucide-react";
const reviews = [
- {
- client: "Samantha K.",
- rating: 5,
- service: "Furniture Assembly",
- note: "Arrived on time and finished faster than expected. Clean work.",
- date: "2 days ago",
- },
- {
- client: "David M.",
- rating: 5,
- service: "Drywall Repair",
- note: "Great communication and the repair looks perfect after paint.",
- date: "Last week",
- },
- {
- client: "Nadia R.",
- rating: 4,
- service: "Plumbing",
- note: "Solved the leak quickly. Would book again.",
- date: "2 weeks ago",
- },
+ { name: "K. Morris", rating: 5, text: "Fast response and high-quality work. Would hire again.", date: "2 days ago" },
+ { name: "S. Dlamini", rating: 5, text: "Very professional and on time. Great communication.", date: "1 week ago" },
+ { name: "J. Patel", rating: 4, text: "Good work overall. Slight delay but handled well.", date: "3 weeks ago" },
];
-function Stars({ value }: { value: number }) {
+function Stars({ rating }: { rating: number }) {
return (
{reviews.map((review) => (
-
+
- {review.client}
+ {review.name}
{review.date}
- {review.service}
+
+
+
-
-
- {review.note}
-
+ {review.text}
))}
From f4cfac28f10e73aef42e651186732d0145f30135 Mon Sep 17 00:00:00 2001
From: Mqhele-dot <89026892+Mqhele-dot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 22:02:45 +0200
Subject: [PATCH 6/9] Scope typecheck to marketplace client
---
package.json | 2 +-
tsconfig.check.json | 23 +++++++++++++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
create mode 100644 tsconfig.check.json
diff --git a/package.json b/package.json
index 00e5f284..ff0026e3 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dev": "tsx server/index.ts",
"build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
"start": "NODE_ENV=production node dist/index.js",
- "check": "tsc",
+ "check": "tsc -p tsconfig.check.json --noEmit",
"db:push": "drizzle-kit push"
},
"dependencies": {
diff --git a/tsconfig.check.json b/tsconfig.check.json
new file mode 100644
index 00000000..e64ef3ae
--- /dev/null
+++ b/tsconfig.check.json
@@ -0,0 +1,23 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "include": [
+ "client/src/**/*.ts",
+ "client/src/**/*.tsx",
+ "client/vite-env.d.ts",
+ "shared/**/*.ts",
+ "shared/**/*.tsx"
+ ],
+ "exclude": [
+ "server/**",
+ "electron/**",
+ "client/src/pages/billing*",
+ "client/src/pages/inventory*",
+ "client/src/pages/reports*",
+ "client/src/pages/**/billing/**",
+ "client/src/pages/**/inventory/**",
+ "client/src/pages/**/reports/**"
+ ]
+}
From e33a88d6f1227b5db8262fd30c6af5fe4ecf9e95 Mon Sep 17 00:00:00 2001
From: Mqhele-dot <89026892+Mqhele-dot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 22:18:06 +0200
Subject: [PATCH 7/9] Remove legacy inventory and electron files
---
DATABASE_SETUP.md | 180 -
DESKTOP_APP_SETUP.md | 195 -
...-table-should-include-a--1743502078295.txt | 167 -
...update-stock-levels-acro-1743321664353.txt | 41 -
...cognition-for-Item-Entry-1743434758051.txt | 149 -
...cognition-for-Item-Entry-1743435690258.txt | 149 -
build-electron.sh | 14 -
electron-builder.json | 46 -
electron/convert-icons.cjs | 45 -
electron/convert-icons.js | 86 -
electron/database-service.js | 526 --
electron/db.js | 257 -
electron/icons/app-icon.png | 0
electron/icons/app-icon.svg | 11 -
electron/icons/tray-icon.png | 0
electron/icons/tray-icon.svg | 29 -
electron/index.js | 17 -
electron/ipc-handlers.js | 849 --
electron/main.js | 495 --
electron/preload.js | 116 -
electron/sync-service.js | 426 -
generated-icon.png | Bin 602988 -> 0 bytes
package.json | 9 +-
scripts/build-electron.js | 120 -
scripts/convert-svg-to-png.js | 53 -
scripts/electron-build-prod.sh | 3 -
scripts/electron-build.sh | 3 -
scripts/electron-dev.sh | 3 -
scripts/start-electron.js | 116 -
server/auth.ts | 957 --
.../document-extractor-controller.ts | 377 -
.../image-recognition-controller.ts | 252 -
.../controllers/profile-picture-controller.ts | 162 -
server/controllers/user-controller.ts | 256 -
server/db.ts | 87 -
server/forecast-service.ts | 162 -
server/index.ts | 165 -
server/init-db.ts | 61 -
server/proxy.ts | 92 -
server/real-time-sync-service.ts | 653 --
server/reorder-request-generators.ts | 250 -
server/routes.ts | 5364 ------------
server/services/cloudinary-service.ts | 95 -
server/services/document-extractor-service.ts | 1009 ---
server/services/document-generator-service.ts | 649 --
server/services/email-service.ts | 357 -
server/services/image-recognition-service.ts | 212 -
server/services/openai-service.ts | 311 -
server/services/pdfjs-setup.ts | 102 -
server/services/security-service.ts | 177 -
server/services/two-factor-service.ts | 57 -
server/storage.ts | 7705 -----------------
server/vite.ts | 88 -
server/websocket-service.ts | 739 --
setup-db.js | 52 -
start-electron-dev.sh | 5 -
test-report.csv | 1 -
test_inventory.csv | 5 -
test_inventory.pdf | Bin 1933 -> 0 bytes
test_inventory.xlsx | Bin 7087 -> 0 bytes
test_purchaseorders.xlsx | Bin 6921 -> 0 bytes
test_purchaserequisitions.xlsx | Bin 6924 -> 0 bytes
test_reorderrequests.xlsx | Bin 6812 -> 0 bytes
test_suppliers.xlsx | Bin 7117 -> 0 bytes
64 files changed, 4 insertions(+), 24503 deletions(-)
delete mode 100644 DATABASE_SETUP.md
delete mode 100644 DESKTOP_APP_SETUP.md
delete mode 100644 attached_assets/Pasted--Step-1-Database-Schema-Updated-with-Profile-Picture-Storage-Your-users-table-should-include-a--1743502078295.txt
delete mode 100644 attached_assets/Pasted-1-Advanced-Inventory-Management-Real-Time-Inventory-Sync-Automatically-update-stock-levels-acro-1743321664353.txt
delete mode 100644 attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743434758051.txt
delete mode 100644 attached_assets/Pasted-Core-Inventory-Management-Database-Backend-Features-AI-Image-Recognition-for-Item-Entry-1743435690258.txt
delete mode 100755 build-electron.sh
delete mode 100644 electron-builder.json
delete mode 100644 electron/convert-icons.cjs
delete mode 100644 electron/convert-icons.js
delete mode 100644 electron/database-service.js
delete mode 100644 electron/db.js
delete mode 100644 electron/icons/app-icon.png
delete mode 100644 electron/icons/app-icon.svg
delete mode 100644 electron/icons/tray-icon.png
delete mode 100644 electron/icons/tray-icon.svg
delete mode 100644 electron/index.js
delete mode 100644 electron/ipc-handlers.js
delete mode 100644 electron/main.js
delete mode 100644 electron/preload.js
delete mode 100644 electron/sync-service.js
delete mode 100644 generated-icon.png
delete mode 100644 scripts/build-electron.js
delete mode 100644 scripts/convert-svg-to-png.js
delete mode 100755 scripts/electron-build-prod.sh
delete mode 100755 scripts/electron-build.sh
delete mode 100755 scripts/electron-dev.sh
delete mode 100644 scripts/start-electron.js
delete mode 100644 server/auth.ts
delete mode 100644 server/controllers/document-extractor-controller.ts
delete mode 100644 server/controllers/image-recognition-controller.ts
delete mode 100644 server/controllers/profile-picture-controller.ts
delete mode 100644 server/controllers/user-controller.ts
delete mode 100644 server/db.ts
delete mode 100644 server/forecast-service.ts
delete mode 100644 server/index.ts
delete mode 100644 server/init-db.ts
delete mode 100644 server/proxy.ts
delete mode 100644 server/real-time-sync-service.ts
delete mode 100644 server/reorder-request-generators.ts
delete mode 100644 server/routes.ts
delete mode 100644 server/services/cloudinary-service.ts
delete mode 100644 server/services/document-extractor-service.ts
delete mode 100644 server/services/document-generator-service.ts
delete mode 100644 server/services/email-service.ts
delete mode 100644 server/services/image-recognition-service.ts
delete mode 100644 server/services/openai-service.ts
delete mode 100644 server/services/pdfjs-setup.ts
delete mode 100644 server/services/security-service.ts
delete mode 100644 server/services/two-factor-service.ts
delete mode 100644 server/storage.ts
delete mode 100644 server/vite.ts
delete mode 100644 server/websocket-service.ts
delete mode 100644 setup-db.js
delete mode 100755 start-electron-dev.sh
delete mode 100644 test-report.csv
delete mode 100644 test_inventory.csv
delete mode 100644 test_inventory.pdf
delete mode 100644 test_inventory.xlsx
delete mode 100644 test_purchaseorders.xlsx
delete mode 100644 test_purchaserequisitions.xlsx
delete mode 100644 test_reorderrequests.xlsx
delete mode 100644 test_suppliers.xlsx
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/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/electron-builder.json b/electron-builder.json
deleted file mode 100644
index 4e14b92a..00000000
--- a/electron-builder.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "appId": "com.inventorymgmt.app",
- "productName": "Inventory Management System",
- "directories": {
- "output": "dist_electron"
- },
- "files": [
- "electron/**/*",
- "client/dist/**/*",
- "server/**/*",
- "shared/**/*",
- "package.json"
- ],
- "extraResources": [
- {
- "from": "resources",
- "to": "resources",
- "filter": ["**/*"]
- }
- ],
- "mac": {
- "category": "public.app-category.business",
- "target": ["dmg", "zip"],
- "icon": "electron/icons/app-icon.icns"
- },
- "win": {
- "target": ["nsis"],
- "icon": "electron/icons/app-icon.ico"
- },
- "linux": {
- "target": ["AppImage", "deb"],
- "category": "Office",
- "icon": "electron/icons/app-icon.png"
- },
- "nsis": {
- "oneClick": false,
- "allowToChangeInstallationDirectory": true,
- "createDesktopShortcut": true,
- "createStartMenuShortcut": true
- },
- "publish": {
- "provider": "github",
- "private": true,
- "releaseType": "release"
- }
-}
\ No newline at end of file
diff --git a/electron/convert-icons.cjs b/electron/convert-icons.cjs
deleted file mode 100644
index dddac307..00000000
--- a/electron/convert-icons.cjs
+++ /dev/null
@@ -1,45 +0,0 @@
-const { createCanvas, loadImage } = require('canvas');
-const fs = require('fs');
-const path = require('path');
-
-// Function to convert SVG to PNG
-async function convertSvgToPng(svgPath, outputPath, size) {
- try {
- // Load SVG
- console.log(`Converting ${svgPath} to PNG with size ${size}x${size}...`);
- const img = await loadImage(svgPath);
-
- // Create canvas
- const canvas = createCanvas(size, size);
- const ctx = canvas.getContext('2d');
-
- // Draw image
- ctx.drawImage(img, 0, 0, size, size);
-
- // Write to file
- const buffer = canvas.toBuffer('image/png');
- fs.writeFileSync(outputPath, buffer);
-
- console.log(`Converted ${svgPath} to ${outputPath}`);
- } catch (error) {
- console.error(`Error converting ${svgPath}:`, error);
- }
-}
-
-// Ensure icons directory exists
-const iconsDir = path.join(__dirname, 'icons');
-if (!fs.existsSync(iconsDir)) {
- fs.mkdirSync(iconsDir, { recursive: true });
-}
-
-// Convert app icon
-const appSvgPath = path.join(iconsDir, 'app-icon.svg');
-const appPngPath = path.join(iconsDir, 'app-icon.png');
-convertSvgToPng(appSvgPath, appPngPath, 512);
-
-// Convert tray icon
-const traySvgPath = path.join(iconsDir, 'tray-icon.svg');
-const trayPngPath = path.join(iconsDir, 'tray-icon.png');
-convertSvgToPng(traySvgPath, trayPngPath, 256);
-
-console.log('Icon conversion complete.');
\ No newline at end of file
diff --git a/electron/convert-icons.js b/electron/convert-icons.js
deleted file mode 100644
index 8b206f70..00000000
--- a/electron/convert-icons.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * Convert SVG Icons to PNG
- *
- * This utility script converts SVG icons to PNG format for use in the
- * Electron application. It uses the canvas package to perform the conversion.
- */
-
-const fs = require('fs');
-const path = require('path');
-const { createCanvas, loadImage } = require('canvas');
-
-/**
- * Convert an SVG file to a PNG file
- * @param {string} svgPath - Path to the source SVG file
- * @param {string} outputPath - Path to save the output PNG file
- * @param {number} size - Size of the output PNG (width and height)
- * @returns {Promise
}
- */
-async function convertSvgToPng(svgPath, outputPath, size) {
- try {
- // Create a canvas with the specified size
- const canvas = createCanvas(size, size);
- const ctx = canvas.getContext('2d');
-
- // Load the SVG image
- const image = await loadImage(svgPath);
-
- // Draw the image on the canvas
- ctx.drawImage(image, 0, 0, size, size);
-
- // Convert the canvas to a PNG buffer
- const buffer = canvas.toBuffer('image/png');
-
- // Write the buffer to a file
- fs.writeFileSync(outputPath, buffer);
-
- console.log(`Converted ${svgPath} to ${outputPath} (${size}x${size})`);
- } catch (error) {
- console.error(`Error converting ${svgPath} to PNG:`, error);
- throw error;
- }
-}
-
-/**
- * Main function to convert all icons
- */
-async function convertIcons() {
- // Define source SVG path
- const svgPath = path.join(__dirname, '../public/logo.svg');
-
- // Create icons directory if it doesn't exist
- const iconsDir = path.join(__dirname, '../icons');
- if (!fs.existsSync(iconsDir)) {
- fs.mkdirSync(iconsDir, { recursive: true });
- }
-
- // Generate various icon sizes
- const sizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024];
-
- // Convert the icons
- try {
- for (const size of sizes) {
- const outputPath = path.join(iconsDir, `${size}x${size}.png`);
- await convertSvgToPng(svgPath, outputPath, size);
- }
-
- // Create a special icon for the system tray
- await convertSvgToPng(svgPath, path.join(iconsDir, 'tray-icon.png'), 24);
-
- console.log('All icons converted successfully');
- } catch (error) {
- console.error('Icon conversion failed:', error);
- process.exit(1);
- }
-}
-
-// Execute the conversion if this script is run directly
-if (require.main === module) {
- convertIcons();
-}
-
-// Export for use in other scripts
-module.exports = {
- convertSvgToPng,
- convertIcons
-};
\ No newline at end of file
diff --git a/electron/database-service.js b/electron/database-service.js
deleted file mode 100644
index 2edac0c6..00000000
--- a/electron/database-service.js
+++ /dev/null
@@ -1,526 +0,0 @@
-/**
- * Electron Database Service
- *
- * Handles local SQLite database operations for the desktop version of InvTrack.
- * Provides offline functionality with synchronization capabilities.
- */
-
-const fs = require('fs');
-const path = require('path');
-const log = require('electron-log');
-const { app } = require('electron');
-const sqlite3 = require('sqlite3').verbose();
-const { open } = require('sqlite');
-const { promisify } = require('util');
-const zlib = require('zlib');
-
-// Compression utilities
-const gzip = promisify(zlib.gzip);
-const gunzip = promisify(zlib.gunzip);
-
-// Paths
-const DATA_DIR = path.join(app.getPath('userData'), 'data');
-const DB_PATH = path.join(DATA_DIR, 'invtrack.db');
-const BACKUP_DIR = path.join(DATA_DIR, 'backups');
-
-// Initialize variables
-let db = null;
-let syncInProgress = false;
-let lastSyncTime = null;
-
-/**
- * Initialize the database service
- */
-async function initialize() {
- try {
- // Ensure data directory exists
- if (!fs.existsSync(DATA_DIR)) {
- fs.mkdirSync(DATA_DIR, { recursive: true });
- }
-
- // Ensure backup directory exists
- if (!fs.existsSync(BACKUP_DIR)) {
- fs.mkdirSync(BACKUP_DIR, { recursive: true });
- }
-
- // Open database connection
- db = await open({
- filename: DB_PATH,
- driver: sqlite3.Database
- });
-
- // Enable foreign keys
- await db.run('PRAGMA foreign_keys = ON');
-
- // Create tables if they don't exist
- await createTables();
-
- log.info('Database service initialized successfully');
- return true;
- } catch (error) {
- log.error('Failed to initialize database service:', error);
- return false;
- }
-}
-
-/**
- * Create database tables
- */
-async function createTables() {
- try {
- // Users table
- await db.exec(`
- CREATE TABLE IF NOT EXISTS users (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- username TEXT NOT NULL UNIQUE,
- email TEXT UNIQUE,
- password TEXT NOT NULL,
- fullName TEXT,
- role TEXT DEFAULT 'user',
- profilePicture TEXT,
- lastLogin TEXT,
- createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
- updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
- syncStatus TEXT DEFAULT 'pending'
- )
- `);
-
- // Categories table
- await db.exec(`
- CREATE TABLE IF NOT EXISTS categories (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- description TEXT,
- createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
- updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
- syncStatus TEXT DEFAULT 'pending'
- )
- `);
-
- // Warehouses table
- await db.exec(`
- CREATE TABLE IF NOT EXISTS warehouses (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- location TEXT,
- description TEXT,
- capacity INTEGER,
- isActive INTEGER DEFAULT 1,
- code TEXT,
- contactPerson TEXT,
- contactEmail TEXT,
- contactPhone TEXT,
- createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
- updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
- syncStatus TEXT DEFAULT 'pending'
- )
- `);
-
- // Inventory items table
- await db.exec(`
- CREATE TABLE IF NOT EXISTS inventory_items (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- description TEXT,
- sku TEXT UNIQUE,
- barcode TEXT,
- categoryId INTEGER,
- defaultWarehouseId INTEGER,
- price REAL DEFAULT 0,
- cost REAL DEFAULT 0,
- quantity INTEGER DEFAULT 0,
- lowStockThreshold INTEGER,
- reorderPoint INTEGER,
- weight REAL,
- dimensions TEXT,
- pictures TEXT,
- isActive INTEGER DEFAULT 1,
- createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
- updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
- syncStatus TEXT DEFAULT 'pending',
- FOREIGN KEY (categoryId) REFERENCES categories(id),
- FOREIGN KEY (defaultWarehouseId) REFERENCES warehouses(id)
- )
- `);
-
- // Warehouse inventory table (for tracking items across warehouses)
- await db.exec(`
- CREATE TABLE IF NOT EXISTS warehouse_inventory (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- warehouseId INTEGER NOT NULL,
- itemId INTEGER NOT NULL,
- quantity INTEGER DEFAULT 0,
- location TEXT,
- updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
- syncStatus TEXT DEFAULT 'pending',
- FOREIGN KEY (warehouseId) REFERENCES warehouses(id),
- FOREIGN KEY (itemId) REFERENCES inventory_items(id),
- UNIQUE(warehouseId, itemId)
- )
- `);
-
- // Stock movements table
- await db.exec(`
- CREATE TABLE IF NOT EXISTS stock_movements (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- itemId INTEGER NOT NULL,
- warehouseId INTEGER NOT NULL,
- quantity INTEGER NOT NULL,
- previousQuantity INTEGER,
- type TEXT NOT NULL,
- reason TEXT,
- reference TEXT,
- userId INTEGER,
- createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
- syncStatus TEXT DEFAULT 'pending',
- FOREIGN KEY (itemId) REFERENCES inventory_items(id),
- FOREIGN KEY (warehouseId) REFERENCES warehouses(id),
- FOREIGN KEY (userId) REFERENCES users(id)
- )
- `);
-
- // Suppliers table
- await db.exec(`
- CREATE TABLE IF NOT EXISTS suppliers (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- contactPerson TEXT,
- email TEXT,
- phone TEXT,
- address TEXT,
- website TEXT,
- notes TEXT,
- isActive INTEGER DEFAULT 1,
- createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
- updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
- syncStatus TEXT DEFAULT 'pending'
- )
- `);
-
- // Sync metadata table (for tracking sync status)
- await db.exec(`
- CREATE TABLE IF NOT EXISTS sync_metadata (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- entityName TEXT NOT NULL,
- lastSyncTime TEXT,
- syncVersion INTEGER DEFAULT 1,
- updateCount INTEGER DEFAULT 0
- )
- `);
-
- // Set up triggers to update the updatedAt field
- await db.exec(`
- CREATE TRIGGER IF NOT EXISTS update_users_timestamp
- AFTER UPDATE ON users
- BEGIN
- UPDATE users SET updatedAt = CURRENT_TIMESTAMP, syncStatus = 'pending' WHERE id = NEW.id;
- END;
- `);
-
- // Repeat similar triggers for other tables
-
- log.info('Database tables created successfully');
- return true;
- } catch (error) {
- log.error('Failed to create database tables:', error);
- return false;
- }
-}
-
-/**
- * Create a backup of the database
- * @param {string} customDir Optional custom directory to save the backup
- * @returns {Promise} Result of the backup operation
- */
-async function createBackup(customDir = null) {
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const backupDir = customDir || BACKUP_DIR;
- const backupPath = path.join(backupDir, `invtrack-backup-${timestamp}.db.gz`);
-
- try {
- // Ensure the backup directory exists
- if (!fs.existsSync(backupDir)) {
- fs.mkdirSync(backupDir, { recursive: true });
- }
-
- // Read the database file
- const dbData = fs.readFileSync(DB_PATH);
-
- // Compress the database data
- const compressedData = await gzip(dbData);
-
- // Write the compressed data to the backup file
- fs.writeFileSync(backupPath, compressedData);
-
- log.info(`Database backup created at ${backupPath}`);
- return {
- success: true,
- path: backupPath,
- size: compressedData.length,
- timestamp
- };
- } catch (error) {
- log.error('Failed to create database backup:', error);
- return {
- success: false,
- error: error.message,
- timestamp
- };
- }
-}
-
-/**
- * Restore the database from a backup
- * @param {string} backupPath Path to the backup file
- * @returns {Promise} Result of the restore operation
- */
-async function restoreFromBackup(backupPath) {
- try {
- // Close existing database connection
- if (db) {
- await db.close();
- db = null;
- }
-
- // Read the compressed backup file
- const compressedData = fs.readFileSync(backupPath);
-
- // Decompress the data
- const dbData = await gunzip(compressedData);
-
- // Create a backup of the current database before restoring
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const currentBackupPath = path.join(BACKUP_DIR, `pre-restore-backup-${timestamp}.db.gz`);
- const currentDbData = fs.readFileSync(DB_PATH);
- const compressedCurrentData = await gzip(currentDbData);
- fs.writeFileSync(currentBackupPath, compressedCurrentData);
-
- // Write the decompressed data to the database file
- fs.writeFileSync(DB_PATH, dbData);
-
- // Reopen the database connection
- db = await open({
- filename: DB_PATH,
- driver: sqlite3.Database
- });
-
- // Enable foreign keys
- await db.run('PRAGMA foreign_keys = ON');
-
- log.info(`Database restored from backup ${backupPath}`);
- return {
- success: true,
- originalBackupPath: backupPath,
- previousDatabaseBackupPath: currentBackupPath
- };
- } catch (error) {
- log.error('Failed to restore database from backup:', error);
- // Try to reopen the original database
- try {
- db = await open({
- filename: DB_PATH,
- driver: sqlite3.Database
- });
- await db.run('PRAGMA foreign_keys = ON');
- } catch (reopenError) {
- log.error('Failed to reopen database after restore failure:', reopenError);
- }
-
- return {
- success: false,
- error: error.message
- };
- }
-}
-
-/**
- * Get database information and statistics
- * @returns {Promise} Database information
- */
-async function getDatabaseInfo() {
- try {
- const stats = fs.statSync(DB_PATH);
- const tables = await db.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
-
- const tableStats = [];
- for (const table of tables) {
- const count = await db.get(`SELECT COUNT(*) as count FROM ${table.name}`);
- const pendingSync = await db.get(`SELECT COUNT(*) as count FROM ${table.name} WHERE syncStatus = 'pending'`);
-
- tableStats.push({
- name: table.name,
- rowCount: count.count,
- pendingSyncCount: pendingSync ? pendingSync.count : 0
- });
- }
-
- const syncMetadata = await db.get("SELECT MAX(lastSyncTime) as lastSync FROM sync_metadata");
-
- return {
- fileSize: stats.size,
- lastModified: stats.mtime,
- created: stats.birthtime,
- tables: tableStats,
- lastSync: syncMetadata ? syncMetadata.lastSync : null,
- path: DB_PATH
- };
- } catch (error) {
- log.error('Failed to get database information:', error);
- return {
- error: error.message,
- exists: fs.existsSync(DB_PATH)
- };
- }
-}
-
-/**
- * Sync data with the remote server
- * @param {function} progressCallback Optional callback for reporting sync progress
- * @returns {Promise} Sync result
- */
-async function syncWithRemoteServer(progressCallback = null) {
- if (syncInProgress) {
- return { success: false, message: 'Sync already in progress' };
- }
-
- syncInProgress = true;
- const startTime = Date.now();
- const reportProgress = (stage, percent, message) => {
- if (progressCallback) {
- progressCallback({ stage, percent, message });
- }
- };
-
- try {
- reportProgress('init', 0, 'Initializing sync process');
-
- // 1. Get all pending changes to send to the server
- reportProgress('preparing', 10, 'Preparing local changes');
- const pendingChanges = await getPendingChanges();
-
- reportProgress('uploading', 30, `Uploading ${pendingChanges.length} changes`);
- // 2. Send changes to server (would be implemented using fetch in a real app)
- // This is a placeholder for the actual server communication
-
- // 3. Get server changes
- reportProgress('downloading', 60, 'Downloading server changes');
- // This would fetch changes from the server that we don't have locally
-
- // 4. Apply server changes to local database
- reportProgress('applying', 80, 'Applying server changes');
- // This would update our local database with the changes from the server
-
- // 5. Update sync metadata
- const now = new Date().toISOString();
- await db.run(`
- INSERT OR REPLACE INTO sync_metadata (entityName, lastSyncTime, updateCount)
- VALUES ('global', ?, (SELECT COALESCE(updateCount, 0) + 1 FROM sync_metadata WHERE entityName = 'global'))
- `, [now]);
-
- lastSyncTime = now;
-
- // 6. Mark all synced entities as 'synced'
- await db.run(`UPDATE users SET syncStatus = 'synced' WHERE syncStatus = 'pending'`);
- await db.run(`UPDATE categories SET syncStatus = 'synced' WHERE syncStatus = 'pending'`);
- await db.run(`UPDATE warehouses SET syncStatus = 'synced' WHERE syncStatus = 'pending'`);
- await db.run(`UPDATE inventory_items SET syncStatus = 'synced' WHERE syncStatus = 'pending'`);
- await db.run(`UPDATE warehouse_inventory SET syncStatus = 'synced' WHERE syncStatus = 'pending'`);
- await db.run(`UPDATE stock_movements SET syncStatus = 'synced' WHERE syncStatus = 'pending'`);
- await db.run(`UPDATE suppliers SET syncStatus = 'synced' WHERE syncStatus = 'pending'`);
-
- reportProgress('complete', 100, 'Sync completed successfully');
-
- const elapsed = (Date.now() - startTime) / 1000;
- log.info(`Sync completed in ${elapsed.toFixed(2)}s`);
-
- syncInProgress = false;
- return {
- success: true,
- changesSent: pendingChanges.length,
- changesReceived: 0, // Placeholder
- syncTime: now,
- elapsedSeconds: elapsed
- };
- } catch (error) {
- log.error('Sync failed:', error);
- reportProgress('error', 0, `Sync failed: ${error.message}`);
- syncInProgress = false;
- return {
- success: false,
- error: error.message
- };
- }
-}
-
-/**
- * Get pending changes to send to the server
- * @returns {Promise} Pending changes
- */
-async function getPendingChanges() {
- const changes = [];
-
- // Get pending changes from each table
- const pendingUsers = await db.all("SELECT * FROM users WHERE syncStatus = 'pending'");
- const pendingCategories = await db.all("SELECT * FROM categories WHERE syncStatus = 'pending'");
- const pendingWarehouses = await db.all("SELECT * FROM warehouses WHERE syncStatus = 'pending'");
- const pendingItems = await db.all("SELECT * FROM inventory_items WHERE syncStatus = 'pending'");
- const pendingWarehouseInventory = await db.all("SELECT * FROM warehouse_inventory WHERE syncStatus = 'pending'");
- const pendingMovements = await db.all("SELECT * FROM stock_movements WHERE syncStatus = 'pending'");
- const pendingSuppliers = await db.all("SELECT * FROM suppliers WHERE syncStatus = 'pending'");
-
- // Add changes with their entity type
- pendingUsers.forEach(user => {
- changes.push({ entity: 'users', data: user });
- });
-
- pendingCategories.forEach(category => {
- changes.push({ entity: 'categories', data: category });
- });
-
- pendingWarehouses.forEach(warehouse => {
- changes.push({ entity: 'warehouses', data: warehouse });
- });
-
- pendingItems.forEach(item => {
- changes.push({ entity: 'inventory_items', data: item });
- });
-
- pendingWarehouseInventory.forEach(wi => {
- changes.push({ entity: 'warehouse_inventory', data: wi });
- });
-
- pendingMovements.forEach(movement => {
- changes.push({ entity: 'stock_movements', data: movement });
- });
-
- pendingSuppliers.forEach(supplier => {
- changes.push({ entity: 'suppliers', data: supplier });
- });
-
- return changes;
-}
-
-/**
- * Check if a table exists in the database
- * @param {string} tableName Table name to check
- * @returns {Promise} Whether the table exists
- */
-async function tableExists(tableName) {
- const result = await db.get(
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
- [tableName]
- );
- return result !== undefined;
-}
-
-// Export public API
-module.exports = {
- initialize,
- createBackup,
- restoreFromBackup,
- getDatabaseInfo,
- syncWithRemoteServer,
- get db() { return db; },
- get lastSyncTime() { return lastSyncTime; },
- get isSyncInProgress() { return syncInProgress; }
-};
\ No newline at end of file
diff --git a/electron/db.js b/electron/db.js
deleted file mode 100644
index feb5b55c..00000000
--- a/electron/db.js
+++ /dev/null
@@ -1,257 +0,0 @@
-/**
- * Database Module for Electron
- *
- * This module handles local data storage for the Electron application.
- * It manages database initialization, backup, and restoration.
- */
-
-const { app } = require('electron');
-const path = require('path');
-const fs = require('fs');
-const fsPromises = fs.promises;
-
-// Define database paths
-let DATA_DIRECTORY;
-let DB_FILE;
-let BACKUPS_DIRECTORY;
-
-/**
- * Initialize the data directory structure
- */
-function initializeDataDirectory() {
- DATA_DIRECTORY = path.join(app.getPath('userData'), 'data');
- DB_FILE = path.join(DATA_DIRECTORY, 'invtrack.db');
- BACKUPS_DIRECTORY = path.join(DATA_DIRECTORY, 'backups');
-
- // Create directories if they don't exist
- if (!fs.existsSync(DATA_DIRECTORY)) {
- fs.mkdirSync(DATA_DIRECTORY, { recursive: true });
- }
-
- if (!fs.existsSync(BACKUPS_DIRECTORY)) {
- fs.mkdirSync(BACKUPS_DIRECTORY, { recursive: true });
- }
-}
-
-/**
- * Create a backup of the database
- * @param {string} customDirectory Optional custom directory to save the backup to
- * @returns {Promise} Path to the backup file
- */
-async function createBackup(customDirectory = null) {
- // Create a timestamped backup filename
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const backupFileName = `invtrack-backup-${timestamp}.db`;
-
- // Determine the backup directory
- const backupDir = customDirectory || BACKUPS_DIRECTORY;
- if (!fs.existsSync(backupDir)) {
- fs.mkdirSync(backupDir, { recursive: true });
- }
-
- const backupPath = path.join(backupDir, backupFileName);
-
- // Copy the database file to the backup location
- try {
- await fsPromises.copyFile(DB_FILE, backupPath);
- console.log(`Backup created at: ${backupPath}`);
- return backupPath;
- } catch (error) {
- console.error('Error creating database backup:', error);
- throw error;
- }
-}
-
-/**
- * Restore the database from a backup file
- * @param {string} backupPath Path to the backup file
- * @returns {Promise}
- */
-async function restoreFromBackup(backupPath) {
- try {
- // Validate the backup file
- if (!fs.existsSync(backupPath)) {
- throw new Error(`Backup file not found: ${backupPath}`);
- }
-
- // Create a backup of the current database before restoring
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
- const preRestoreBackupPath = path.join(
- BACKUPS_DIRECTORY,
- `pre-restore-backup-${timestamp}.db`
- );
-
- // Create a backup before restoring (if the current database exists)
- if (fs.existsSync(DB_FILE)) {
- await fsPromises.copyFile(DB_FILE, preRestoreBackupPath);
- }
-
- // Copy the backup file to the database location
- await fsPromises.copyFile(backupPath, DB_FILE);
-
- console.log(`Database restored from: ${backupPath}`);
- console.log(`Pre-restore backup created at: ${preRestoreBackupPath}`);
-
- return true;
- } catch (error) {
- console.error('Error restoring database:', error);
- throw error;
- }
-}
-
-/**
- * Initialize the database
- */
-function initializeDatabase() {
- // Simple check if database exists
- if (!fs.existsSync(DB_FILE)) {
- console.log('Database does not exist. Creating empty database file.');
- fs.writeFileSync(DB_FILE, '', { encoding: 'utf8' });
- }
-}
-
-/**
- * Initialize the database module
- */
-function initialize() {
- console.log('Initializing database module...');
- initializeDataDirectory();
- initializeDatabase();
- console.log('Database initialized.');
-}
-
-/**
- * Get the database instance
- * This is a placeholder for more complex database implementations
- * such as SQLite, IndexedDB, etc.
- */
-function getDb() {
- // In a real application, this would return a database connection or ORM instance
- return {
- path: DB_FILE,
- backupsPath: BACKUPS_DIRECTORY
- };
-}
-
-/**
- * Get database information and statistics
- * @returns {Object} Database information
- */
-async function getDatabaseInfo() {
- try {
- // Check if database file exists
- const dbExists = fs.existsSync(DB_FILE);
-
- // Get file stats
- let stats = null;
- let size = '0 KB';
-
- if (dbExists) {
- stats = fs.statSync(DB_FILE);
- // Convert to human-readable size
- const fileSizeInBytes = stats.size;
- const fileSizeInKB = fileSizeInBytes / 1024;
- if (fileSizeInKB < 1024) {
- size = `${fileSizeInKB.toFixed(2)} KB`;
- } else {
- const fileSizeInMB = fileSizeInKB / 1024;
- size = `${fileSizeInMB.toFixed(2)} MB`;
- }
- }
-
- // Get the most recent backup if any
- let lastBackup = null;
- if (fs.existsSync(BACKUPS_DIRECTORY)) {
- const backupFiles = fs.readdirSync(BACKUPS_DIRECTORY)
- .filter(file => file.startsWith('invtrack-backup-'))
- .sort();
-
- if (backupFiles.length > 0) {
- const latestBackupFile = backupFiles[backupFiles.length - 1];
- const backupStats = fs.statSync(path.join(BACKUPS_DIRECTORY, latestBackupFile));
- lastBackup = backupStats.mtime.toISOString();
- }
- }
-
- // In a real application, we would query the database to get record counts
- // For this example, we'll just return placeholder data
- return {
- status: dbExists ? 'healthy' : 'error',
- size,
- location: DB_FILE,
- lastBackup,
- dataCount: {
- inventory: 0, // Would fetch from database in real app
- movements: 0, // Would fetch from database in real app
- suppliers: 0, // Would fetch from database in real app
- users: 0 // Would fetch from database in real app
- }
- };
- } catch (error) {
- console.error('Error getting database info:', error);
- return {
- status: 'error',
- size: '0 KB',
- location: DB_FILE,
- lastBackup: null,
- dataCount: {
- inventory: 0,
- movements: 0,
- suppliers: 0,
- users: 0
- }
- };
- }
-}
-
-/**
- * Synchronize the local database with the remote server
- * @param {Function} progressCallback Optional callback to report sync progress
- * @returns {Promise} Whether sync was successful
- */
-async function syncDatabase(progressCallback = null) {
- // In a real application, this would sync data with a remote server
- // For now, we'll simulate the process with timeouts
-
- try {
- console.log('Starting database synchronization...');
-
- // Simulate sync progress
- const totalSteps = 5;
-
- for (let step = 1; step <= totalSteps; step++) {
- // Calculate progress percentage
- const progress = Math.floor((step / totalSteps) * 100);
-
- // Report progress if a callback is provided
- if (typeof progressCallback === 'function') {
- progressCallback(progress);
- }
-
- console.log(`Sync progress: ${progress}%`);
-
- // Simulate network delay
- await new Promise(resolve => setTimeout(resolve, 1000));
- }
-
- // Update a timestamp file to record successful sync
- const syncTimestampFile = path.join(DATA_DIRECTORY, 'last_sync.txt');
- fs.writeFileSync(syncTimestampFile, new Date().toISOString());
-
- console.log('Database synchronization completed successfully');
- return true;
- } catch (error) {
- console.error('Error synchronizing database:', error);
- return false;
- }
-}
-
-// Export the module
-module.exports = {
- initialize,
- getDb,
- createBackup,
- restoreFromBackup,
- getDatabaseInfo,
- syncDatabase
-};
\ No newline at end of file
diff --git a/electron/icons/app-icon.png b/electron/icons/app-icon.png
deleted file mode 100644
index e69de29b..00000000
diff --git a/electron/icons/app-icon.svg b/electron/icons/app-icon.svg
deleted file mode 100644
index 78c2f9fa..00000000
--- a/electron/icons/app-icon.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/electron/icons/tray-icon.png b/electron/icons/tray-icon.png
deleted file mode 100644
index e69de29b..00000000
diff --git a/electron/icons/tray-icon.svg b/electron/icons/tray-icon.svg
deleted file mode 100644
index 6b4888a0..00000000
--- a/electron/icons/tray-icon.svg
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/electron/index.js b/electron/index.js
deleted file mode 100644
index ccd38286..00000000
--- a/electron/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Electron Application Entry Point
- *
- * This is the bootstrapping file for the Electron application.
- * It sets up the environment and launches the main process.
- */
-
-// Import electron-updater for auto-updates in production
-// This is commented out for now, uncomment and install when ready to implement
-// const { autoUpdater } = require('electron-updater');
-
-// Set environment variables
-process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
-process.env.NODE_ENV = process.env.NODE_ENV || (require('electron').app.isPackaged ? 'production' : 'development');
-
-// Import the main process file
-require('./main');
\ No newline at end of file
diff --git a/electron/ipc-handlers.js b/electron/ipc-handlers.js
deleted file mode 100644
index 7ae5e25c..00000000
--- a/electron/ipc-handlers.js
+++ /dev/null
@@ -1,849 +0,0 @@
-/**
- * IPC Handlers for Electron
- *
- * This module defines all the IPC (Inter-Process Communication) handlers
- * for the Electron application, allowing the renderer process (web app)
- * to safely communicate with the main process (Node.js).
- */
-
-const { ipcMain, BrowserWindow, dialog, app } = require('electron');
-const path = require('path');
-const fs = require('fs');
-const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
-const Excel = require('exceljs');
-const db = require('./db');
-const os = require('os');
-const { createObjectCsvWriter } = require('csv-writer');
-
-// Window Management Handlers
-function handleWindowControls() {
- // Minimize the window
- ipcMain.on('window:minimize', (event) => {
- const win = BrowserWindow.fromWebContents(event.sender);
- if (win) win.minimize();
- });
-
- // Maximize or restore the window
- ipcMain.on('window:maximize', (event) => {
- const win = BrowserWindow.fromWebContents(event.sender);
- if (win) {
- if (win.isMaximized()) {
- win.unmaximize();
- } else {
- win.maximize();
- }
- }
- });
-
- // Close the window
- ipcMain.on('window:close', (event) => {
- const win = BrowserWindow.fromWebContents(event.sender);
- if (win) win.close();
- });
-
- // Check if the window is maximized
- ipcMain.handle('window:is-maximized', (event) => {
- const win = BrowserWindow.fromWebContents(event.sender);
- return win ? win.isMaximized() : false;
- });
-}
-
-// Application Handlers
-function handleAppControls() {
- // Get application version
- ipcMain.handle('app:get-version', () => {
- return app.getVersion();
- });
-
- // Get application data directory
- ipcMain.handle('app:get-data-directory', () => {
- return app.getPath('userData');
- });
-
- // Check for updates (implement with electron-updater in production)
- ipcMain.on('app:check-updates', (event) => {
- // This is a placeholder. In a real application, use electron-updater
- // to check for updates from your update server
- const updateInfo = {
- version: '1.0.1',
- releaseDate: new Date().toISOString(),
- releaseNotes: 'Bug fixes and performance improvements'
- };
-
- event.sender.send('update-available', updateInfo);
-
- // Simulate download completion after a delay
- setTimeout(() => {
- event.sender.send('update-downloaded', updateInfo);
- }, 3000);
- });
-
- // Install update and restart (implement with electron-updater in production)
- ipcMain.on('app:install-update', () => {
- // This is a placeholder. In a real application, use electron-updater
- // to quit and install
- app.relaunch();
- app.quit();
- });
-}
-
-// Document Generation Handlers
-function handleDocumentGeneration() {
- // Generate PDF document
- ipcMain.handle('document:generate-pdf', async (_, data, templateName, options = {}) => {
- try {
- const pdfDoc = await PDFDocument.create();
- const page = pdfDoc.addPage();
- const { width, height } = page.getSize();
-
- const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
- const fontSize = 12;
-
- page.drawText('InvTrack - Generated Report', {
- x: 50,
- y: height - 50,
- size: 24,
- font,
- color: rgb(0, 0, 0),
- });
-
- page.drawText(`Template: ${templateName}`, {
- x: 50,
- y: height - 100,
- size: fontSize,
- font,
- color: rgb(0, 0, 0),
- });
-
- page.drawText(`Date: ${new Date().toLocaleDateString()}`, {
- x: 50,
- y: height - 120,
- size: fontSize,
- font,
- color: rgb(0, 0, 0),
- });
-
- // Output data content
- let y = height - 160;
- if (typeof data === 'string') {
- page.drawText(data, {
- x: 50,
- y,
- size: fontSize,
- font,
- color: rgb(0, 0, 0),
- });
- } else if (Array.isArray(data)) {
- for (const item of data) {
- page.drawText(JSON.stringify(item), {
- x: 50,
- y,
- size: fontSize,
- font,
- color: rgb(0, 0, 0),
- });
- y -= 20;
-
- // Add a new page if we run out of space
- if (y < 50) {
- const newPage = pdfDoc.addPage();
- y = newPage.getSize().height - 50;
- }
- }
- } else if (typeof data === 'object') {
- for (const [key, value] of Object.entries(data)) {
- page.drawText(`${key}: ${value}`, {
- x: 50,
- y,
- size: fontSize,
- font,
- color: rgb(0, 0, 0),
- });
- y -= 20;
-
- // Add a new page if we run out of space
- if (y < 50) {
- const newPage = pdfDoc.addPage();
- y = newPage.getSize().height - 50;
- }
- }
- }
-
- const pdfBytes = await pdfDoc.save();
-
- // Determine the output file path
- const fileName = options.fileName || `${templateName}-${Date.now()}.pdf`;
- const directory = options.directory || app.getPath('downloads');
- const filePath = path.join(directory, fileName);
-
- fs.writeFileSync(filePath, pdfBytes);
-
- return filePath;
- } catch (error) {
- console.error('Error generating PDF:', error);
- return null;
- }
- });
-
- // Export data to Excel
- ipcMain.handle('document:export-excel', async (_, data, options = {}) => {
- try {
- const workbook = new Excel.Workbook();
- const worksheet = workbook.addWorksheet(options.sheetName || 'Data');
-
- if (Array.isArray(data) && data.length > 0) {
- // Create header row from first object's keys
- const headers = Object.keys(data[0]);
- worksheet.columns = headers.map(header => ({
- header,
- key: header,
- width: 20
- }));
-
- // Add data rows
- worksheet.addRows(data);
- }
-
- // Determine the output file path
- const fileName = options.fileName || `Export-${Date.now()}.xlsx`;
- const directory = options.directory || app.getPath('downloads');
- const filePath = path.join(directory, fileName);
-
- await workbook.xlsx.writeFile(filePath);
-
- return filePath;
- } catch (error) {
- console.error('Error exporting to Excel:', error);
- return null;
- }
- });
-
- // Export data to CSV
- ipcMain.handle('document:export-csv', async (_, data, options = {}) => {
- try {
- if (!Array.isArray(data) || data.length === 0) {
- throw new Error('Data must be a non-empty array');
- }
-
- // Determine the output file path
- const fileName = options.fileName || `Export-${Date.now()}.csv`;
- const directory = options.directory || app.getPath('downloads');
- const filePath = path.join(directory, fileName);
-
- // Create CSV header from first object's keys
- const headers = Object.keys(data[0]).map(key => ({
- id: key,
- title: key
- }));
-
- const csvWriter = createObjectCsvWriter({
- path: filePath,
- header: headers
- });
-
- await csvWriter.writeRecords(data);
-
- return filePath;
- } catch (error) {
- console.error('Error exporting to CSV:', error);
- return null;
- }
- });
-}
-
-// Dialog Handlers
-function handleDialogs() {
- // Open file dialog
- ipcMain.handle('dialog:open-file', async (_, options = {}) => {
- const { canceled, filePaths } = await dialog.showOpenDialog({
- title: options.title,
- defaultPath: options.defaultPath || app.getPath('documents'),
- buttonLabel: options.buttonLabel,
- filters: options.filters || [
- { name: 'All Files', extensions: ['*'] }
- ],
- properties: options.properties || ['openFile']
- });
-
- return canceled ? null : filePaths[0];
- });
-
- // Save file dialog
- ipcMain.handle('dialog:save-file', async (_, options = {}) => {
- const { canceled, filePath } = await dialog.showSaveDialog({
- title: options.title,
- defaultPath: options.defaultPath || app.getPath('documents'),
- buttonLabel: options.buttonLabel,
- filters: options.filters || [
- { name: 'All Files', extensions: ['*'] }
- ]
- });
-
- return canceled ? null : filePath;
- });
-}
-
-// Barcode Scanner Handlers
-function handleBarcodeScanner() {
- let scannerWindow = null;
- let cameraActive = false;
- let onScanCallback = null;
-
- // Start the barcode scanner
- ipcMain.handle('start-barcode-scanner', async (event, options = {}) => {
- try {
- const mainWindow = BrowserWindow.fromWebContents(event.sender);
- if (!mainWindow) {
- throw new Error('Parent window not found');
- }
-
- // Set up callback for scan results
- onScanCallback = (result) => {
- if (event.sender.isDestroyed()) return;
- event.sender.send('barcode-scan-result', result);
- };
-
- // Determine scanner type
- const scannerType = options.type || 'auto';
-
- // Create scanner window
- scannerWindow = new BrowserWindow({
- width: 800,
- height: 600,
- parent: mainWindow,
- modal: true,
- show: false,
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
- preload: path.join(__dirname, 'preload.js')
- },
- });
-
- // Load the scanner HTML (using a data URL for simplicity)
- scannerWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(`
-
-
-
-
-
- Barcode Scanner
-
-
-
-
-
-
- Stop Camera
- Manual Entry
- Close
-
-
Scanning...
-
-
-
Enter barcode value manually:
-
-
- Submit
- Cancel
-
-
-
-
-
- `)}`);
-
- // Handle scanner window events
- scannerWindow.once('ready-to-show', () => {
- scannerWindow.show();
- cameraActive = true;
- });
-
- scannerWindow.on('closed', () => {
- cameraActive = false;
- scannerWindow = null;
- });
-
- // Return success
- return { success: true };
- } catch (error) {
- console.error('Error starting barcode scanner:', error);
- return { success: false, error: error.message };
- }
- });
-
- // Stop the barcode scanner
- ipcMain.handle('stop-barcode-scanner', () => {
- try {
- if (scannerWindow) {
- scannerWindow.close();
- scannerWindow = null;
- }
- cameraActive = false;
- onScanCallback = null;
- return { success: true };
- } catch (error) {
- console.error('Error stopping barcode scanner:', error);
- return { success: false, error: error.message };
- }
- });
-
- // Handle result from the scanner window
- ipcMain.on('barcode-scanner:result', (_, result) => {
- if (onScanCallback) {
- onScanCallback(result);
- }
-
- // Optionally close the scanner window after a successful scan
- if (scannerWindow) {
- setTimeout(() => {
- if (scannerWindow) {
- scannerWindow.close();
- scannerWindow = null;
- }
- }, 500);
- }
- });
-
- // Monitor scanner status
- ipcMain.on('barcode-scanner:camera-started', () => {
- cameraActive = true;
- });
-
- ipcMain.on('barcode-scanner:camera-stopped', () => {
- cameraActive = false;
- });
-
- ipcMain.on('barcode-scanner:camera-error', (_, errorMessage) => {
- console.error('Camera error:', errorMessage);
- cameraActive = false;
- });
-
- // Generate barcodes and QR codes
- ipcMain.handle('barcode:generate', async (_, options) => {
- try {
- // In a real application, this would use a barcode generation library
- // For now, we'll just simulate success
- return {
- success: true,
- message: `Generated ${options.type} with value ${options.value}`
- };
- } catch (error) {
- console.error('Error generating barcode:', error);
- return { success: false, error: error.message };
- }
- });
-}
-
-// Database Handlers
-function handleDatabase() {
- // Create database backup
- ipcMain.handle('create-database-backup', async (_, options = {}) => {
- try {
- const directory = options.directory || app.getPath('documents');
- const backupPath = await db.createBackup(directory);
- return { success: true, path: backupPath };
- } catch (error) {
- console.error('Error creating database backup:', error);
- return { success: false, error: error.message };
- }
- });
-
- // Restore database from backup
- ipcMain.handle('restore-database-backup', async (_, backupPath) => {
- try {
- await db.restoreFromBackup(backupPath);
- return { success: true };
- } catch (error) {
- console.error('Error restoring database:', error);
- return { success: false, error: error.message };
- }
- });
-
- // For backward compatibility
- ipcMain.handle('database:create-backup', async (_, options = {}) => {
- try {
- const directory = options.directory || app.getPath('documents');
- const backupPath = await db.createBackup(directory);
- return backupPath;
- } catch (error) {
- console.error('Error creating database backup:', error);
- return null;
- }
- });
-
- // For backward compatibility
- ipcMain.handle('database:restore', async (_, backupPath) => {
- try {
- await db.restoreFromBackup(backupPath);
- return true;
- } catch (error) {
- console.error('Error restoring database:', error);
- return false;
- }
- });
-
- // Get database information
- ipcMain.handle('get-database-info', async () => {
- try {
- const info = await db.getDatabaseInfo();
- return info;
- } catch (error) {
- console.error('Error getting database info:', error);
- return {
- status: 'error',
- size: '0 KB',
- location: 'unknown',
- lastBackup: null,
- dataCount: {
- inventory: 0,
- movements: 0,
- suppliers: 0,
- users: 0
- },
- error: error.message
- };
- }
- });
-
- // Synchronize database with server
- ipcMain.handle('sync-database', async () => {
- try {
- // Set up a progress reporter function
- const reportProgress = (progress) => {
- // Send progress updates to all windows
- BrowserWindow.getAllWindows().forEach(win => {
- if (win && !win.isDestroyed()) {
- win.webContents.send('sync-progress', progress);
- }
- });
- };
-
- // Start the sync process
- const result = await db.syncDatabase(reportProgress);
-
- // Notify all windows of the database status change
- BrowserWindow.getAllWindows().forEach(async (win) => {
- if (win && !win.isDestroyed()) {
- // Get updated info to send with the notification
- const updatedInfo = await db.getDatabaseInfo();
- win.webContents.send('database-status-changed', updatedInfo);
- }
- });
-
- return result;
- } catch (error) {
- console.error('Error syncing database:', error);
- throw error;
- }
- });
-
- // Network connectivity check
- ipcMain.handle('check-network-status', () => {
- // This is a simple implementation - for production, you might want
- // to actually test connectivity to your server
- return !app.isPackaged || require('electron-is-dev') ? true : require('electron-util').isOnline();
- });
-}
-
-/**
- * Register all IPC handlers with the main process
- */
-function registerIpcHandlers() {
- handleWindowControls();
- handleAppControls();
- handleDocumentGeneration();
- handleDialogs();
- handleBarcodeScanner();
- handleDatabase();
-}
-
-module.exports = {
- registerIpcHandlers
-};
\ No newline at end of file
diff --git a/electron/main.js b/electron/main.js
deleted file mode 100644
index ae3a71ba..00000000
--- a/electron/main.js
+++ /dev/null
@@ -1,495 +0,0 @@
-/**
- * Electron Main Process
- *
- * This is the entry point for the Electron application.
- * It creates the browser window, sets up the application menu,
- * and handles various system events.
- */
-
-const { app, BrowserWindow, Menu, Tray, shell, dialog } = require('electron');
-const path = require('path');
-const url = require('url');
-const { is } = require('electron-util');
-const fs = require('fs');
-const log = require('electron-log');
-const { registerIpcHandlers } = require('./ipc-handlers');
-
-// Import our services
-const db = require('./db');
-const databaseService = require('./database-service');
-const syncService = require('./sync-service');
-
-// Keep a global reference of objects to prevent garbage collection
-let mainWindow;
-let tray;
-let isQuitting = false;
-
-// Check if we're in development or production
-const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
-
-/**
- * Check for updates (placeholder implementation)
- * In a real application, use electron-updater
- */
-function checkForUpdates() {
- if (isDev) {
- console.log('Skipping update check in development mode');
- return;
- }
-
- console.log('Checking for updates...');
- // This is where you would implement the actual update check
- // using electron-updater
-}
-
-/**
- * Create the main application window
- */
-function createMainWindow() {
- const windowConfig = {
- width: 1200,
- height: 800,
- minWidth: 800,
- minHeight: 600,
- backgroundColor: '#f5f5f5',
- webPreferences: {
- nodeIntegration: false,
- contextIsolation: true,
- preload: path.join(__dirname, 'preload.js'),
- spellcheck: true,
- devTools: isDev
- },
- // Remove the default frame in favor of our custom one
- frame: false,
- // Disable the traffic lights on macOS
- titleBarStyle: 'hidden',
- show: false
- };
-
- mainWindow = new BrowserWindow(windowConfig);
-
- // Load the application
- const startUrl = isDev
- ? 'http://localhost:5000' // Development server
- : url.format({
- pathname: path.join(__dirname, '../dist/index.html'),
- protocol: 'file:',
- slashes: true
- });
-
- mainWindow.loadURL(startUrl);
-
- // Show window when it's ready to avoid flickering
- mainWindow.once('ready-to-show', () => {
- mainWindow.show();
-
- // Check for updates on startup
- checkForUpdates();
- });
-
- // Handle window close events
- mainWindow.on('close', (event) => {
- if (!isQuitting) {
- // Prevent the window from closing
- event.preventDefault();
-
- // Ask the user if they want to quit
- dialog.showMessageBox(mainWindow, {
- type: 'question',
- buttons: ['Yes', 'No'],
- defaultId: 0,
- title: 'Confirm',
- message: 'Are you sure you want to quit?',
- detail: 'Any unsaved changes will be lost.'
- }).then(({ response }) => {
- if (response === 0) {
- isQuitting = true;
- mainWindow.close();
- }
- });
- }
- });
-
- // Emitted when the window is closed
- mainWindow.on('closed', () => {
- mainWindow = null;
- });
-
- // Open external links in the default browser
- mainWindow.webContents.setWindowOpenHandler(({ url }) => {
- shell.openExternal(url);
- return { action: 'deny' };
- });
-
- // Prevent navigation to non-local URLs
- mainWindow.webContents.on('will-navigate', (event, url) => {
- if (!url.startsWith('file://') && !url.startsWith('http://localhost')) {
- event.preventDefault();
- shell.openExternal(url);
- }
- });
-
- // Set up the application menu
- createAppMenu();
-
- // Set up the system tray icon
- createTray();
-
- // Return the window for further use
- return mainWindow;
-}
-
-/**
- * Create the application menu
- */
-function createAppMenu() {
- const isMac = process.platform === 'darwin';
-
- const template = [
- // App menu (macOS only)
- ...(isMac ? [{
- label: app.name,
- submenu: [
- { role: 'about' },
- { type: 'separator' },
- { role: 'services' },
- { type: 'separator' },
- { role: 'hide' },
- { role: 'hideOthers' },
- { role: 'unhide' },
- { type: 'separator' },
- { role: 'quit' }
- ]
- }] : []),
-
- // File menu
- {
- label: 'File',
- submenu: [
- {
- label: 'Export Data',
- submenu: [
- {
- label: 'Export to Excel',
- click: () => {
- if (mainWindow) {
- mainWindow.webContents.send('menu:export-excel');
- }
- }
- },
- {
- label: 'Export to CSV',
- click: () => {
- if (mainWindow) {
- mainWindow.webContents.send('menu:export-csv');
- }
- }
- },
- {
- label: 'Export to PDF',
- click: () => {
- if (mainWindow) {
- mainWindow.webContents.send('menu:export-pdf');
- }
- }
- }
- ]
- },
- {
- label: 'Database',
- submenu: [
- {
- label: 'Create Backup',
- click: async () => {
- try {
- const { filePath } = await dialog.showSaveDialog({
- title: 'Save Database Backup',
- defaultPath: path.join(app.getPath('documents'), `invtrack-backup-${new Date().toISOString().replace(/[:.]/g, '-')}.db`),
- buttonLabel: 'Save Backup',
- filters: [
- { name: 'Database Files', extensions: ['db'] },
- { name: 'All Files', extensions: ['*'] }
- ]
- });
-
- if (filePath) {
- const backupDir = path.dirname(filePath);
- const backupPath = await db.createBackup(backupDir);
- dialog.showMessageBox({
- type: 'info',
- title: 'Backup Created',
- message: 'Database backup created successfully',
- detail: `Backup saved to: ${backupPath}`
- });
- }
- } catch (error) {
- dialog.showErrorBox('Backup Error', `Failed to create backup: ${error.message}`);
- }
- }
- },
- {
- label: 'Restore from Backup',
- click: async () => {
- try {
- const { filePaths, canceled } = await dialog.showOpenDialog({
- title: 'Select Database Backup',
- defaultPath: app.getPath('documents'),
- buttonLabel: 'Restore',
- filters: [
- { name: 'Database Files', extensions: ['db'] },
- { name: 'All Files', extensions: ['*'] }
- ],
- properties: ['openFile']
- });
-
- if (!canceled && filePaths.length > 0) {
- const { response } = await dialog.showMessageBox({
- type: 'warning',
- title: 'Confirm Restore',
- message: 'Are you sure you want to restore the database?',
- detail: 'This will overwrite your current data. This operation cannot be undone.',
- buttons: ['Cancel', 'Restore'],
- defaultId: 0,
- cancelId: 0
- });
-
- if (response === 1) {
- await db.restoreFromBackup(filePaths[0]);
- dialog.showMessageBox({
- type: 'info',
- title: 'Restore Complete',
- message: 'Database restored successfully',
- detail: 'The application will now restart to apply the changes.'
- });
-
- // Restart the application
- app.relaunch();
- app.exit();
- }
- }
- } catch (error) {
- dialog.showErrorBox('Restore Error', `Failed to restore database: ${error.message}`);
- }
- }
- }
- ]
- },
- { type: 'separator' },
- isMac ? { role: 'close' } : { role: 'quit' }
- ]
- },
-
- // Edit menu
- {
- label: 'Edit',
- submenu: [
- { role: 'undo' },
- { role: 'redo' },
- { type: 'separator' },
- { role: 'cut' },
- { role: 'copy' },
- { role: 'paste' },
- ...(isMac ? [
- { role: 'pasteAndMatchStyle' },
- { role: 'delete' },
- { role: 'selectAll' },
- { type: 'separator' },
- {
- label: 'Speech',
- submenu: [
- { role: 'startSpeaking' },
- { role: 'stopSpeaking' }
- ]
- }
- ] : [
- { role: 'delete' },
- { type: 'separator' },
- { role: 'selectAll' }
- ])
- ]
- },
-
- // View menu
- {
- label: 'View',
- submenu: [
- { role: 'reload' },
- { role: 'forceReload' },
- ...(isDev ? [{ role: 'toggleDevTools' }] : []),
- { type: 'separator' },
- { role: 'resetZoom' },
- { role: 'zoomIn' },
- { role: 'zoomOut' },
- { type: 'separator' },
- { role: 'togglefullscreen' }
- ]
- },
-
- // Window menu
- {
- label: 'Window',
- submenu: [
- { role: 'minimize' },
- { role: 'zoom' },
- ...(isMac ? [
- { type: 'separator' },
- { role: 'front' },
- { type: 'separator' },
- { role: 'window' }
- ] : [
- { role: 'close' }
- ])
- ]
- },
-
- // Help menu
- {
- role: 'help',
- submenu: [
- {
- label: 'Learn More',
- click: async () => {
- await shell.openExternal('https://electronjs.org');
- }
- },
- {
- label: 'Documentation',
- click: async () => {
- await shell.openExternal('https://example.com/docs');
- }
- },
- { type: 'separator' },
- {
- label: 'Check for Updates',
- click: () => {
- checkForUpdates();
- }
- },
- { type: 'separator' },
- {
- label: 'About',
- click: () => {
- dialog.showMessageBox({
- title: 'About InvTrack',
- message: 'InvTrack - Inventory Management System',
- detail: `Version: ${app.getVersion()}\nElectron: ${process.versions.electron}\nNode: ${process.versions.node}\nChrome: ${process.versions.chrome}`
- });
- }
- }
- ]
- }
- ];
-
- const menu = Menu.buildFromTemplate(template);
- Menu.setApplicationMenu(menu);
-}
-
-/**
- * Create the system tray icon
- */
-function createTray() {
- const iconPath = path.join(__dirname, '../icons/tray-icon.png');
- tray = new Tray(iconPath);
-
- const contextMenu = Menu.buildFromTemplate([
- {
- label: 'Open InvTrack',
- click: () => {
- if (mainWindow) {
- mainWindow.show();
- }
- }
- },
- { type: 'separator' },
- {
- label: 'Check for Updates',
- click: () => {
- checkForUpdates();
- }
- },
- { type: 'separator' },
- {
- label: 'Quit',
- click: () => {
- isQuitting = true;
- app.quit();
- }
- }
- ]);
-
- tray.setToolTip('InvTrack - Inventory Management System');
- tray.setContextMenu(contextMenu);
-
- tray.on('click', () => {
- if (mainWindow) {
- if (mainWindow.isVisible()) {
- if (mainWindow.isMinimized()) {
- mainWindow.restore();
- }
- mainWindow.focus();
- } else {
- mainWindow.show();
- }
- }
- });
-}
-
-// This method will be called when Electron has finished
-// initialization and is ready to create browser windows.
-app.whenReady().then(async () => {
- try {
- // Initialize the database modules
- log.info('Initializing database services...');
- db.initialize();
-
- // Initialize the SQL database service
- await databaseService.initialize();
-
- // Initialize the sync service with server URL
- const serverUrl = isDev
- ? 'http://localhost:5000'
- : process.env.SERVER_URL || 'https://api.invtrack.com';
-
- syncService.initialize(serverUrl, {
- syncOnStartup: !isDev, // Only sync on startup in production
- autoSync: !isDev, // Only auto-sync in production
- syncInterval: 5 * 60 * 1000 // 5 minutes
- });
-
- // Register all IPC handlers
- log.info('Registering IPC handlers...');
- registerIpcHandlers();
-
- // Create the main window
- log.info('Creating main application window...');
- createMainWindow();
-
- // On macOS, re-create a window when the dock icon is clicked
- app.on('activate', () => {
- if (BrowserWindow.getAllWindows().length === 0) {
- createMainWindow();
- }
- });
-
- log.info('Application initialization complete');
- } catch (error) {
- log.error('Error during application initialization:', error);
- dialog.showErrorBox(
- 'Initialization Error',
- `Failed to initialize application: ${error.message}`
- );
- }
-});
-
-// Quit when all windows are closed, except on macOS where it's common
-// for applications to stay running until the user explicitly quits
-app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') {
- app.quit();
- }
-});
-
-// On macOS, force quit when Command+Q is pressed
-app.on('before-quit', () => {
- isQuitting = true;
-});
\ No newline at end of file
diff --git a/electron/preload.js b/electron/preload.js
deleted file mode 100644
index 649105ce..00000000
--- a/electron/preload.js
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Preload Script for Electron
- *
- * This script runs in the context of the renderer process before the web page loads.
- * It provides a safe way to expose specific Node.js functionality to the renderer
- * process via contextBridge.
- */
-
-const { contextBridge, ipcRenderer } = require('electron');
-const os = require('os');
-
-/**
- * Expose selected APIs from Electron to the renderer process.
- * These APIs are accessible via `window.electron` in the renderer.
- */
-contextBridge.exposeInMainWorld('electron', {
- /**
- * Send a message to the main process via IPC
- * @param {string} channel - The channel name
- * @param {any[]} args - Arguments to pass to the main process
- */
- send: (channel, ...args) => {
- // Whitelist of channels that are allowed to be sent from the renderer
- const validSendChannels = [
- 'window:minimize',
- 'window:maximize',
- 'window:close',
- 'app:check-updates',
- 'app:install-update'
- ];
-
- if (validSendChannels.includes(channel)) {
- ipcRenderer.send(channel, ...args);
- }
- },
-
- /**
- * Set up a listener for messages from the main process
- * @param {string} channel - The channel to listen on
- * @param {Function} func - Callback function
- * @returns {Function} - Function to remove the listener
- */
- on: (channel, func) => {
- // Whitelist of channels that are allowed to be received by the renderer
- const validReceiveChannels = [
- 'update-available',
- 'update-downloaded',
- 'menu:export-excel',
- 'menu:export-csv',
- 'menu:export-pdf'
- ];
-
- if (validReceiveChannels.includes(channel)) {
- // Add the event listener
- const subscription = (event, ...args) => func(...args);
- ipcRenderer.on(channel, subscription);
-
- // Return a function to clean up the event listener
- return () => {
- ipcRenderer.removeListener(channel, subscription);
- };
- }
-
- // Return a no-op function if the channel is not valid
- return () => {};
- },
-
- /**
- * Invoke a method in the main process and get a Promise for the result
- * @param {string} channel - The channel name
- * @param {any[]} args - Arguments to pass to the main process
- * @returns {Promise} - Promise that resolves with the result
- */
- invoke: (channel, ...args) => {
- // Whitelist of channels that are allowed to be invoked from the renderer
- const validInvokeChannels = [
- 'window:is-maximized',
- 'app:get-version',
- 'app:get-data-directory',
- 'document:generate-pdf',
- 'document:export-excel',
- 'document:export-csv',
- 'dialog:open-file',
- 'dialog:save-file',
- 'barcode:scan',
- 'database:create-backup',
- 'database:restore'
- ];
-
- if (validInvokeChannels.includes(channel)) {
- return ipcRenderer.invoke(channel, ...args);
- }
-
- // Return a rejected promise if the channel is not valid
- return Promise.reject(new Error(`Unauthorized IPC invoke: ${channel}`));
- },
-
- // System information API
- system: {
- /**
- * Get information about the system
- * @returns {Object} - Object containing system information
- */
- getInfo: () => ({
- platform: process.platform,
- arch: process.arch,
- version: process.getSystemVersion(),
- hostname: os.hostname(),
- cpus: os.cpus().length,
- memory: {
- total: os.totalmem(),
- free: os.freemem()
- }
- })
- }
-});
\ No newline at end of file
diff --git a/electron/sync-service.js b/electron/sync-service.js
deleted file mode 100644
index d2ac1d25..00000000
--- a/electron/sync-service.js
+++ /dev/null
@@ -1,426 +0,0 @@
-/**
- * Electron Sync Service
- *
- * Handles synchronization between the local SQLite database
- * and the remote server.
- */
-
-const log = require('electron-log');
-const { ipcMain } = require('electron');
-const databaseService = require('./database-service');
-const WebSocket = require('ws');
-
-// Configuration
-let syncConfig = {
- autoSync: true,
- syncInterval: 5 * 60 * 1000, // 5 minutes
- backgroundSync: true,
- syncOnStartup: true,
- syncOnNetworkChange: true,
- conflictResolution: 'server', // 'server', 'client', or 'manual'
- maxRetries: 3,
- serverUrl: '',
- compressionThreshold: 10240, // 10KB
- batchSize: 100
-};
-
-// State
-let wsConnection = null;
-let syncTimer = null;
-let offlineQueue = [];
-let isOnline = true;
-let syncInProgress = false;
-let syncListeners = [];
-let lastSyncStatus = null;
-
-/**
- * Initialize the sync service
- * @param {string} serverUrl Remote server URL
- * @param {Object} config Optional configuration overrides
- */
-function initialize(serverUrl, config = {}) {
- syncConfig = { ...syncConfig, ...config, serverUrl };
-
- // Register IPC handlers
- registerIpcHandlers();
-
- // Set up automatic syncing if configured
- if (syncConfig.autoSync) {
- startAutoSync();
- }
-
- // Set up network change detection
- setupNetworkDetection();
-
- // Perform initial sync if configured
- if (syncConfig.syncOnStartup) {
- performSync();
- }
-
- log.info('Sync service initialized with server:', serverUrl);
-}
-
-/**
- * Register IPC handlers for sync operations
- */
-function registerIpcHandlers() {
- ipcMain.handle('sync:start', async () => {
- return await performSync();
- });
-
- ipcMain.handle('sync:getStatus', () => {
- return getSyncStatus();
- });
-
- ipcMain.handle('sync:getConfig', () => {
- return syncConfig;
- });
-
- ipcMain.handle('sync:updateConfig', (_, newConfig) => {
- const oldAutoSync = syncConfig.autoSync;
-
- // Update config
- syncConfig = { ...syncConfig, ...newConfig };
-
- // Handle auto-sync state change
- if (!oldAutoSync && syncConfig.autoSync) {
- startAutoSync();
- } else if (oldAutoSync && !syncConfig.autoSync) {
- stopAutoSync();
- }
-
- return syncConfig;
- });
-
- ipcMain.handle('sync:clearQueue', () => {
- const queueSize = offlineQueue.length;
- offlineQueue = [];
- return { cleared: queueSize };
- });
-
- log.info('Sync IPC handlers registered');
-}
-
-/**
- * Setup network status detection
- */
-function setupNetworkDetection() {
- // Check online status initially
- isOnline = navigator ? navigator.onLine : true;
-
- // In Electron main process, we need a different approach
- // since navigator is not available. This is a simplified version.
- setInterval(() => {
- // Use a simple ping to check connectivity
- const newOnlineStatus = checkConnectivity();
-
- if (newOnlineStatus !== isOnline) {
- const prevStatus = isOnline;
- isOnline = newOnlineStatus;
-
- log.info(`Network status changed: ${prevStatus ? 'online' : 'offline'} -> ${isOnline ? 'online' : 'offline'}`);
-
- // If we just came online and have sync on network change enabled
- if (isOnline && !prevStatus && syncConfig.syncOnNetworkChange) {
- log.info('Reconnected to network, triggering sync');
- performSync();
- }
- }
- }, 30000); // Check every 30 seconds
-}
-
-/**
- * Check connectivity to the server
- * @returns {boolean} Whether the app is online
- */
-function checkConnectivity() {
- // This would be implemented to check connectivity to the server
- // For now, we'll just return true as a placeholder
- return true;
-}
-
-/**
- * Start automatic sync process
- */
-function startAutoSync() {
- if (syncTimer) {
- clearInterval(syncTimer);
- }
-
- syncTimer = setInterval(() => {
- if (isOnline && !syncInProgress) {
- performSync();
- }
- }, syncConfig.syncInterval);
-
- log.info(`Auto sync started with interval of ${syncConfig.syncInterval / 1000} seconds`);
-}
-
-/**
- * Stop automatic sync process
- */
-function stopAutoSync() {
- if (syncTimer) {
- clearInterval(syncTimer);
- syncTimer = null;
- log.info('Auto sync stopped');
- }
-}
-
-/**
- * Perform synchronization with server
- * @returns {Promise} Sync result
- */
-async function performSync() {
- if (syncInProgress) {
- log.info('Sync already in progress, skipping');
- return { success: false, message: 'Sync already in progress' };
- }
-
- syncInProgress = true;
- notifySyncListeners('started');
-
- try {
- // Check if we have a websocket connection
- ensureWebSocketConnection();
-
- // First, try to send any queued offline changes
- if (offlineQueue.length > 0 && isOnline) {
- await processSyncQueue();
- }
-
- // Then perform normal sync
- const result = await databaseService.syncWithRemoteServer(
- (progress) => notifySyncListeners('progress', progress)
- );
-
- syncInProgress = false;
- lastSyncStatus = {
- timestamp: new Date().toISOString(),
- success: result.success,
- details: result
- };
-
- notifySyncListeners('completed', result);
- log.info('Sync completed', result);
- return result;
- } catch (error) {
- syncInProgress = false;
- lastSyncStatus = {
- timestamp: new Date().toISOString(),
- success: false,
- error: error.message
- };
-
- notifySyncListeners('error', { error: error.message });
- log.error('Sync failed', error);
- return { success: false, error: error.message };
- }
-}
-
-/**
- * Get current sync status
- * @returns {Object} Sync status
- */
-function getSyncStatus() {
- return {
- inProgress: syncInProgress,
- isOnline,
- queueSize: offlineQueue.length,
- lastSync: lastSyncStatus,
- config: syncConfig
- };
-}
-
-/**
- * Ensure WebSocket connection is established
- */
-function ensureWebSocketConnection() {
- if (!isOnline || !syncConfig.serverUrl) {
- return;
- }
-
- // If already connected, do nothing
- if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
- return;
- }
-
- // If connecting, do nothing
- if (wsConnection && wsConnection.readyState === WebSocket.CONNECTING) {
- return;
- }
-
- // Close any existing connection
- if (wsConnection) {
- try {
- wsConnection.close();
- } catch (error) {
- log.warn('Error closing WebSocket connection:', error);
- }
- }
-
- // Determine the WebSocket URL
- let wsUrl = syncConfig.serverUrl.replace(/^http/, 'ws');
- if (!wsUrl.endsWith('/')) {
- wsUrl += '/';
- }
- wsUrl += 'sync';
-
- // Create new connection
- try {
- wsConnection = new WebSocket(wsUrl);
-
- wsConnection.on('open', () => {
- log.info('WebSocket connection established');
-
- // Send device info
- wsConnection.send(JSON.stringify({
- type: 'capabilities',
- payload: {
- platform: process.platform,
- osVersion: process.getSystemVersion(),
- appVersion: process.env.npm_package_version || '1.0.0',
- isElectron: true,
- supportsCompression: true,
- deviceId: getDeviceId()
- }
- }));
- });
-
- wsConnection.on('message', (data) => {
- handleWebSocketMessage(data);
- });
-
- wsConnection.on('close', () => {
- log.info('WebSocket connection closed');
- });
-
- wsConnection.on('error', (error) => {
- log.error('WebSocket error:', error);
- });
- } catch (error) {
- log.error('Failed to establish WebSocket connection:', error);
- }
-}
-
-/**
- * Get a unique device ID
- * @returns {string} Device ID
- */
-function getDeviceId() {
- // In a real app, we would generate and persist a unique device ID
- // For now, we'll just use a placeholder
- return 'electron-app-' + Math.random().toString(36).substr(2, 9);
-}
-
-/**
- * Handle incoming WebSocket messages
- * @param {string|Buffer} data Message data
- */
-function handleWebSocketMessage(data) {
- try {
- const message = JSON.parse(data.toString());
-
- switch (message.type) {
- case 'sync_request':
- // Server is requesting a sync
- performSync();
- break;
-
- case 'data_change':
- // Server is notifying about a data change
- // We would apply this change to our local database
- log.info('Received data change notification:', message.payload.entity);
- break;
-
- case 'heartbeat':
- // Respond to heartbeat
- if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
- wsConnection.send(JSON.stringify({
- type: 'heartbeat',
- timestamp: new Date().toISOString()
- }));
- }
- break;
-
- default:
- log.warn('Unhandled WebSocket message type:', message.type);
- }
- } catch (error) {
- log.error('Error handling WebSocket message:', error);
- }
-}
-
-/**
- * Process the offline sync queue
- * @returns {Promise} Whether processing was successful
- */
-async function processSyncQueue() {
- if (offlineQueue.length === 0) {
- return true;
- }
-
- log.info(`Processing offline sync queue: ${offlineQueue.length} items`);
-
- // In a real implementation, we would actually send these changes to the server
- // For now, we'll just simulate success
-
- // Clear the queue after "processing"
- offlineQueue = [];
-
- return true;
-}
-
-/**
- * Add a sync operation to the offline queue
- * @param {string} operation Type of operation
- * @param {string} entity Entity type
- * @param {Object} data Operation data
- */
-function addToOfflineQueue(operation, entity, data) {
- offlineQueue.push({
- operation,
- entity,
- data,
- timestamp: new Date().toISOString()
- });
-
- log.info(`Added ${operation} on ${entity} to offline queue. Queue size: ${offlineQueue.length}`);
-}
-
-/**
- * Register a listener for sync events
- * @param {Function} listener Event listener
- * @returns {Function} Function to remove the listener
- */
-function addSyncListener(listener) {
- syncListeners.push(listener);
-
- return () => {
- syncListeners = syncListeners.filter(l => l !== listener);
- };
-}
-
-/**
- * Notify all sync listeners of an event
- * @param {string} event Event name
- * @param {any} data Event data
- */
-function notifySyncListeners(event, data = null) {
- syncListeners.forEach(listener => {
- try {
- listener(event, data);
- } catch (error) {
- log.error('Error in sync listener:', error);
- }
- });
-}
-
-// Export the public API
-module.exports = {
- initialize,
- performSync,
- getSyncStatus,
- addSyncListener,
- addToOfflineQueue,
-};
\ No newline at end of file
diff --git a/generated-icon.png b/generated-icon.png
deleted file mode 100644
index a39e0184b2d6068d9b9ce35b3818dadb023fb9c8..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 602988
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