First template code generated from replit.com
This commit is contained in:
71
server/index.ts
Normal file
71
server/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
||||
|
||||
const originalResJson = res.json;
|
||||
res.json = function (bodyJson, ...args) {
|
||||
capturedJsonResponse = bodyJson;
|
||||
return originalResJson.apply(res, [bodyJson, ...args]);
|
||||
};
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
if (path.startsWith("/api")) {
|
||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||
if (capturedJsonResponse) {
|
||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||
}
|
||||
|
||||
if (logLine.length > 80) {
|
||||
logLine = logLine.slice(0, 79) + "…";
|
||||
}
|
||||
|
||||
log(logLine);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const server = await registerRoutes(app);
|
||||
|
||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
const message = err.message || "Internal Server Error";
|
||||
|
||||
res.status(status).json({ message });
|
||||
throw err;
|
||||
});
|
||||
|
||||
// importantly only setup vite in development and after
|
||||
// setting up all the other routes so the catch-all route
|
||||
// doesn't interfere with the other routes
|
||||
if (app.get("env") === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// ALWAYS serve the app on the port specified in the environment variable PORT
|
||||
// Other ports are firewalled. Default to 5000 if not specified.
|
||||
// this serves both the API and the client.
|
||||
// It is the only port that is not firewalled.
|
||||
const port = parseInt(process.env.PORT || '5000', 10);
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
reusePort: true,
|
||||
}, () => {
|
||||
log(`serving on port ${port}`);
|
||||
});
|
||||
})();
|
||||
258
server/routes.ts
Normal file
258
server/routes.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import type { Express } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { insertProductSchema, insertStockMovementSchema, insertDocumentSchema } from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Products
|
||||
app.get("/api/products", async (req, res) => {
|
||||
try {
|
||||
const products = await storage.getProducts();
|
||||
res.json(products);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch products" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/products/search", async (req, res) => {
|
||||
try {
|
||||
const query = req.query.q as string;
|
||||
if (!query) {
|
||||
return res.status(400).json({ message: "Search query is required" });
|
||||
}
|
||||
const products = await storage.searchProducts(query);
|
||||
res.json(products);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to search products" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/products/low-stock", async (req, res) => {
|
||||
try {
|
||||
const products = await storage.getLowStockProducts();
|
||||
res.json(products);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch low stock products" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/products/out-of-stock", async (req, res) => {
|
||||
try {
|
||||
const products = await storage.getOutOfStockProducts();
|
||||
res.json(products);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch out of stock products" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/products/:id", async (req, res) => {
|
||||
try {
|
||||
const product = await storage.getProduct(req.params.id);
|
||||
if (!product) {
|
||||
return res.status(404).json({ message: "Product not found" });
|
||||
}
|
||||
res.json(product);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch product" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/products", async (req, res) => {
|
||||
try {
|
||||
const productData = insertProductSchema.parse(req.body);
|
||||
const product = await storage.createProduct(productData);
|
||||
res.status(201).json(product);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: "Invalid product data", errors: error.errors });
|
||||
}
|
||||
res.status(500).json({ message: "Failed to create product" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/products/:id", async (req, res) => {
|
||||
try {
|
||||
const updates = insertProductSchema.partial().parse(req.body);
|
||||
const product = await storage.updateProduct(req.params.id, updates);
|
||||
if (!product) {
|
||||
return res.status(404).json({ message: "Product not found" });
|
||||
}
|
||||
res.json(product);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: "Invalid product data", errors: error.errors });
|
||||
}
|
||||
res.status(500).json({ message: "Failed to update product" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/products/:id", async (req, res) => {
|
||||
try {
|
||||
const deleted = await storage.deleteProduct(req.params.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "Product not found" });
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to delete product" });
|
||||
}
|
||||
});
|
||||
|
||||
// Stock movements
|
||||
app.get("/api/stock-movements", async (req, res) => {
|
||||
try {
|
||||
const productId = req.query.productId as string;
|
||||
const movements = await storage.getStockMovements(productId);
|
||||
res.json(movements);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch stock movements" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/stock-movements", async (req, res) => {
|
||||
try {
|
||||
const movementData = insertStockMovementSchema.parse(req.body);
|
||||
const movement = await storage.createStockMovement(movementData);
|
||||
res.status(201).json(movement);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: "Invalid stock movement data", errors: error.errors });
|
||||
}
|
||||
res.status(500).json({ message: "Failed to create stock movement" });
|
||||
}
|
||||
});
|
||||
|
||||
// Documents
|
||||
app.get("/api/documents", async (req, res) => {
|
||||
try {
|
||||
const documents = await storage.getDocuments();
|
||||
res.json(documents);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch documents" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/documents/:id", async (req, res) => {
|
||||
try {
|
||||
const document = await storage.getDocument(req.params.id);
|
||||
if (!document) {
|
||||
return res.status(404).json({ message: "Document not found" });
|
||||
}
|
||||
res.json(document);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch document" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/documents", async (req, res) => {
|
||||
try {
|
||||
const documentData = insertDocumentSchema.parse(req.body);
|
||||
const document = await storage.createDocument(documentData);
|
||||
res.status(201).json(document);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: "Invalid document data", errors: error.errors });
|
||||
}
|
||||
res.status(500).json({ message: "Failed to create document" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/documents/:id", async (req, res) => {
|
||||
try {
|
||||
const updates = insertDocumentSchema.partial().parse(req.body);
|
||||
const document = await storage.updateDocument(req.params.id, updates);
|
||||
if (!document) {
|
||||
return res.status(404).json({ message: "Document not found" });
|
||||
}
|
||||
res.json(document);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ message: "Invalid document data", errors: error.errors });
|
||||
}
|
||||
res.status(500).json({ message: "Failed to update document" });
|
||||
}
|
||||
});
|
||||
|
||||
// Generate PDF document
|
||||
app.get("/api/documents/:id/pdf", async (req, res) => {
|
||||
try {
|
||||
const document = await storage.getDocument(req.params.id);
|
||||
if (!document) {
|
||||
return res.status(404).json({ message: "Document not found" });
|
||||
}
|
||||
|
||||
// Simple PDF generation for now - in production would use proper PDF library
|
||||
const pdfContent = `
|
||||
Document: ${document.title}
|
||||
Type: ${document.type}
|
||||
Number: ${document.documentNumber}
|
||||
Status: ${document.status}
|
||||
Created: ${document.createdAt}
|
||||
|
||||
Content:
|
||||
${JSON.stringify(document.content, null, 2)}
|
||||
`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${document.documentNumber}.pdf"`);
|
||||
res.send(pdfContent);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to generate PDF" });
|
||||
}
|
||||
});
|
||||
|
||||
// Notifications
|
||||
app.get("/api/notifications", async (req, res) => {
|
||||
try {
|
||||
const notifications = await storage.getNotifications();
|
||||
res.json(notifications);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch notifications" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/notifications/unread-count", async (req, res) => {
|
||||
try {
|
||||
const count = await storage.getUnreadNotificationCount();
|
||||
res.json({ count });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch notification count" });
|
||||
}
|
||||
});
|
||||
|
||||
app.patch("/api/notifications/:id/read", async (req, res) => {
|
||||
try {
|
||||
const success = await storage.markNotificationAsRead(req.params.id);
|
||||
if (!success) {
|
||||
return res.status(404).json({ message: "Notification not found" });
|
||||
}
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to mark notification as read" });
|
||||
}
|
||||
});
|
||||
|
||||
// Dashboard stats
|
||||
app.get("/api/dashboard/stats", async (req, res) => {
|
||||
try {
|
||||
const products = await storage.getProducts();
|
||||
const lowStockProducts = await storage.getLowStockProducts();
|
||||
const outOfStockProducts = await storage.getOutOfStockProducts();
|
||||
|
||||
const stats = {
|
||||
totalItems: products.length,
|
||||
inStockItems: products.filter(p => p.currentStock > p.minThreshold).length,
|
||||
lowStockItems: lowStockProducts.length,
|
||||
outOfStockItems: outOfStockProducts.length,
|
||||
};
|
||||
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: "Failed to fetch dashboard stats" });
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
return httpServer;
|
||||
}
|
||||
349
server/storage.ts
Normal file
349
server/storage.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import {
|
||||
type Product,
|
||||
type InsertProduct,
|
||||
type StockMovement,
|
||||
type InsertStockMovement,
|
||||
type Document,
|
||||
type InsertDocument,
|
||||
type Order,
|
||||
type InsertOrder,
|
||||
type Notification,
|
||||
type InsertNotification
|
||||
} from "@shared/schema";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export interface IStorage {
|
||||
// Products
|
||||
getProducts(): Promise<Product[]>;
|
||||
getProduct(id: string): Promise<Product | undefined>;
|
||||
getProductBySku(sku: string): Promise<Product | undefined>;
|
||||
createProduct(product: InsertProduct): Promise<Product>;
|
||||
updateProduct(id: string, product: Partial<InsertProduct>): Promise<Product | undefined>;
|
||||
deleteProduct(id: string): Promise<boolean>;
|
||||
searchProducts(query: string): Promise<Product[]>;
|
||||
getLowStockProducts(): Promise<Product[]>;
|
||||
getOutOfStockProducts(): Promise<Product[]>;
|
||||
|
||||
// Stock movements
|
||||
getStockMovements(productId?: string): Promise<StockMovement[]>;
|
||||
createStockMovement(movement: InsertStockMovement): Promise<StockMovement>;
|
||||
|
||||
// Documents
|
||||
getDocuments(): Promise<Document[]>;
|
||||
getDocument(id: string): Promise<Document | undefined>;
|
||||
createDocument(document: InsertDocument): Promise<Document>;
|
||||
updateDocument(id: string, document: Partial<InsertDocument>): Promise<Document | undefined>;
|
||||
deleteDocument(id: string): Promise<boolean>;
|
||||
|
||||
// Orders
|
||||
getOrders(): Promise<Order[]>;
|
||||
getOrder(id: string): Promise<Order | undefined>;
|
||||
createOrder(order: InsertOrder): Promise<Order>;
|
||||
updateOrder(id: string, order: Partial<InsertOrder>): Promise<Order | undefined>;
|
||||
|
||||
// Notifications
|
||||
getNotifications(): Promise<Notification[]>;
|
||||
createNotification(notification: InsertNotification): Promise<Notification>;
|
||||
markNotificationAsRead(id: string): Promise<boolean>;
|
||||
getUnreadNotificationCount(): Promise<number>;
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
private products: Map<string, Product>;
|
||||
private stockMovements: Map<string, StockMovement>;
|
||||
private documents: Map<string, Document>;
|
||||
private orders: Map<string, Order>;
|
||||
private notifications: Map<string, Notification>;
|
||||
|
||||
constructor() {
|
||||
this.products = new Map();
|
||||
this.stockMovements = new Map();
|
||||
this.documents = new Map();
|
||||
this.orders = new Map();
|
||||
this.notifications = new Map();
|
||||
|
||||
// Initialize with sample data
|
||||
this.initializeSampleData();
|
||||
}
|
||||
|
||||
private initializeSampleData() {
|
||||
const sampleProducts: Product[] = [
|
||||
{
|
||||
id: "1",
|
||||
sku: "SH-001",
|
||||
name: "Safety Helmets - White",
|
||||
description: "High-quality safety helmets for construction work",
|
||||
currentStock: 125,
|
||||
minThreshold: 20,
|
||||
unit: "units",
|
||||
price: "29.99",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
sku: "WG-003",
|
||||
name: "Work Gloves - Medium",
|
||||
description: "Durable work gloves for general construction",
|
||||
currentStock: 8,
|
||||
minThreshold: 15,
|
||||
unit: "pairs",
|
||||
price: "12.50",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
sku: "HV-005",
|
||||
name: "Hi-Vis Vests - Large",
|
||||
description: "High-visibility safety vests",
|
||||
currentStock: 0,
|
||||
minThreshold: 10,
|
||||
unit: "units",
|
||||
price: "15.75",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
sampleProducts.forEach(product => {
|
||||
this.products.set(product.id, product);
|
||||
});
|
||||
|
||||
// Create notifications for low/out of stock items
|
||||
const lowStockNotification: Notification = {
|
||||
id: "notif-1",
|
||||
type: "low_stock",
|
||||
title: "Low Stock Alert",
|
||||
message: "Work Gloves - Medium is below minimum threshold",
|
||||
isRead: false,
|
||||
relatedId: "2",
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const outOfStockNotification: Notification = {
|
||||
id: "notif-2",
|
||||
type: "out_of_stock",
|
||||
title: "Out of Stock Alert",
|
||||
message: "Hi-Vis Vests - Large is out of stock",
|
||||
isRead: false,
|
||||
relatedId: "3",
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
this.notifications.set("notif-1", lowStockNotification);
|
||||
this.notifications.set("notif-2", outOfStockNotification);
|
||||
}
|
||||
|
||||
// Products
|
||||
async getProducts(): Promise<Product[]> {
|
||||
return Array.from(this.products.values());
|
||||
}
|
||||
|
||||
async getProduct(id: string): Promise<Product | undefined> {
|
||||
return this.products.get(id);
|
||||
}
|
||||
|
||||
async getProductBySku(sku: string): Promise<Product | undefined> {
|
||||
return Array.from(this.products.values()).find(product => product.sku === sku);
|
||||
}
|
||||
|
||||
async createProduct(insertProduct: InsertProduct): Promise<Product> {
|
||||
const id = randomUUID();
|
||||
const product: Product = {
|
||||
...insertProduct,
|
||||
id,
|
||||
description: insertProduct.description ?? null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.products.set(id, product);
|
||||
return product;
|
||||
}
|
||||
|
||||
async updateProduct(id: string, updates: Partial<InsertProduct>): Promise<Product | undefined> {
|
||||
const product = this.products.get(id);
|
||||
if (!product) return undefined;
|
||||
|
||||
const updatedProduct: Product = {
|
||||
...product,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.products.set(id, updatedProduct);
|
||||
return updatedProduct;
|
||||
}
|
||||
|
||||
async deleteProduct(id: string): Promise<boolean> {
|
||||
return this.products.delete(id);
|
||||
}
|
||||
|
||||
async searchProducts(query: string): Promise<Product[]> {
|
||||
const products = Array.from(this.products.values());
|
||||
const lowercaseQuery = query.toLowerCase();
|
||||
return products.filter(product =>
|
||||
product.name.toLowerCase().includes(lowercaseQuery) ||
|
||||
product.sku.toLowerCase().includes(lowercaseQuery) ||
|
||||
product.description?.toLowerCase().includes(lowercaseQuery)
|
||||
);
|
||||
}
|
||||
|
||||
async getLowStockProducts(): Promise<Product[]> {
|
||||
return Array.from(this.products.values()).filter(
|
||||
product => product.currentStock > 0 && product.currentStock <= product.minThreshold
|
||||
);
|
||||
}
|
||||
|
||||
async getOutOfStockProducts(): Promise<Product[]> {
|
||||
return Array.from(this.products.values()).filter(
|
||||
product => product.currentStock === 0
|
||||
);
|
||||
}
|
||||
|
||||
// Stock movements
|
||||
async getStockMovements(productId?: string): Promise<StockMovement[]> {
|
||||
const movements = Array.from(this.stockMovements.values());
|
||||
if (productId) {
|
||||
return movements.filter(movement => movement.productId === productId);
|
||||
}
|
||||
return movements;
|
||||
}
|
||||
|
||||
async createStockMovement(insertMovement: InsertStockMovement): Promise<StockMovement> {
|
||||
const id = randomUUID();
|
||||
const movement: StockMovement = {
|
||||
...insertMovement,
|
||||
id,
|
||||
reason: insertMovement.reason ?? null,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
this.stockMovements.set(id, movement);
|
||||
|
||||
// Update product stock
|
||||
const product = this.products.get(insertMovement.productId);
|
||||
if (product) {
|
||||
let newStock = product.currentStock;
|
||||
if (insertMovement.type === 'in') {
|
||||
newStock += insertMovement.quantity;
|
||||
} else if (insertMovement.type === 'out') {
|
||||
newStock -= insertMovement.quantity;
|
||||
} else if (insertMovement.type === 'adjustment') {
|
||||
newStock = insertMovement.quantity;
|
||||
}
|
||||
|
||||
await this.updateProduct(insertMovement.productId, { currentStock: Math.max(0, newStock) });
|
||||
}
|
||||
|
||||
return movement;
|
||||
}
|
||||
|
||||
// Documents
|
||||
async getDocuments(): Promise<Document[]> {
|
||||
return Array.from(this.documents.values());
|
||||
}
|
||||
|
||||
async getDocument(id: string): Promise<Document | undefined> {
|
||||
return this.documents.get(id);
|
||||
}
|
||||
|
||||
async createDocument(insertDocument: InsertDocument): Promise<Document> {
|
||||
const id = randomUUID();
|
||||
const document: Document = {
|
||||
...insertDocument,
|
||||
id,
|
||||
content: insertDocument.content ?? {},
|
||||
status: insertDocument.status ?? "draft",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.documents.set(id, document);
|
||||
return document;
|
||||
}
|
||||
|
||||
async updateDocument(id: string, updates: Partial<InsertDocument>): Promise<Document | undefined> {
|
||||
const document = this.documents.get(id);
|
||||
if (!document) return undefined;
|
||||
|
||||
const updatedDocument: Document = {
|
||||
...document,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.documents.set(id, updatedDocument);
|
||||
return updatedDocument;
|
||||
}
|
||||
|
||||
async deleteDocument(id: string): Promise<boolean> {
|
||||
return this.documents.delete(id);
|
||||
}
|
||||
|
||||
// Orders
|
||||
async getOrders(): Promise<Order[]> {
|
||||
return Array.from(this.orders.values());
|
||||
}
|
||||
|
||||
async getOrder(id: string): Promise<Order | undefined> {
|
||||
return this.orders.get(id);
|
||||
}
|
||||
|
||||
async createOrder(insertOrder: InsertOrder): Promise<Order> {
|
||||
const id = randomUUID();
|
||||
const order: Order = {
|
||||
...insertOrder,
|
||||
id,
|
||||
status: insertOrder.status ?? "pending",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.orders.set(id, order);
|
||||
return order;
|
||||
}
|
||||
|
||||
async updateOrder(id: string, updates: Partial<InsertOrder>): Promise<Order | undefined> {
|
||||
const order = this.orders.get(id);
|
||||
if (!order) return undefined;
|
||||
|
||||
const updatedOrder: Order = {
|
||||
...order,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
this.orders.set(id, updatedOrder);
|
||||
return updatedOrder;
|
||||
}
|
||||
|
||||
// Notifications
|
||||
async getNotifications(): Promise<Notification[]> {
|
||||
return Array.from(this.notifications.values()).sort(
|
||||
(a, b) => (b.createdAt?.getTime() || 0) - (a.createdAt?.getTime() || 0)
|
||||
);
|
||||
}
|
||||
|
||||
async createNotification(insertNotification: InsertNotification): Promise<Notification> {
|
||||
const id = randomUUID();
|
||||
const notification: Notification = {
|
||||
...insertNotification,
|
||||
id,
|
||||
isRead: insertNotification.isRead ?? false,
|
||||
relatedId: insertNotification.relatedId ?? null,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
this.notifications.set(id, notification);
|
||||
return notification;
|
||||
}
|
||||
|
||||
async markNotificationAsRead(id: string): Promise<boolean> {
|
||||
const notification = this.notifications.get(id);
|
||||
if (!notification) return false;
|
||||
|
||||
notification.isRead = true;
|
||||
this.notifications.set(id, notification);
|
||||
return true;
|
||||
}
|
||||
|
||||
async getUnreadNotificationCount(): Promise<number> {
|
||||
return Array.from(this.notifications.values()).filter(n => !n.isRead).length;
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new MemStorage();
|
||||
85
server/vite.ts
Normal file
85
server/vite.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createServer as createViteServer, createLogger } from "vite";
|
||||
import { type Server } from "http";
|
||||
import viteConfig from "../vite.config";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const viteLogger = createLogger();
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
console.log(`${formattedTime} [${source}] ${message}`);
|
||||
}
|
||||
|
||||
export async function setupVite(app: Express, server: Server) {
|
||||
const serverOptions = {
|
||||
middlewareMode: true,
|
||||
hmr: { server },
|
||||
allowedHosts: true as const,
|
||||
};
|
||||
|
||||
const vite = await createViteServer({
|
||||
...viteConfig,
|
||||
configFile: false,
|
||||
customLogger: {
|
||||
...viteLogger,
|
||||
error: (msg, options) => {
|
||||
viteLogger.error(msg, options);
|
||||
process.exit(1);
|
||||
},
|
||||
},
|
||||
server: serverOptions,
|
||||
appType: "custom",
|
||||
});
|
||||
|
||||
app.use(vite.middlewares);
|
||||
app.use("*", async (req, res, next) => {
|
||||
const url = req.originalUrl;
|
||||
|
||||
try {
|
||||
const clientTemplate = path.resolve(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"client",
|
||||
"index.html",
|
||||
);
|
||||
|
||||
// always reload the index.html file from disk incase it changes
|
||||
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
||||
template = template.replace(
|
||||
`src="/src/main.tsx"`,
|
||||
`src="/src/main.tsx?v=${nanoid()}"`,
|
||||
);
|
||||
const page = await vite.transformIndexHtml(url, template);
|
||||
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
||||
} catch (e) {
|
||||
vite.ssrFixStacktrace(e as Error);
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath = path.resolve(import.meta.dirname, "public");
|
||||
|
||||
if (!fs.existsSync(distPath)) {
|
||||
throw new Error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`,
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user