Merge pull request #171 from R0m1k3/1.1

1.1
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-16 12:38:10 +02:00
commit d717f02577
10 files changed
+1115 -47

No files matched your search

+309
View File
@@ -0,0 +1,309 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuthUnified } from "@/hooks/useAuthUnified";
import { useStore } from "@/components/Layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Megaphone, Plus, X, AlertTriangle, Info, Bell } from "lucide-react";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { insertAnnouncementSchema } from "@shared/schema";
import type { AnnouncementWithRelations } from "@shared/schema";
import { z } from "zod";
import { safeFormat, safeDate } from "@/lib/dateUtils";
const formSchema = insertAnnouncementSchema.extend({
groupId: z.coerce.number().optional(),
});
export default function AnnouncementCard() {
const { user } = useAuthUnified();
const { selectedStoreId } = useStore();
const queryClient = useQueryClient();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
content: "",
priority: "normal",
groupId: undefined,
},
});
// Fetch announcements
const { data: announcements = [] } = useQuery<AnnouncementWithRelations[]>({
queryKey: ['/api/announcements', selectedStoreId],
queryFn: async () => {
const params = new URLSearchParams();
if (selectedStoreId && user?.role === 'admin') {
params.append('storeId', selectedStoreId.toString());
}
const response = await fetch(`/api/announcements?${params}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch announcements');
}
return response.json();
},
});
// Fetch groups for admin
const { data: groups = [] } = useQuery<any[]>({
queryKey: ['/api/groups'],
enabled: user?.role === 'admin',
});
// Create announcement mutation
const createMutation = useMutation({
mutationFn: async (data: z.infer<typeof formSchema>) => {
const response = await fetch('/api/announcements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to create announcement');
}
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/announcements'] });
setIsCreateDialogOpen(false);
form.reset();
},
});
// Delete announcement mutation
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/api/announcements/${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to delete announcement');
}
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/announcements'] });
},
});
const onSubmit = (data: z.infer<typeof formSchema>) => {
createMutation.mutate(data);
};
const getPriorityIcon = (priority: string) => {
switch (priority) {
case 'urgent':
return <AlertTriangle className="h-4 w-4" />;
case 'important':
return <Bell className="h-4 w-4" />;
default:
return <Info className="h-4 w-4" />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'urgent':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
case 'important':
return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200';
default:
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
}
};
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
<div className="flex items-center space-x-2">
<Megaphone className="h-5 w-5 text-muted-foreground" />
<CardTitle className="text-lg">Informations</CardTitle>
</div>
{user?.role === 'admin' && (
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
Nouveau
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Nouvelle information</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Titre</FormLabel>
<FormControl>
<Input placeholder="Titre de l'information" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Contenu</FormLabel>
<FormControl>
<Textarea
placeholder="Contenu de l'information"
rows={4}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priorité</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Sélectionner une priorité" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="normal">Normale</SelectItem>
<SelectItem value="important">Importante</SelectItem>
<SelectItem value="urgent">Urgente</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="groupId"
render={({ field }) => (
<FormItem>
<FormLabel>Magasin (optionnel)</FormLabel>
<Select onValueChange={(value) => field.onChange(value ? parseInt(value) : undefined)}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Tous les magasins" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="">Tous les magasins</SelectItem>
{groups.map((group: any) => (
<SelectItem key={group.id} value={group.id.toString()}>
{group.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
>
Annuler
</Button>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending ? 'Création...' : 'Créer'}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
)}
</CardHeader>
<CardContent>
{announcements.length === 0 ? (
<div className="text-center text-muted-foreground py-6">
<Megaphone className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Aucune information disponible</p>
</div>
) : (
<div className="space-y-3 max-h-80 overflow-y-auto">
{announcements.map((announcement) => (
<div
key={announcement.id}
className="p-3 bg-muted/50 rounded-lg space-y-2"
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
{getPriorityIcon(announcement.priority)}
<h4 className="font-medium text-sm truncate">
{announcement.title}
</h4>
<Badge variant="secondary" className={getPriorityColor(announcement.priority)}>
{announcement.priority === 'urgent' ? 'Urgent' :
announcement.priority === 'important' ? 'Important' : 'Normal'}
</Badge>
</div>
<p className="text-sm text-muted-foreground line-clamp-2">
{announcement.content}
</p>
<div className="flex items-center justify-between text-xs text-muted-foreground mt-2">
<span>
Par {announcement.author.firstName} {announcement.author.lastName}
{announcement.group && ` • ${announcement.group.name}`}
</span>
<span>
{safeFormat(safeDate(announcement.createdAt), 'dd/MM/yyyy HH:mm')}
</span>
</div>
</div>
{user?.role === 'admin' && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 ml-2 flex-shrink-0"
onClick={() => deleteMutation.mutate(announcement.id)}
disabled={deleteMutation.isPending}
>
<X className="h-3 w-3" />
</Button>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
+3 -46
View File
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Calendar, Package, ShoppingCart, TrendingUp, Clock, MapPin, User, AlertTriangle, CheckCircle, Truck, FileText, BarChart3, Megaphone, Shield, XCircle, CheckSquare, Circle } from "lucide-react";
import { safeFormat, safeDate } from "@/lib/dateUtils";
import type { PublicityWithRelations } from "@shared/schema";
import AnnouncementCard from "@/components/AnnouncementCard";
export default function Dashboard() {
const { user } = useAuthUnified();
@@ -476,52 +477,8 @@ export default function Dashboard() {
{/* Section Rapprochement BL */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
{/* Statistiques des commandes clients */}
<Card className="bg-white border border-gray-200 shadow-lg hover:shadow-xl transition-shadow">
<CardHeader className="pb-4 border-b border-gray-100">
<CardTitle className="text-lg font-semibold text-gray-800 flex items-center">
<User className="h-5 w-5 mr-3 text-purple-600" />
Commandes Clients
</CardTitle>
</CardHeader>
<CardContent className="space-y-3 p-6">
<div className="flex items-center justify-between p-3 hover:bg-gray-50 transition-colors">
<div className="flex items-center space-x-3">
<div className="h-3 w-3 bg-red-500"></div>
<span className="text-sm font-medium text-gray-700">En attente</span>
</div>
<span className="font-semibold text-red-600 text-lg">{customerOrderStats.waiting}</span>
</div>
<div className="flex items-center justify-between p-3 hover:bg-gray-50 transition-colors">
<div className="flex items-center space-x-3">
<div className="h-3 w-3 bg-orange-500"></div>
<span className="text-sm font-medium text-gray-700">En cours</span>
</div>
<span className="font-semibold text-orange-600 text-lg">{customerOrderStats.inProgress}</span>
</div>
<div className="flex items-center justify-between p-3 hover:bg-gray-50 transition-colors">
<div className="flex items-center space-x-3">
<div className="h-3 w-3 bg-green-500"></div>
<span className="text-sm font-medium text-gray-700">Disponibles</span>
</div>
<span className="font-semibold text-green-600 text-lg">{customerOrderStats.available}</span>
</div>
<div className="flex items-center justify-between p-3 hover:bg-gray-50 transition-colors">
<div className="flex items-center space-x-3">
<div className="h-3 w-3 bg-blue-500"></div>
<span className="text-sm font-medium text-gray-700">Retirées</span>
</div>
<span className="font-semibold text-blue-600 text-lg">{customerOrderStats.withdrawn}</span>
</div>
<div className="flex items-center justify-between border-t border-gray-200 pt-3 mt-3 p-3">
<div className="flex items-center space-x-3">
<div className="h-3 w-3 bg-gray-500"></div>
<span className="text-sm font-semibold text-gray-800">Total commandes</span>
</div>
<span className="font-bold text-xl text-gray-800">{customerOrderStats.total}</span>
</div>
</CardContent>
</Card>
{/* Système d'informations - Remplace les commandes clients */}
<AnnouncementCard />
{/* Tâches à faire */}
<Card className="bg-white border border-gray-200 shadow-lg hover:shadow-xl transition-shadow">
+386
View File
@@ -0,0 +1,386 @@
-- Initial database schema for complete installation
-- This file creates all tables required for the application
-- Generated: 2025-08-16
-- Tables de base
CREATE TABLE "deliveries" (
"id" serial PRIMARY KEY NOT NULL,
"order_id" integer,
"supplier_id" integer NOT NULL,
"group_id" integer NOT NULL,
"delivered_date" timestamp,
"quantity" integer NOT NULL,
"unit" varchar NOT NULL,
"status" varchar DEFAULT 'planned' NOT NULL,
"created_by" varchar NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"bl_number" varchar,
"bl_amount" numeric(10, 2),
"invoice_reference" varchar,
"invoice_amount" numeric(10, 2),
"reconciled" boolean DEFAULT false,
"scheduled_date" date NOT NULL,
"notes" text,
"validated_at" timestamp,
CONSTRAINT "deliveries_status_check_fixed" CHECK ((status)::text = ANY ((ARRAY['planned'::character varying, 'delivered'::character varying])::text[]))
);
CREATE TABLE "orders" (
"id" serial PRIMARY KEY NOT NULL,
"supplier_id" integer NOT NULL,
"group_id" integer NOT NULL,
"planned_date" date NOT NULL,
"quantity" integer,
"unit" varchar,
"status" varchar DEFAULT 'pending' NOT NULL,
"created_by" varchar NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"notes" text,
CONSTRAINT "orders_status_check" CHECK ((status)::text = ANY ((ARRAY['pending'::character varying, 'planned'::character varying, 'delivered'::character varying])::text[]))
);
CREATE TABLE "groups" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"color" varchar NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"nocodb_config_id" integer,
"nocodb_table_id" varchar,
"nocodb_table_name" varchar,
"invoice_column_name" varchar DEFAULT 'Ref Facture'
);
CREATE TABLE "sessions" (
"sid" varchar PRIMARY KEY NOT NULL,
"sess" jsonb NOT NULL,
"expire" timestamp NOT NULL
);
CREATE TABLE "suppliers" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"contact" varchar,
"phone" varchar,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"has_dlc" boolean DEFAULT false
);
CREATE TABLE "user_groups" (
"user_id" varchar NOT NULL,
"group_id" integer NOT NULL,
"created_at" timestamp DEFAULT now()
);
CREATE TABLE "publicities" (
"id" serial PRIMARY KEY NOT NULL,
"pub_number" varchar(255) NOT NULL,
"designation" text NOT NULL,
"start_date" date NOT NULL,
"end_date" date NOT NULL,
"year" integer NOT NULL,
"created_by" varchar(255) NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE "session" (
"sid" varchar PRIMARY KEY NOT NULL,
"sess" json NOT NULL,
"expire" timestamp(6) NOT NULL
);
CREATE TABLE "users" (
"id" varchar PRIMARY KEY NOT NULL,
"email" varchar,
"first_name" varchar,
"last_name" varchar,
"profile_image_url" varchar,
"role" varchar DEFAULT 'employee' NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"password" varchar,
"username" varchar,
"password_changed" boolean DEFAULT false,
"name" varchar(255),
CONSTRAINT "users_email_unique" UNIQUE("email"),
CONSTRAINT "users_username_key" UNIQUE("username")
);
CREATE TABLE "roles" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(100) NOT NULL,
"display_name" varchar(200) NOT NULL,
"description" text,
"color" varchar(20) DEFAULT '#6b7280',
"is_system" boolean DEFAULT false,
"is_active" boolean DEFAULT true,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "roles_name_key" UNIQUE("name")
);
CREATE TABLE "permissions" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(100) NOT NULL,
"display_name" varchar(200) NOT NULL,
"description" text,
"category" varchar(50) NOT NULL,
"action" varchar(50) NOT NULL,
"resource" varchar(50) NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"is_system" boolean DEFAULT false,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "permissions_name_key" UNIQUE("name")
);
CREATE TABLE "nocodb_config" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"base_url" varchar NOT NULL,
"api_token" varchar NOT NULL,
"description" text,
"is_active" boolean DEFAULT true,
"created_by" varchar NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"project_id" varchar
);
CREATE TABLE "customer_orders" (
"id" serial PRIMARY KEY NOT NULL,
"order_taker" varchar NOT NULL,
"customer_name" varchar NOT NULL,
"customer_phone" varchar NOT NULL,
"product_designation" text NOT NULL,
"product_reference" varchar,
"gencode" varchar,
"status" varchar DEFAULT 'En attente de Commande' NOT NULL,
"deposit" numeric(10, 2) DEFAULT '0.00',
"is_promotional_price" boolean DEFAULT false,
"customer_notified" boolean DEFAULT false,
"group_id" integer NOT NULL,
"created_by" varchar NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"supplier_id" integer,
"quantity" integer DEFAULT 1 NOT NULL,
"customer_email" varchar(255),
"notes" text
);
CREATE TABLE "dlc_products" (
"id" serial PRIMARY KEY NOT NULL,
"product_name" varchar(255) NOT NULL,
"expiry_date" date NOT NULL,
"date_type" varchar(10) NOT NULL,
"quantity" integer NOT NULL,
"unit" varchar(50) NOT NULL,
"supplier_id" integer NOT NULL,
"location" varchar(255) NOT NULL,
"status" varchar(20) DEFAULT 'en_cours' NOT NULL,
"notes" text,
"alert_threshold" integer DEFAULT 3 NOT NULL,
"validated_at" timestamp,
"validated_by" varchar(255),
"group_id" integer NOT NULL,
"created_by" varchar(255) NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"gencode" varchar,
"name" varchar(255),
"dlc_date" date,
"product_code" varchar(255),
"description" text,
CONSTRAINT "dlc_products_date_type_check" CHECK ((date_type)::text = ANY ((ARRAY['dlc'::character varying, 'ddm'::character varying, 'dluo'::character varying])::text[])),
CONSTRAINT "dlc_products_status_check" CHECK ((status)::text = ANY ((ARRAY['en_cours'::character varying, 'expires_soon'::character varying, 'expires'::character varying, 'valides'::character varying])::text[]))
);
CREATE TABLE "tasks" (
"id" serial PRIMARY KEY NOT NULL,
"title" text NOT NULL,
"description" text,
"status" text DEFAULT 'pending' NOT NULL,
"priority" text DEFAULT 'medium' NOT NULL,
"due_date" timestamp,
"group_id" integer NOT NULL,
"created_by" text NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
"completed_at" timestamp,
"assigned_to" text DEFAULT '' NOT NULL,
"completed_by" varchar(255),
CONSTRAINT "tasks_status_check" CHECK (status = ANY (ARRAY['pending'::text, 'in_progress'::text, 'completed'::text, 'cancelled'::text])),
CONSTRAINT "tasks_priority_check" CHECK (priority = ANY (ARRAY['low'::text, 'medium'::text, 'high'::text, 'urgent'::text]))
);
-- Nouvelle table Announcements (Système d'informations)
CREATE TABLE "announcements" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar(255) NOT NULL,
"content" text NOT NULL,
"priority" varchar(20) NOT NULL DEFAULT 'normal',
"author_id" varchar(255) NOT NULL,
"group_id" integer,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "announcements_priority_check" CHECK (priority IN ('normal', 'important', 'urgent'))
);
CREATE TABLE "publicity_participations" (
"publicity_id" integer NOT NULL,
"group_id" integer NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "publicity_participations_pkey" PRIMARY KEY("publicity_id","group_id")
);
CREATE TABLE "role_permissions" (
"role_id" integer NOT NULL,
"permission_id" integer NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "role_permissions_pkey" PRIMARY KEY("role_id","permission_id")
);
CREATE TABLE "user_roles" (
"user_id" varchar(255) NOT NULL,
"role_id" integer NOT NULL,
"assigned_by" varchar(255) NOT NULL,
"assigned_at" timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_roles_pkey" PRIMARY KEY("user_id","role_id")
);
-- Tables pour SAV (Service Après-Vente)
CREATE TABLE IF NOT EXISTS "sav_tickets" (
"id" serial PRIMARY KEY NOT NULL,
"ticket_number" varchar(50) NOT NULL UNIQUE,
"title" varchar(255) NOT NULL,
"description" text NOT NULL,
"status" varchar(20) NOT NULL DEFAULT 'nouveau',
"priority" varchar(20) NOT NULL DEFAULT 'normale',
"supplier_id" integer NOT NULL,
"group_id" integer NOT NULL,
"created_by" varchar(255) NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sav_tickets_status_check" CHECK (status IN ('nouveau', 'en_cours', 'en_attente', 'resolu', 'ferme')),
CONSTRAINT "sav_tickets_priority_check" CHECK (priority IN ('faible', 'normale', 'elevee', 'critique'))
);
CREATE TABLE IF NOT EXISTS "sav_ticket_history" (
"id" serial PRIMARY KEY NOT NULL,
"ticket_id" integer NOT NULL,
"action" varchar(50) NOT NULL,
"description" text NOT NULL,
"created_by" varchar(255) NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP
);
-- Tables pour sauvegarde et configuration météo
CREATE TABLE IF NOT EXISTS "database_backups" (
"id" serial PRIMARY KEY NOT NULL,
"filename" varchar(255) NOT NULL,
"file_size" bigint NOT NULL,
"backup_type" varchar(50) NOT NULL,
"created_by" varchar(255) NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"status" varchar(20) DEFAULT 'completed'
);
CREATE TABLE IF NOT EXISTS "weather_settings" (
"id" serial PRIMARY KEY NOT NULL,
"api_key" varchar(255) NOT NULL,
"location" varchar(255) NOT NULL,
"is_active" boolean DEFAULT true,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "weather_data" (
"id" serial PRIMARY KEY NOT NULL,
"date" varchar(10) NOT NULL,
"is_current_year" boolean NOT NULL,
"temp_max" varchar(10),
"temp_min" varchar(10),
"conditions" varchar(255),
"icon" varchar(50),
"description" text,
"location" varchar(255),
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date, is_current_year)
);
CREATE TABLE IF NOT EXISTS "invoice_verification_cache" (
"id" serial PRIMARY KEY NOT NULL,
"invoice_reference" varchar(255) NOT NULL,
"nocodb_config_id" integer NOT NULL,
"verification_result" text NOT NULL,
"is_found" boolean NOT NULL,
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
"expires_at" timestamp NOT NULL,
UNIQUE(invoice_reference, nocodb_config_id)
);
-- Contraintes de clés étrangères
ALTER TABLE "dlc_products" ADD CONSTRAINT "dlc_products_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "public"."suppliers"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "dlc_products" ADD CONSTRAINT "dlc_products_validated_by_fkey" FOREIGN KEY ("validated_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "dlc_products" ADD CONSTRAINT "dlc_products_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "dlc_products" ADD CONSTRAINT "dlc_products_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_completed_by_fkey" FOREIGN KEY ("completed_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "announcements" ADD CONSTRAINT "announcements_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE CASCADE;
ALTER TABLE "announcements" ADD CONSTRAINT "announcements_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE CASCADE;
ALTER TABLE "publicity_participations" ADD CONSTRAINT "publicity_participations_publicity_id_fkey" FOREIGN KEY ("publicity_id") REFERENCES "public"."publicities"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "publicity_participations" ADD CONSTRAINT "publicity_participations_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "public"."permissions"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "sav_tickets" ADD CONSTRAINT "sav_tickets_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "public"."suppliers"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "sav_tickets" ADD CONSTRAINT "sav_tickets_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "sav_tickets" ADD CONSTRAINT "sav_tickets_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
ALTER TABLE "sav_ticket_history" ADD CONSTRAINT "sav_ticket_history_ticket_id_fkey" FOREIGN KEY ("ticket_id") REFERENCES "public"."sav_tickets"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "sav_ticket_history" ADD CONSTRAINT "sav_ticket_history_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
-- Index pour optimisation des performances
CREATE INDEX "idx_deliveries_bl_number" ON "deliveries" USING btree ("bl_number" text_ops);
CREATE INDEX "idx_deliveries_group_id" ON "deliveries" USING btree ("group_id" int4_ops);
CREATE INDEX "idx_deliveries_invoice_ref" ON "deliveries" USING btree ("invoice_reference" text_ops);
CREATE INDEX "idx_deliveries_status" ON "deliveries" USING btree ("status" text_ops);
CREATE INDEX "idx_orders_group_id" ON "orders" USING btree ("group_id" int4_ops);
CREATE INDEX "idx_orders_status" ON "orders" USING btree ("status" text_ops);
CREATE INDEX "IDX_session_expire" ON "sessions" USING btree ("expire" timestamp_ops);
CREATE INDEX "idx_publicities_start_date" ON "publicities" USING btree ("start_date" date_ops);
CREATE INDEX "idx_publicities_year" ON "publicities" USING btree ("year" int4_ops);
CREATE INDEX "idx_dlc_products_expiry_date" ON "dlc_products" USING btree ("expiry_date" date_ops);
CREATE INDEX "idx_dlc_products_group_id" ON "dlc_products" USING btree ("group_id" int4_ops);
CREATE INDEX "idx_dlc_products_status" ON "dlc_products" USING btree ("status" text_ops);
CREATE INDEX "idx_dlc_products_supplier_id" ON "dlc_products" USING btree ("supplier_id" int4_ops);
CREATE INDEX "idx_announcements_priority" ON "announcements" USING btree ("priority" text_ops);
CREATE INDEX "idx_announcements_created_at" ON "announcements" USING btree ("created_at" timestamp_ops);
CREATE INDEX "idx_announcements_author_id" ON "announcements" USING btree ("author_id" text_ops);
CREATE INDEX "idx_announcements_group_id" ON "announcements" USING btree ("group_id" int4_ops);
CREATE INDEX "idx_role_permissions_permission_id" ON "role_permissions" USING btree ("permission_id" int4_ops);
CREATE INDEX "idx_role_permissions_role_id" ON "role_permissions" USING btree ("role_id" int4_ops);
CREATE INDEX "idx_user_roles_assigned_by" ON "user_roles" USING btree ("assigned_by" text_ops);
CREATE INDEX "idx_user_roles_role_id" ON "user_roles" USING btree ("role_id" int4_ops);
CREATE INDEX "idx_user_roles_user_id" ON "user_roles" USING btree ("user_id" text_ops);
CREATE INDEX "idx_sav_tickets_status" ON "sav_tickets" USING btree ("status" text_ops);
CREATE INDEX "idx_sav_tickets_priority" ON "sav_tickets" USING btree ("priority" text_ops);
CREATE INDEX "idx_sav_tickets_group_id" ON "sav_tickets" USING btree ("group_id" int4_ops);
CREATE INDEX "idx_sav_tickets_created_at" ON "sav_tickets" USING btree ("created_at" timestamp_ops);
CREATE INDEX "idx_sav_ticket_history_ticket_id" ON "sav_ticket_history" USING btree ("ticket_id" int4_ops);
CREATE INDEX "idx_weather_data_date_year" ON "weather_data" USING btree ("date" text_ops, "is_current_year" bool_ops);
CREATE INDEX "idx_invoice_verification_expires" ON "invoice_verification_cache" USING btree ("expires_at" timestamp_ops);
-- Commentaires sur les tables
COMMENT ON TABLE "announcements" IS 'Admin-managed announcements/information system';
COMMENT ON COLUMN "announcements"."priority" IS 'Priority level: normal, important, urgent';
COMMENT ON COLUMN "announcements"."group_id" IS 'NULL for global announcements, specific group_id for store-specific announcements';
COMMENT ON TABLE "sav_tickets" IS 'Service après-vente tickets management';
COMMENT ON TABLE "weather_data" IS 'Cache for weather API data';
COMMENT ON TABLE "invoice_verification_cache" IS 'Cache for NocoDB invoice verification results';
@@ -0,0 +1,27 @@
-- Migration: Create announcements table
-- Date: 2025-08-16
-- Description: Create announcements table for admin-managed information system
CREATE TABLE IF NOT EXISTS announcements (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
priority VARCHAR(20) NOT NULL DEFAULT 'normal' CHECK (priority IN ('normal', 'important', 'urgent')),
author_id VARCHAR(255) NOT NULL,
group_id INTEGER NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
);
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_announcements_priority ON announcements(priority);
CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_announcements_author_id ON announcements(author_id);
CREATE INDEX IF NOT EXISTS idx_announcements_group_id ON announcements(group_id);
-- Comment on table
COMMENT ON TABLE announcements IS 'Admin-managed announcements/information system';
COMMENT ON COLUMN announcements.priority IS 'Priority level: normal, important, urgent';
COMMENT ON COLUMN announcements.group_id IS 'NULL for global announcements, specific group_id for store-specific announcements';
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* Database Migration Script
* Automatically runs database migrations for production deployment
*/
const { Pool } = require('pg');
const fs = require('fs');
const path = require('path');
// Database connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
async function runMigrations() {
console.log('🔄 Starting database migrations...');
try {
// Create migrations table if it doesn't exist
await pool.query(`
CREATE TABLE IF NOT EXISTS migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(255) UNIQUE NOT NULL,
executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`);
console.log('✅ Migrations table ready');
// Get list of executed migrations
const { rows: executedMigrations } = await pool.query(
'SELECT filename FROM migrations ORDER BY executed_at'
);
const executedSet = new Set(executedMigrations.map(row => row.filename));
// Read migration files
const migrationsDir = path.join(__dirname);
const files = fs.readdirSync(migrationsDir)
.filter(file => file.endsWith('.sql'))
.sort();
console.log(`📁 Found ${files.length} migration files`);
let appliedCount = 0;
for (const file of files) {
if (executedSet.has(file)) {
console.log(`⏭️ Skipping ${file} (already executed)`);
continue;
}
console.log(`🔄 Executing migration: ${file}`);
const filePath = path.join(migrationsDir, file);
const sql = fs.readFileSync(filePath, 'utf8');
// Execute migration in a transaction
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(sql);
await client.query(
'INSERT INTO migrations (filename) VALUES ($1)',
[file]
);
await client.query('COMMIT');
console.log(`✅ Successfully executed: ${file}`);
appliedCount++;
} catch (error) {
await client.query('ROLLBACK');
throw new Error(`Failed to execute ${file}: ${error.message}`);
} finally {
client.release();
}
}
if (appliedCount === 0) {
console.log('✅ All migrations are up to date');
} else {
console.log(`✅ Successfully applied ${appliedCount} migration(s)`);
}
} catch (error) {
console.error('❌ Migration failed:', error.message);
process.exit(1);
} finally {
await pool.end();
}
}
// Run migrations if this file is executed directly
if (require.main === module) {
runMigrations();
}
module.exports = { runMigrations };
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* Production deployment script
* Runs database migrations and starts the application
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
async function deployToProduction() {
console.log('🚀 Starting production deployment...');
try {
// Check if we're in production environment
if (process.env.NODE_ENV !== 'production') {
console.log('⚠️ Warning: NODE_ENV is not set to production');
}
// Check if DATABASE_URL is available
if (!process.env.DATABASE_URL) {
console.error('❌ DATABASE_URL environment variable is required for production');
process.exit(1);
}
console.log('✅ Environment variables verified');
// Run database migrations
console.log('🔄 Running database migrations...');
const migrateScriptPath = path.join(__dirname, 'migrations', 'migrate.js');
if (fs.existsSync(migrateScriptPath)) {
execSync(`node ${migrateScriptPath}`, { stdio: 'inherit' });
console.log('✅ Database migrations completed');
} else {
console.log('⚠️ No migration script found, skipping migrations');
}
// Start the application
console.log('🚀 Starting application...');
execSync('npm start', { stdio: 'inherit' });
} catch (error) {
console.error('❌ Deployment failed:', error.message);
process.exit(1);
}
}
// Execute if this script is run directly
if (require.main === module) {
deployToProduction();
}
module.exports = { deployToProduction };
+1
View File
@@ -35,6 +35,7 @@ The platform is built with a clear separation of concerns, utilizing a modern fu
- **Invoice Verification Cache:** Robust caching system for invoice verification results with automatic expiration and conflict resolution. Enhanced with unique constraint handling and graceful error management for production stability (August 2025). Fixed production SSL certificate errors and fetch timeout loops with enhanced error handling, network timeout protection, and error result caching to prevent infinite retry loops (August 2025).
- **UI Scrollbar Optimization:** Eliminated redundant vertical scrollbar on the right side of the screen while preserving necessary internal page scrolling. Implemented global CSS rules with `overflow: hidden` on html/body/root and Layout height management to prevent vertical overflow issues in production (August 2025).
- **Task Management:** Système complet de gestion des tâches avec fonctionnalités temporelles avancées - tri intelligent (pending > priorité > date), dates d'échéance avec sélecteur calendrier, indicateurs visuels pour tâches en retard/urgentes, filtres par échéance (aujourd'hui, cette semaine, en retard, sans échéance), vue Kanban et liste, badges colorés pour statut temporel. Interface optimisée pour suivi prioritaire des tâches non validées. Correction critique de l'isolation des données : chaque magasin ne voit maintenant que ses propres tâches (Août 2025). **PRODUCTION FIX (Août 2025)** : Résolution du problème de rechargements multiples nécessaires pour l'administrateur - le cache des tâches s'invalide maintenant automatiquement lors du changement de magasin sélectionné.
- **Information System (Announcements):** Système d'informations complet remplaçant la carte commandes clients dans le dashboard - gestion d'annonces avec niveaux de priorité (normal, important, urgent), contrôle d'accès basé sur les rôles (admin : CRUD complet, autres rôles : lecture seule), tri intelligent par priorité et date, filtrage par magasin pour administrateurs, interface moderne avec badges colorés et indicateurs visuels. Intégration complète avec migrations automatiques et fichier init.sql complet pour nouvelles installations (Août 2025).
- **DLC Module Permissions:** Correction des permissions de validation DLC pour permettre aux admin, directeur et manager de valider les produits DLC. Ajout de logs de debug pour traçabilité des validations (Août 2025).
- **User and Permission Management:** Granular access controls ensuring that users (e.g., employees) have appropriate permissions for tasks like creating customer orders.
- **Reporting:** Statistical reporting, including monthly summaries of deliveries based on `deliveredDate`.
+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
+39 -1
View File
@@ -255,6 +255,18 @@ export const tasks = pgTable("tasks", {
updatedAt: timestamp("updated_at").defaultNow(),
});
// Announcements - Système d'information du dashboard
export const announcements = pgTable("announcements", {
id: serial("id").primaryKey(),
title: varchar("title").notNull(), // Titre du message
content: text("content").notNull(), // Contenu du message
priority: varchar("priority").notNull().default("normal"), // normal, important, urgent
authorId: varchar("author_id").notNull(), // Référence vers l'utilisateur créateur
groupId: integer("group_id"), // Filtrage par magasin (null = global)
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
// Relations
export const usersRelations = relations(users, ({ many }) => ({
userGroups: many(userGroups),
@@ -265,6 +277,7 @@ export const usersRelations = relations(users, ({ many }) => ({
createdDlcProducts: many(dlcProducts),
createdTasks: many(tasks),
assignedTasks: many(tasks),
createdAnnouncements: many(announcements),
}));
export const groupsRelations = relations(groups, ({ one, many }) => ({
@@ -275,7 +288,7 @@ export const groupsRelations = relations(groups, ({ one, many }) => ({
customerOrders: many(customerOrders),
dlcProducts: many(dlcProducts),
tasks: many(tasks),
announcements: many(announcements),
}));
export const userGroupsRelations = relations(userGroups, ({ one }) => ({
@@ -396,6 +409,17 @@ export const tasksRelations = relations(tasks, ({ one }) => ({
}),
}));
export const announcementsRelations = relations(announcements, ({ one }) => ({
author: one(users, {
fields: [announcements.authorId],
references: [users.id],
}),
group: one(groups, {
fields: [announcements.groupId],
references: [groups.id],
}),
}));
// Zod schemas
export const insertUserSchema = createInsertSchema(users).pick({
id: true,
@@ -501,6 +525,12 @@ export const insertTaskSchema = createInsertSchema(tasks).omit({
dueDate: z.coerce.date().optional().nullable(), // Convertit automatiquement les chaînes en Date
});
export const insertAnnouncementSchema = createInsertSchema(announcements).omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const insertNocodbConfigSchema = createInsertSchema(nocodbConfig).omit({
id: true,
createdAt: true,
@@ -557,6 +587,8 @@ export type DlcProductFrontend = Omit<DlcProduct, 'expiryDate'> & { dlcDate: Dat
export type InsertDlcProductFrontend = z.infer<typeof insertDlcProductFrontendSchema>;
export type Task = typeof tasks.$inferSelect;
export type InsertTask = z.infer<typeof insertTaskSchema>;
export type Announcement = typeof announcements.$inferSelect;
export type InsertAnnouncement = z.infer<typeof insertAnnouncementSchema>;
// Complex types with relations
@@ -605,6 +637,12 @@ export type TaskWithRelations = Task & {
group: Group;
};
// Announcement with relations type
export type AnnouncementWithRelations = Announcement & {
author: User;
group?: Group;
};
// SAV (Service Après-Vente) Tables
export const savTickets = pgTable("sav_tickets", {
id: serial("id").primaryKey(),