commit ba255ebd90041fb058595348305768806ea6281b Author: srinidhi9659 Date: Thu Aug 28 19:26:17 2025 +0530 First template code generated from replit.com diff --git a/.local/state/replit/agent/.agent_state_190d0fa80990835caa0d968e1d48188b9e059899.bin b/.local/state/replit/agent/.agent_state_190d0fa80990835caa0d968e1d48188b9e059899.bin new file mode 100644 index 0000000..e4fbcae Binary files /dev/null and b/.local/state/replit/agent/.agent_state_190d0fa80990835caa0d968e1d48188b9e059899.bin differ diff --git a/.local/state/replit/agent/.agent_state_39c57897e7331731812b00de8364898476b571ad.bin b/.local/state/replit/agent/.agent_state_39c57897e7331731812b00de8364898476b571ad.bin new file mode 100644 index 0000000..a92d62e Binary files /dev/null and b/.local/state/replit/agent/.agent_state_39c57897e7331731812b00de8364898476b571ad.bin differ diff --git a/.local/state/replit/agent/.agent_state_62321dcf394689d1acfc77d7b34e397806d9e9fe.bin b/.local/state/replit/agent/.agent_state_62321dcf394689d1acfc77d7b34e397806d9e9fe.bin new file mode 100644 index 0000000..23744b1 Binary files /dev/null and b/.local/state/replit/agent/.agent_state_62321dcf394689d1acfc77d7b34e397806d9e9fe.bin differ diff --git a/.local/state/replit/agent/.agent_state_fd25f21bafe89c04f466ebd7613e42d941276efc.bin b/.local/state/replit/agent/.agent_state_fd25f21bafe89c04f466ebd7613e42d941276efc.bin new file mode 100644 index 0000000..23732bc Binary files /dev/null and b/.local/state/replit/agent/.agent_state_fd25f21bafe89c04f466ebd7613e42d941276efc.bin differ diff --git a/.local/state/replit/agent/.agent_state_main.bin b/.local/state/replit/agent/.agent_state_main.bin new file mode 100644 index 0000000..8d06ef7 Binary files /dev/null and b/.local/state/replit/agent/.agent_state_main.bin differ diff --git a/.local/state/replit/agent/.latest.json b/.local/state/replit/agent/.latest.json new file mode 100644 index 0000000..6a46732 --- /dev/null +++ b/.local/state/replit/agent/.latest.json @@ -0,0 +1 @@ +{"latest": "main"} \ No newline at end of file diff --git a/.local/state/replit/agent/design_reference/93a2f8a6-3d87-4608-bd25-26e02e169e6f/4ecae5fc-ef0d-4456-a812-a381c6bb70ed.html b/.local/state/replit/agent/design_reference/93a2f8a6-3d87-4608-bd25-26e02e169e6f/4ecae5fc-ef0d-4456-a812-a381c6bb70ed.html new file mode 100644 index 0000000..6395959 --- /dev/null +++ b/.local/state/replit/agent/design_reference/93a2f8a6-3d87-4608-bd25-26e02e169e6f/4ecae5fc-ef0d-4456-a812-a381c6bb70ed.html @@ -0,0 +1,543 @@ + + + + + WarehouseTrack Pro - Inventory & Transportation Management + + + + + + + + +
+
+
+
+ +

WarehouseTrack Pro

+
+
+ +
+ +
+ +
+
+
+
+ + + + + + + +
+ +
+
+ +
+

Low Stock Alert

+

5 items are running low on inventory

+
+
+ +
+ + + +
+

Quick Actions

+
+ + + + + + + +
+
+ + + +
+
+
+

Stock Overview

+ +
+ +
+
+
+
+

Total Items

+

1,247

+
+ +
+
+ +
+
+
+

In Stock

+

1,089

+
+ +
+
+ +
+
+
+

Low Stock

+

23

+
+ +
+
+ +
+
+
+

Out of Stock

+

8

+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + + +
+
+

Recent Activity

+ + +
+
+
+ +
+
+

Stock Added

+

50 units of Safety Helmets (SKU: SH-001)

+

2 minutes ago

+
+
+ +
+
+ +
+
+

Shipment Created

+

Delivery note #DN-2024-001 generated

+

15 minutes ago

+
+
+ +
+
+ +
+
+

Low Stock Alert

+

Work Gloves (SKU: WG-003) below minimum threshold

+

1 hour ago

+
+
+
+ +
+
+ + + +
+
+

Document Templates

+

Generate transportation and inventory documents quickly using pre-configured templates

+ +
+ + + + + + + + + + +
+ +
+
+ + + +
+
+
+

Current Inventory

+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SKUProduct NameCurrent StockMin ThresholdStatusActions
SH-001Safety Helmets - White + 125 + units + 20 + In Stock + +
+ + + +
+
WG-003Work Gloves - Medium + 8 + pairs + 15 + Low Stock + +
+ + + +
+
HV-005Hi-Vis Vests - Large + 0 + units + 10 + Out of Stock + +
+ + + +
+
+
+ +
+

Showing 1-3 of 1,247 items

+
+ + +
+
+
+
+ + + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/.local/state/replit/agent/design_reference/93a2f8a6-3d87-4608-bd25-26e02e169e6f/a383d97c-de23-4557-8763-0320f66fa55d.html b/.local/state/replit/agent/design_reference/93a2f8a6-3d87-4608-bd25-26e02e169e6f/a383d97c-de23-4557-8763-0320f66fa55d.html new file mode 100644 index 0000000..e83489f --- /dev/null +++ b/.local/state/replit/agent/design_reference/93a2f8a6-3d87-4608-bd25-26e02e169e6f/a383d97c-de23-4557-8763-0320f66fa55d.html @@ -0,0 +1,543 @@ + + + + + SkillHub - Find Local Classes & Workshops + + + + + + + + +
+
+
+
+

SkillHub

+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + +
+ +
+ + + + +
+
+
+
+ + + +
+
+

Popular Categories

+
+ +
+
+ +
+ Tech Skills +

120+ classes

+
+ +
+
+ +
+ Business +

85+ classes

+
+ +
+
+ +
+ Creative +

65+ classes

+
+ +
+
+ +
+ Trades +

45+ classes

+
+ +
+
+ +
+ Healthcare +

38+ classes

+
+ +
+
+ +
+ Languages +

52+ classes

+
+ +
+
+
+ + + +
+
+
+

Featured Classes

+ +
+ +
+ +
+
+
+

+ Web Development Bootcamp +

+

+ TechSpace Academy +

+
+ + Downtown Seattle, WA + + + 12 weeks +
+
+
+
+ + 3 spots left + +
+
$2,499
+
+
+ +
+
+
+ + + + + +
+ 4.8 (124 reviews) +
+
+ Starts Jan 15 +
+
+ +
+ + + +
+
+ +
+
+
+

+ Digital Marketing Fundamentals +

+

+ Marketing Pro Institute +

+
+ + Capitol Hill, Seattle + + + 6 weeks +
+
+
+
+ + 8 spots left + +
+
$899
+
+
+ +
+
+
+ + + + + +
+ 4.6 (89 reviews) +
+
+ Starts Jan 22 +
+
+ +
+ + + +
+
+ +
+
+
+

+ Certified Nursing Assistant +

+

+ Seattle Health Training Center +

+
+ + Bellevue, WA + + + 8 weeks +
+
+
+
+ + Almost full + +
+
$1,299
+
+
+ +
+
+
+ + + + + +
+ 4.9 (156 reviews) +
+
+ Starts Feb 5 +
+
+ +
+ + + +
+
+ +
+
+
+ + + +
+
+

Quick Actions

+
+ + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.local/state/replit/agent/rapid_build_started b/.local/state/replit/agent/rapid_build_started new file mode 100644 index 0000000..e69de29 diff --git a/.local/state/replit/agent/rapid_build_success b/.local/state/replit/agent/rapid_build_success new file mode 100644 index 0000000..e69de29 diff --git a/.local/state/replit/agent/repl_state.bin b/.local/state/replit/agent/repl_state.bin new file mode 100644 index 0000000..f3b7e32 Binary files /dev/null and b/.local/state/replit/agent/repl_state.bin differ diff --git a/.replit b/.replit new file mode 100644 index 0000000..1ee1981 --- /dev/null +++ b/.replit @@ -0,0 +1,42 @@ +modules = ["nodejs-20", "web", "postgresql-16"] +run = "npm run dev" +hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"] + +[nix] +channel = "stable-24_05" + +[deployment] +deploymentTarget = "autoscale" +build = ["npm", "run", "build"] +run = ["npm", "run", "start"] + +[env] +PORT = "5000" + +[workflows] +runButton = "Project" + +[[workflows.workflow]] +name = "Project" +mode = "parallel" +author = "agent" + +[[workflows.workflow.tasks]] +task = "workflow.run" +args = "Start application" + +[[workflows.workflow]] +name = "Start application" +author = "agent" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "npm run dev" +waitForPort = 5000 + +[agent] +integrations = ["javascript_mem_db==1.0.0"] + +[[ports]] +localPort = 5000 +externalPort = 80 diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..a7d1dc6 --- /dev/null +++ b/client/index.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WarehouseTrack Pro - Mobile Inventory Management + + +
+ + + + + \ No newline at end of file diff --git a/client/public/manifest.json b/client/public/manifest.json new file mode 100644 index 0000000..2e54ad7 --- /dev/null +++ b/client/public/manifest.json @@ -0,0 +1,109 @@ +{ + "name": "WarehouseTrack Pro", + "short_name": "WarehouseTrack", + "description": "Mobile warehouse inventory management system with document generation and barcode scanning", + "start_url": "/", + "display": "standalone", + "theme_color": "#2563eb", + "background_color": "#ffffff", + "orientation": "any", + "scope": "/", + "categories": ["business", "productivity", "utilities"], + "lang": "en", + "icons": [ + { + "src": "/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "maskable any" + }, + { + "src": "/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable any" + } + ], + "shortcuts": [ + { + "name": "Scan Product", + "short_name": "Scan", + "description": "Quick access to barcode scanner", + "url": "/scanner", + "icons": [{ "src": "/icons/icon-96x96.png", "sizes": "96x96" }] + }, + { + "name": "Inventory", + "short_name": "Inventory", + "description": "View and manage inventory", + "url": "/inventory", + "icons": [{ "src": "/icons/icon-96x96.png", "sizes": "96x96" }] + }, + { + "name": "Documents", + "short_name": "Documents", + "description": "Create transportation documents", + "url": "/documents", + "icons": [{ "src": "/icons/icon-96x96.png", "sizes": "96x96" }] + } + ], + "screenshots": [ + { + "src": "/screenshots/mobile-dashboard.png", + "sizes": "390x844", + "type": "image/png", + "form_factor": "narrow", + "label": "Mobile Dashboard - Inventory Overview" + }, + { + "src": "/screenshots/tablet-dashboard.png", + "sizes": "768x1024", + "type": "image/png", + "form_factor": "wide", + "label": "Tablet Dashboard - Enhanced Layout" + }, + { + "src": "/screenshots/tablet-landscape.png", + "sizes": "1024x768", + "type": "image/png", + "form_factor": "wide", + "label": "Tablet Landscape - Sidebar Navigation" + } + ] +} \ No newline at end of file diff --git a/client/public/sw.js b/client/public/sw.js new file mode 100644 index 0000000..65a8885 --- /dev/null +++ b/client/public/sw.js @@ -0,0 +1,95 @@ +// Service Worker for WarehouseTrack Pro PWA +const CACHE_NAME = 'warehousetrack-v1'; +const urlsToCache = [ + '/', + '/manifest.json', + '/src/index.css', + '/src/main.tsx', + 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap' +]; + +// Install event +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then((cache) => cache.addAll(urlsToCache)) + .then(() => self.skipWaiting()) + ); +}); + +// Activate event +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }).then(() => self.clients.claim()) + ); +}); + +// Fetch event - Network First Strategy for API calls, Cache First for static assets +self.addEventListener('fetch', (event) => { + const { request } = event; + + // Handle API requests with network-first strategy + if (request.url.includes('/api/')) { + event.respondWith( + fetch(request) + .then((response) => { + // Don't cache POST/PUT/DELETE requests + if (request.method === 'GET' && response.status === 200) { + const responseClone = response.clone(); + caches.open(CACHE_NAME) + .then((cache) => cache.put(request, responseClone)); + } + return response; + }) + .catch(() => { + // Fall back to cache if network fails + return caches.match(request); + }) + ); + return; + } + + // Handle static assets with cache-first strategy + event.respondWith( + caches.match(request) + .then((response) => { + // Return cached version or fetch from network + return response || fetch(request) + .then((response) => { + // Don't cache non-successful responses + if (!response || response.status !== 200 || response.type !== 'basic') { + return response; + } + + const responseToCache = response.clone(); + caches.open(CACHE_NAME) + .then((cache) => cache.put(request, responseToCache)); + + return response; + }); + }) + ); +}); + +// Handle background sync for offline actions +self.addEventListener('sync', (event) => { + if (event.tag === 'stock-update') { + event.waitUntil( + // Handle offline stock updates when back online + handleOfflineStockUpdates() + ); + } +}); + +// Placeholder function for handling offline updates +function handleOfflineStockUpdates() { + return Promise.resolve(); +} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..8ceeb8d --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,62 @@ +import { Switch, Route } from "wouter"; +import { queryClient } from "./lib/queryClient"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import NotFound from "@/pages/not-found"; +import Dashboard from "@/pages/dashboard"; +import Inventory from "@/pages/inventory"; +import Documents from "@/pages/documents"; +import Scanner from "@/pages/scanner"; +import Reports from "@/pages/reports"; +import Header from "@/components/layout/header"; +import BottomNavigation from "@/components/layout/bottom-navigation"; +import TabletNavigation from "@/components/layout/tablet-navigation"; +import FloatingActionButton from "@/components/layout/floating-action-button"; +import InstallPrompt from "@/components/pwa/install-prompt"; + +function Router() { + return ( + + + + + + + + + ); +} + +function App() { + return ( + + +
+ {/* Mobile Header - hidden on tablets */} +
+
+
+ +
+ {/* Tablet Sidebar Navigation - visible on tablets only */} + + + {/* Main Content */} +
+ +
+
+ + {/* Mobile Bottom Navigation - hidden on tablets */} + + + + +
+
+
+ ); +} + +export default App; diff --git a/client/src/components/common/alert-banner.tsx b/client/src/components/common/alert-banner.tsx new file mode 100644 index 0000000..3bae4aa --- /dev/null +++ b/client/src/components/common/alert-banner.tsx @@ -0,0 +1,101 @@ +import { useState } from "react"; +import { AlertTriangle, X, Info, CheckCircle, XCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface AlertBannerProps { + type: "warning" | "error" | "info" | "success"; + title: string; + message: string; + dismissible?: boolean; + onDismiss?: () => void; + className?: string; + "data-testid"?: string; +} + +const alertConfig = { + warning: { + icon: AlertTriangle, + bgColor: "bg-warning", + textColor: "text-warning-foreground", + iconColor: "text-warning-foreground", + }, + error: { + icon: XCircle, + bgColor: "bg-error", + textColor: "text-error-foreground", + iconColor: "text-error-foreground", + }, + info: { + icon: Info, + bgColor: "bg-primary", + textColor: "text-primary-foreground", + iconColor: "text-primary-foreground", + }, + success: { + icon: CheckCircle, + bgColor: "bg-secondary", + textColor: "text-secondary-foreground", + iconColor: "text-secondary-foreground", + }, +}; + +export default function AlertBanner({ + type, + title, + message, + dismissible = true, + onDismiss, + className, + "data-testid": testId, +}: AlertBannerProps) { + const [isVisible, setIsVisible] = useState(true); + + const config = alertConfig[type]; + const Icon = config.icon; + + const handleDismiss = () => { + setIsVisible(false); + onDismiss?.(); + }; + + if (!isVisible) return null; + + return ( +
+
+ +
+

+ {title} +

+

+ {message} +

+
+
+ {dismissible && ( + + )} +
+ ); +} diff --git a/client/src/components/common/quick-actions.tsx b/client/src/components/common/quick-actions.tsx new file mode 100644 index 0000000..157a5d0 --- /dev/null +++ b/client/src/components/common/quick-actions.tsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { QrCode, Plus, FileText, BarChart3 } from "lucide-react"; +import { useLocation } from "wouter"; +import AddStockDialog from "@/components/inventory/add-stock-dialog"; +import CreateDocumentDialog from "@/components/documents/create-document-dialog"; +import ScannerModal from "@/components/scanner/scanner-modal"; + +const quickActions = [ + { + id: "scan", + title: "Scan Product", + icon: QrCode, + color: "text-primary", + action: "openScanner", + }, + { + id: "add-stock", + title: "Add Stock", + icon: Plus, + color: "text-secondary", + action: "addStock", + }, + { + id: "create-document", + title: "New Document", + icon: FileText, + color: "text-primary", + action: "createDocument", + }, + { + id: "view-reports", + title: "View Reports", + icon: BarChart3, + color: "text-secondary", + action: "viewReports", + }, +]; + +export default function QuickActions() { + const [, setLocation] = useLocation(); + const [isScannerOpen, setIsScannerOpen] = useState(false); + const [isAddStockOpen, setIsAddStockOpen] = useState(false); + const [isCreateDocumentOpen, setIsCreateDocumentOpen] = useState(false); + + const handleAction = (action: string) => { + switch (action) { + case "openScanner": + setIsScannerOpen(true); + break; + case "addStock": + setIsAddStockOpen(true); + break; + case "createDocument": + setIsCreateDocumentOpen(true); + break; + case "viewReports": + setLocation("/reports"); + break; + } + }; + + return ( + <> + + + + Quick Actions + + + +
+ {quickActions.map((action) => { + const Icon = action.icon; + return ( + + ); + })} +
+
+
+ + + + + + + + ); +} diff --git a/client/src/components/common/recent-activity.tsx b/client/src/components/common/recent-activity.tsx new file mode 100644 index 0000000..13d08b7 --- /dev/null +++ b/client/src/components/common/recent-activity.tsx @@ -0,0 +1,161 @@ +import { useQuery } from "@tanstack/react-query"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Plus, Truck, AlertTriangle, FileText, Package } from "lucide-react"; +import type { StockMovement, Document, Product } from "@shared/schema"; + +interface ActivityItem { + id: string; + type: "stock_movement" | "document_created" | "low_stock" | "out_of_stock"; + title: string; + description: string; + timestamp: Date; + icon: typeof Plus; + iconBg: string; +} + +export default function RecentActivity() { + const { data: stockMovements } = useQuery({ + queryKey: ["/api/stock-movements"], + }); + + const { data: documents } = useQuery({ + queryKey: ["/api/documents"], + }); + + const { data: products } = useQuery({ + queryKey: ["/api/products"], + }); + + // Generate activity items from different sources + const generateActivityItems = (): ActivityItem[] => { + const activities: ActivityItem[] = []; + + // Add stock movements + if (stockMovements) { + stockMovements.slice(0, 3).forEach((movement) => { + const product = products?.find(p => p.id === movement.productId); + if (product) { + activities.push({ + id: `stock-${movement.id}`, + type: "stock_movement", + title: movement.type === "in" ? "Stock Added" : movement.type === "out" ? "Stock Removed" : "Stock Adjusted", + description: `${movement.quantity} units of ${product.name} (SKU: ${product.sku})`, + timestamp: movement.createdAt || new Date(), + icon: movement.type === "in" ? Plus : Package, + iconBg: movement.type === "in" ? "bg-secondary" : "bg-primary", + }); + } + }); + } + + // Add document creation activities + if (documents) { + documents.slice(0, 2).forEach((document) => { + activities.push({ + id: `doc-${document.id}`, + type: "document_created", + title: "Document Created", + description: `${document.title} (${document.documentNumber})`, + timestamp: document.createdAt || new Date(), + icon: document.type === "delivery_note" ? Truck : FileText, + iconBg: "bg-primary", + }); + }); + } + + // Add stock alerts + if (products) { + const lowStockProducts = products.filter(p => p.currentStock > 0 && p.currentStock <= p.minThreshold); + const outOfStockProducts = products.filter(p => p.currentStock === 0); + + if (lowStockProducts.length > 0) { + activities.push({ + id: "low-stock-alert", + type: "low_stock", + title: "Low Stock Alert", + description: `${lowStockProducts[0].name} (SKU: ${lowStockProducts[0].sku}) below minimum threshold`, + timestamp: new Date(Date.now() - 3600000), // 1 hour ago + icon: AlertTriangle, + iconBg: "bg-warning", + }); + } + + if (outOfStockProducts.length > 0) { + activities.push({ + id: "out-of-stock-alert", + type: "out_of_stock", + title: "Out of Stock Alert", + description: `${outOfStockProducts[0].name} (SKU: ${outOfStockProducts[0].sku}) is out of stock`, + timestamp: new Date(Date.now() - 7200000), // 2 hours ago + icon: AlertTriangle, + iconBg: "bg-error", + }); + } + } + + // Sort by timestamp (newest first) and limit to 5 items + return activities + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .slice(0, 5); + }; + + const activities = generateActivityItems(); + + const formatTimeAgo = (timestamp: Date) => { + const now = new Date(); + const diffMs = now.getTime() - timestamp.getTime(); + const diffMins = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMins < 1) return "Just now"; + if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; + return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + }; + + return ( + + + + Recent Activity + + + + {activities.length > 0 ? ( +
+ {activities.map((activity) => { + const Icon = activity.icon; + return ( +
+
+ +
+
+

+ {activity.title} +

+

+ {activity.description} +

+

+ {formatTimeAgo(activity.timestamp)} +

+
+
+ ); + })} +
+ ) : ( +
+ No recent activity to display +
+ )} +
+
+ ); +} diff --git a/client/src/components/documents/create-document-dialog.tsx b/client/src/components/documents/create-document-dialog.tsx new file mode 100644 index 0000000..e8a597b --- /dev/null +++ b/client/src/components/documents/create-document-dialog.tsx @@ -0,0 +1,367 @@ +import { useState, useEffect } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { insertDocumentSchema } from "@shared/schema"; +import type { Product } from "@shared/schema"; +import { Truck, List, Tag, ClipboardCheck, BarChart3, Zap, Plus, Minus } from "lucide-react"; + +const documentFormSchema = insertDocumentSchema.extend({ + customerName: z.string().optional(), + customerAddress: z.string().optional(), + items: z.array(z.object({ + productId: z.string(), + quantity: z.number().min(1), + notes: z.string().optional(), + })).optional(), +}); + +interface CreateDocumentDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + templateType?: string | null; +} + +const templateIcons = { + delivery_note: Truck, + packing_list: List, + shipping_label: Tag, + goods_receipt: ClipboardCheck, + stock_report: BarChart3, + dispatch_note: Zap, +}; + +const templateTitles = { + delivery_note: "Delivery Note", + packing_list: "Packing List", + shipping_label: "Shipping Label", + goods_receipt: "Goods Receipt", + stock_report: "Stock Report", + dispatch_note: "Dispatch Note", +}; + +export default function CreateDocumentDialog({ open, onOpenChange, templateType }: CreateDocumentDialogProps) { + const [selectedItems, setSelectedItems] = useState>([]); + const queryClient = useQueryClient(); + const { toast } = useToast(); + + const { data: products } = useQuery({ + queryKey: ["/api/products"], + }); + + const form = useForm({ + resolver: zodResolver(documentFormSchema), + defaultValues: { + type: templateType || "delivery_note", + documentNumber: "", + title: "", + content: {}, + status: "draft", + customerName: "", + customerAddress: "", + items: [], + }, + }); + + useEffect(() => { + if (templateType && open) { + const docNumber = `${templateType.toUpperCase().replace('_', '-')}-${Date.now()}`; + form.setValue("type", templateType); + form.setValue("documentNumber", docNumber); + form.setValue("title", templateTitles[templateType as keyof typeof templateTitles] || "New Document"); + setSelectedItems([]); + } + }, [templateType, open, form]); + + const createDocumentMutation = useMutation({ + mutationFn: (data: z.infer) => { + const documentData = { + ...data, + content: { + customerName: data.customerName, + customerAddress: data.customerAddress, + items: selectedItems.map(item => { + const product = products?.find(p => p.id === item.productId); + return { + ...item, + productName: product?.name, + productSku: product?.sku, + unit: product?.unit, + }; + }), + createdAt: new Date().toISOString(), + totalItems: selectedItems.reduce((sum, item) => sum + item.quantity, 0), + }, + }; + return apiRequest("POST", "/api/documents", documentData); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/documents"] }); + toast({ + title: "Document Created", + description: "Document has been created successfully", + }); + form.reset(); + setSelectedItems([]); + onOpenChange(false); + }, + onError: () => { + toast({ + title: "Error", + description: "Failed to create document", + variant: "destructive", + }); + }, + }); + + const addItem = () => { + setSelectedItems([...selectedItems, { productId: "", quantity: 1 }]); + }; + + const removeItem = (index: number) => { + setSelectedItems(selectedItems.filter((_, i) => i !== index)); + }; + + const updateItem = (index: number, field: string, value: any) => { + const updatedItems = [...selectedItems]; + updatedItems[index] = { ...updatedItems[index], [field]: value }; + setSelectedItems(updatedItems); + }; + + const onSubmit = (data: z.infer) => { + createDocumentMutation.mutate(data); + }; + + const Icon = templateType ? templateIcons[templateType as keyof typeof templateIcons] : Truck; + + return ( + + + + + {Icon && } + Create {templateTitles[templateType as keyof typeof templateTitles] || "Document"} + + + +
+ +
+ ( + + Document Number + + + + + + )} + /> + ( + + Status + + + + )} + /> +
+ + ( + + Document Title + + + + + + )} + /> + + {(templateType === "delivery_note" || templateType === "packing_list" || templateType === "shipping_label") && ( + <> +
+ ( + + Customer Name + + + + + + )} + /> + ( + + Customer Address + +