feat: add AI Dental Shopping section with sidebar nav and Login Info page
- Add AI Dental Shopping to sidebar with Search/Tag and Login Info sub-pages - Build full-stack Login Info CRUD: save vendor name, website, username, password per user - Add ShoppingVendor Prisma model, run db push, regenerate client and Zod schemas - Add storage layer, REST API at /api/shopping-vendors/, and frontend table with add/edit/delete modal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ import officeContactRoutes from "./office-contact";
|
||||
import procedureTimeslotRoutes from "./procedure-timeslot";
|
||||
import insuranceContactsRoutes from "./insurance-contacts";
|
||||
import commissionsRoutes from "./commissions";
|
||||
import shoppingVendorsRoutes from "./shopping-vendors";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -68,5 +69,6 @@ router.use("/office-contact", officeContactRoutes);
|
||||
router.use("/procedure-timeslot", procedureTimeslotRoutes);
|
||||
router.use("/insurance-contacts", insuranceContactsRoutes);
|
||||
router.use("/commissions", commissionsRoutes);
|
||||
router.use("/shopping-vendors", shoppingVendorsRoutes);
|
||||
|
||||
export default router;
|
||||
|
||||
60
apps/Backend/src/routes/shopping-vendors.ts
Normal file
60
apps/Backend/src/routes/shopping-vendors.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import { storage } from "../storage";
|
||||
import { insertShoppingVendorSchema, ShoppingVendor } from "@repo/db/types";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
if (!req.user?.id) return res.status(401).json({ message: "Unauthorized" });
|
||||
const vendors = await storage.getShoppingVendorsByUser(req.user.id);
|
||||
return res.status(200).json(vendors);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to fetch vendors", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
if (!req.user?.id) return res.status(401).json({ message: "Unauthorized" });
|
||||
const parseResult = insertShoppingVendorSchema.safeParse({ ...req.body, userId: req.user.id });
|
||||
if (!parseResult.success) {
|
||||
const firstError = Object.values(parseResult.error.flatten().fieldErrors)[0]?.[0] || "Invalid input";
|
||||
return res.status(400).json({ message: firstError });
|
||||
}
|
||||
const vendor = await storage.createShoppingVendor(parseResult.data);
|
||||
return res.status(201).json(vendor);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to create vendor", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).send("Invalid vendor ID");
|
||||
const vendor = await storage.updateShoppingVendor(id, req.body as Partial<ShoppingVendor>);
|
||||
return res.status(200).json(vendor);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to update vendor", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
const id = Number(req.params.id);
|
||||
if (isNaN(id)) return res.status(400).send("Invalid ID");
|
||||
const existing = await storage.getShoppingVendor(id);
|
||||
if (!existing) return res.status(404).json({ message: "Vendor not found" });
|
||||
if (existing.userId !== userId) return res.status(403).json({ message: "Forbidden" });
|
||||
const ok = await storage.deleteShoppingVendor(userId, id);
|
||||
if (!ok) return res.status(404).json({ message: "Vendor not found or already deleted" });
|
||||
return res.status(204).send();
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: "Failed to delete vendor", details: String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -24,6 +24,7 @@ import { officeContactStorage } from "./office-contact-storage";
|
||||
import { procedureTimeslotStorage } from "./procedure-timeslot-storage";
|
||||
import { insuranceContactStorage } from "./insurance-contact-storage";
|
||||
import { commissionsStorage } from "./commissions-storage";
|
||||
import { shoppingVendorStorage } from "./shopping-vendor-storage";
|
||||
|
||||
|
||||
export const storage = {
|
||||
@@ -51,6 +52,7 @@ export const storage = {
|
||||
...procedureTimeslotStorage,
|
||||
...insuranceContactStorage,
|
||||
...commissionsStorage,
|
||||
...shoppingVendorStorage,
|
||||
|
||||
};
|
||||
|
||||
|
||||
37
apps/Backend/src/storage/shopping-vendor-storage.ts
Normal file
37
apps/Backend/src/storage/shopping-vendor-storage.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { InsertShoppingVendor, ShoppingVendor } from "@repo/db/types";
|
||||
import { prisma as db } from "@repo/db/client";
|
||||
|
||||
export interface IShoppingVendorStorage {
|
||||
getShoppingVendor(id: number): Promise<ShoppingVendor | null>;
|
||||
getShoppingVendorsByUser(userId: number): Promise<ShoppingVendor[]>;
|
||||
createShoppingVendor(data: InsertShoppingVendor): Promise<ShoppingVendor>;
|
||||
updateShoppingVendor(id: number, updates: Partial<ShoppingVendor>): Promise<ShoppingVendor | null>;
|
||||
deleteShoppingVendor(userId: number, id: number): Promise<boolean>;
|
||||
}
|
||||
|
||||
export const shoppingVendorStorage: IShoppingVendorStorage = {
|
||||
async getShoppingVendor(id: number) {
|
||||
return await db.shoppingVendor.findUnique({ where: { id } }) as ShoppingVendor | null;
|
||||
},
|
||||
|
||||
async getShoppingVendorsByUser(userId: number) {
|
||||
return await db.shoppingVendor.findMany({ where: { userId }, orderBy: { id: "asc" } }) as ShoppingVendor[];
|
||||
},
|
||||
|
||||
async createShoppingVendor(data: InsertShoppingVendor) {
|
||||
return await db.shoppingVendor.create({ data }) as ShoppingVendor;
|
||||
},
|
||||
|
||||
async updateShoppingVendor(id: number, updates: Partial<ShoppingVendor>) {
|
||||
return await db.shoppingVendor.update({ where: { id }, data: updates }) as ShoppingVendor;
|
||||
},
|
||||
|
||||
async deleteShoppingVendor(userId: number, id: number) {
|
||||
try {
|
||||
await db.shoppingVendor.delete({ where: { userId, id } });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user