mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
10 files changed
+1115
-47
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user