Introduce an information system for important announcements

Implement a new announcement system with priority levels, role-based access, and store-specific filtering, replacing the customer order statistics card on the dashboard.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e437ffed-c31b-44eb-9fd5-b6f9faa8e7ce
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/e437ffed-c31b-44eb-9fd5-b6f9faa8e7ce/biGr9ER
This commit is contained in:
michaelschal committed 2025-08-16 09:54:34 +00:00
1 parent 5688450117
commit 30cfc4b3de
9 files changed
+729 -47

No files matched your search

+88
View File
@@ -34,6 +34,7 @@ import {
insertDlcProductSchema,
insertDlcProductFrontendSchema,
insertTaskSchema,
insertAnnouncementSchema,
insertNocodbConfigSchema,
insertSavTicketSchema,
insertSavTicketHistorySchema,
@@ -2996,6 +2997,93 @@ RÉSUMÉ DU SCAN
});
// Route de géolocalisation météo
// Announcement routes
app.get('/api/announcements', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims ? req.user.claims.sub : req.user.id;
const user = await storage.getUserWithGroups(userId);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
// Get user's group IDs for filtering
let groupIds: number[] = [];
if (user.role === 'admin') {
// Admin can see all announcements, but can filter by selected store
const storeId = req.query.storeId;
if (storeId) {
groupIds = [parseInt(storeId)];
}
} else {
// Other roles see only their assigned groups + global announcements
const userGroups = (user as any).userGroups;
groupIds = userGroups ? userGroups.map((ug: any) => ug.groupId) : [];
}
const announcements = await storage.getAnnouncements(groupIds.length > 0 ? groupIds : undefined);
res.json(announcements);
} catch (error) {
console.error("Error fetching announcements:", error);
res.status(500).json({ message: "Failed to fetch announcements" });
}
});
app.post('/api/announcements', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims ? req.user.claims.sub : req.user.id;
const user = await storage.getUserWithGroups(userId);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
// Only admin can create announcements
if (user.role !== 'admin') {
return res.status(403).json({ message: "Only administrators can create announcements" });
}
const announcementData = insertAnnouncementSchema.parse({
...req.body,
authorId: user.id,
});
const announcement = await storage.createAnnouncement(announcementData);
res.status(201).json(announcement);
} catch (error) {
console.error("Error creating announcement:", error);
if (error instanceof z.ZodError) {
return res.status(400).json({ message: "Validation error", errors: error.errors });
}
res.status(500).json({ message: "Failed to create announcement" });
}
});
app.delete('/api/announcements/:id', isAuthenticated, async (req: any, res) => {
try {
const userId = req.user.claims ? req.user.claims.sub : req.user.id;
const user = await storage.getUserWithGroups(userId);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
// Only admin can delete announcements
if (user.role !== 'admin') {
return res.status(403).json({ message: "Only administrators can delete announcements" });
}
const id = parseInt(req.params.id);
const success = await storage.deleteAnnouncement(id);
if (!success) {
return res.status(404).json({ message: "Announcement not found" });
}
res.json({ message: "Announcement deleted successfully" });
} catch (error) {
console.error("Error deleting announcement:", error);
res.status(500).json({ message: "Failed to delete announcement" });
}
});
app.post('/api/weather/geolocation', isAuthenticated, async (req: any, res) => {
try {
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
+109
View File
@@ -10,6 +10,7 @@ import {
customerOrders,
dlcProducts,
tasks,
announcements,
nocodbConfig,
invoiceVerificationCache,
savTickets,
@@ -47,6 +48,9 @@ import {
type Task,
type InsertTask,
type TaskWithRelations,
type Announcement,
type InsertAnnouncement,
type AnnouncementWithRelations,
type NocodbConfig,
type InsertNocodbConfig,
type InvoiceVerificationCache,
@@ -170,6 +174,11 @@ export interface IStorage {
updateTask(id: number, task: Partial<InsertTask>): Promise<Task>;
deleteTask(id: number): Promise<void>;
completeTask(id: number, completedBy?: string): Promise<void>;
// Announcement operations
createAnnouncement(announcement: InsertAnnouncement): Promise<Announcement>;
getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]>;
deleteAnnouncement(id: number): Promise<boolean>;
// SAV operations
getSavTickets(filters?: {
@@ -1580,6 +1589,55 @@ export class DatabaseStorage implements IStorage {
.where(eq(tasks.id, id));
}
// Announcement operations
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
const [announcement] = await db.insert(announcements).values(announcementData).returning();
return announcement;
}
async getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]> {
let query = db
.select({
announcement: announcements,
author: users,
group: groups,
})
.from(announcements)
.leftJoin(users, eq(announcements.authorId, users.id))
.leftJoin(groups, eq(announcements.groupId, groups.id));
// Filter by groupIds if provided (for admin filtering by store)
if (groupIds && groupIds.length > 0) {
query = query.where(
or(
isNull(announcements.groupId), // Global announcements (no group)
inArray(announcements.groupId, groupIds)
)
);
}
const results = await query.orderBy(
// Sort by priority (urgent > important > normal) then by date (newest first)
sql`CASE
WHEN ${announcements.priority} = 'urgent' THEN 3
WHEN ${announcements.priority} = 'important' THEN 2
ELSE 1
END DESC`,
desc(announcements.createdAt)
);
return results.map(result => ({
...result.announcement,
author: result.author!,
group: result.group,
}));
}
async deleteAnnouncement(id: number): Promise<boolean> {
const result = await db.delete(announcements).where(eq(announcements.id, id));
return result.rowCount ? result.rowCount > 0 : false;
}
// SAV operations
async getSavTickets(filters?: {
groupIds?: number[];
@@ -1861,6 +1919,7 @@ export class MemStorage implements IStorage {
private customerOrders = new Map<number, CustomerOrder>();
private dlcProducts = new Map<number, DlcProduct>();
private tasks = new Map<number, Task>();
private announcements = new Map<number, Announcement>();
private savTickets = new Map<number, SavTicket>();
private savTicketHistories = new Map<number, SavTicketHistory[]>();
@@ -1873,6 +1932,7 @@ export class MemStorage implements IStorage {
customerOrder: 1,
dlcProduct: 1,
task: 1,
announcement: 1,
savTicket: 1,
savTicketHistory: 1,
};
@@ -3272,6 +3332,55 @@ export class MemStorage implements IStorage {
// In development, this is a no-op
console.log('🧹 DEV: Weather cache cleared due to location change');
}
// Announcement operations
async createAnnouncement(announcementData: InsertAnnouncement): Promise<Announcement> {
const id = this.idCounters.announcement++;
const announcement: Announcement = {
id,
...announcementData,
createdAt: new Date(),
updatedAt: new Date(),
};
this.announcements.set(id, announcement);
return announcement;
}
async getAnnouncements(groupIds?: number[]): Promise<AnnouncementWithRelations[]> {
const announcements = Array.from(this.announcements.values());
let filteredAnnouncements = announcements;
// Filter by groupIds if provided (for admin filtering by store)
if (groupIds && groupIds.length > 0) {
filteredAnnouncements = announcements.filter(announcement =>
!announcement.groupId || groupIds.includes(announcement.groupId)
);
}
// Add relations
return filteredAnnouncements.map(announcement => {
const author = this.users.get(announcement.authorId);
const group = announcement.groupId ? this.groups.get(announcement.groupId) : undefined;
return {
...announcement,
author: author!,
group,
};
}).sort((a, b) => {
// Sort by priority (urgent > important > normal) then by date (newest first)
const priorityOrder = { urgent: 3, important: 2, normal: 1 };
const priorityDiff = (priorityOrder[b.priority as keyof typeof priorityOrder] || 1) -
(priorityOrder[a.priority as keyof typeof priorityOrder] || 1);
if (priorityDiff !== 0) return priorityDiff;
return new Date(b.createdAt!).getTime() - new Date(a.createdAt!).getTime();
});
}
async deleteAnnouncement(id: number): Promise<boolean> {
return this.announcements.delete(id);
}
}
// Use MemStorage in development, DatabaseStorage in production