Restored to '3891ae828b859a29b86a84154ffef43740610335'

Replit-Restored-To: 3891ae828b
This commit is contained in:
michaelschal committed 2025-07-19 12:37:14 +00:00
1 parent 2f847dd333
commit 0821b70b7d
8 files changed
+212 -694

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

@@ -1,450 +0,0 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 16.9
-- Dumped by pg_dump version 16.5
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
ALTER TABLE IF EXISTS ONLY public.users DROP CONSTRAINT IF EXISTS users_store_id_stores_id_fk;
ALTER TABLE IF EXISTS ONLY public.sales DROP CONSTRAINT IF EXISTS sales_user_id_users_id_fk;
ALTER TABLE IF EXISTS ONLY public.sales DROP CONSTRAINT IF EXISTS sales_store_id_stores_id_fk;
ALTER TABLE IF EXISTS ONLY public.sale_products DROP CONSTRAINT IF EXISTS sale_products_sale_id_sales_id_fk;
DROP INDEX IF EXISTS public."IDX_session_expire";
ALTER TABLE IF EXISTS ONLY public.users DROP CONSTRAINT IF EXISTS users_username_unique;
ALTER TABLE IF EXISTS ONLY public.users DROP CONSTRAINT IF EXISTS users_pkey;
ALTER TABLE IF EXISTS ONLY public.users DROP CONSTRAINT IF EXISTS users_email_unique;
ALTER TABLE IF EXISTS ONLY public.stores DROP CONSTRAINT IF EXISTS stores_pkey;
ALTER TABLE IF EXISTS ONLY public.sessions DROP CONSTRAINT IF EXISTS sessions_pkey;
ALTER TABLE IF EXISTS ONLY public.sales DROP CONSTRAINT IF EXISTS sales_pkey;
ALTER TABLE IF EXISTS ONLY public.sale_products DROP CONSTRAINT IF EXISTS sale_products_pkey;
ALTER TABLE IF EXISTS public.users ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.stores ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.sales ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.sale_products ALTER COLUMN id DROP DEFAULT;
DROP SEQUENCE IF EXISTS public.users_id_seq;
DROP TABLE IF EXISTS public.users;
DROP SEQUENCE IF EXISTS public.stores_id_seq;
DROP TABLE IF EXISTS public.stores;
DROP TABLE IF EXISTS public.sessions;
DROP SEQUENCE IF EXISTS public.sales_id_seq;
DROP TABLE IF EXISTS public.sales;
DROP SEQUENCE IF EXISTS public.sale_products_id_seq;
DROP TABLE IF EXISTS public.sale_products;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: sale_products; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.sale_products (
id integer NOT NULL,
sale_id integer NOT NULL,
type_article character varying(255) NOT NULL,
categorie character varying(2) NOT NULL,
quantite integer NOT NULL,
gencode character varying(13) NOT NULL,
created_at timestamp without time zone DEFAULT now()
);
--
-- Name: sale_products_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.sale_products_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: sale_products_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.sale_products_id_seq OWNED BY public.sale_products.id;
--
-- Name: sales; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.sales (
id integer NOT NULL,
store_id integer NOT NULL,
user_id integer NOT NULL,
"timestamp" timestamp without time zone DEFAULT now(),
vendeur character varying(255) NOT NULL,
nom character varying(255) NOT NULL,
prenom character varying(255) NOT NULL,
date_naissance character varying(10) NOT NULL,
lieu_naissance character varying(255),
type_identite character varying(50) NOT NULL,
numero_identite character varying(100) NOT NULL,
autorite_delivrance character varying(255),
date_delivrance character varying(10) NOT NULL,
photo_recto text,
photo_verso text,
mode_paiement character varying(50) DEFAULT 'Espèce'::character varying NOT NULL,
photo_ticket text
);
--
-- Name: sales_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.sales_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: sales_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.sales_id_seq OWNED BY public.sales.id;
--
-- Name: sessions; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.sessions (
sid character varying NOT NULL,
sess jsonb NOT NULL,
expire timestamp without time zone NOT NULL
);
--
-- Name: stores; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.stores (
id integer NOT NULL,
name character varying(255) NOT NULL,
address text,
phone character varying(20),
email character varying(255),
is_active boolean DEFAULT true,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
--
-- Name: stores_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.stores_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: stores_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.stores_id_seq OWNED BY public.stores.id;
--
-- Name: users; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.users (
id integer NOT NULL,
username character varying(50) NOT NULL,
password character varying(255) NOT NULL,
email character varying(255),
first_name character varying(100),
last_name character varying(100),
role character varying(20) DEFAULT 'employee'::character varying NOT NULL,
store_id integer,
is_active boolean DEFAULT true,
created_at timestamp without time zone DEFAULT now(),
updated_at timestamp without time zone DEFAULT now()
);
--
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
--
-- Name: sale_products id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sale_products ALTER COLUMN id SET DEFAULT nextval('public.sale_products_id_seq'::regclass);
--
-- Name: sales id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sales ALTER COLUMN id SET DEFAULT nextval('public.sales_id_seq'::regclass);
--
-- Name: stores id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.stores ALTER COLUMN id SET DEFAULT nextval('public.stores_id_seq'::regclass);
--
-- Name: users id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
--
-- Data for Name: sale_products; Type: TABLE DATA; Schema: public; Owner: -
--
COPY public.sale_products (id, sale_id, type_article, categorie, quantite, gencode, created_at) FROM stdin;
1 1 Pétard aérien F3 1 3660092323745 2025-07-09 19:05:20.904549
2 2 Pétard aérien F3 1 3660092323745 2025-07-09 19:07:15.650704
3 4 Batterie F3 1 3660092323745 2025-07-09 19:11:36.505833
4 5 Fusée F2 10 3660092323745 2025-07-09 19:16:46.797165
5 6 Chandelle romaine F2 1 3660092323745 2025-07-10 00:08:19.097391
6 7 Batterie F3 1 3660092323745 2025-07-11 15:36:32.19478
7 8 Batterie Test F2 1 1234567890123 2025-07-11 18:27:55.231199
8 9 Chandelle F2 1 1234567890123 2025-07-11 18:38:21.913752
9 10 Chandelle F2 1 1234567890124 2025-07-11 18:38:45.251595
10 11 Chandelle F2 1 1234567890125 2025-07-11 18:39:03.438134
11 12 Chandelle F2 1 1234567890999 2025-07-11 18:48:31.159612
12 13 Chandelle F2 1 1234567890999 2025-07-11 19:02:01.423938
13 14 Test F2 1 1234567890123 2025-07-13 11:24:50.830933
14 16 Pétard à mèche F3 1 3560238902912 2025-07-19 10:35:03.315083
15 16 Pétard à composition flash F3 1 3560238902912 2025-07-19 10:35:03.315083
16 17 Batterie F3 1 3560238902912 2025-07-19 10:36:48.91106
17 17 Combinaison nécessitant un support externe F3 1 3560238902912 2025-07-19 10:36:48.91106
\.
--
-- Data for Name: sales; Type: TABLE DATA; Schema: public; Owner: -
--
COPY public.sales (id, store_id, user_id, "timestamp", vendeur, nom, prenom, date_naissance, lieu_naissance, type_identite, numero_identite, autorite_delivrance, date_delivrance, photo_recto, photo_verso, mode_paiement, photo_ticket) FROM stdin;
1 2 1 2025-07-09 19:05:20.904549 Moi SCHAL Michaël 2025-07-02 Bar le duc CNI qsdfqsdgc23445 Frouard 2025-07-01 \N \N Espèce \N
2 2 1 2025-07-09 19:07:15.650704 Moi SCHAL Michaël 2025-07-02 Bar le duc CNI qsdfqsdgc23445 Frouard 2025-07-01 \N \N Espèce \N
4 1 1 2025-07-09 19:11:36.505833 Moi SCHAL Michaël 2025-07-18 Bar le duc CNI qsdfqsdgc23445 Frouard 2025-07-01 \N \N Espèce \N
5 2 2 2025-07-09 19:16:46.797165 Moi ASSOCIATION GRAND AIR Anne-Laure 2025-06-03 Bar le duc Passeport qsdfqsdgc23445 Frouard 2025-07-01 \N \N Espèce \N
6 1 1 2025-07-10 00:08:19.097391 Moi SCHAL Anne-Laure 2018-02-10 Bar le duc Passeport qsdfqsdgc23445 Frouard 2025-07-01 \N \N Espèce \N
7 2 1 2025-07-11 15:36:32.19478 Moi SCHAL Michaël 1998-02-05 Bar le duc CNI qsdfqsdgc23445 Frouard 2025-07-11 \N \N Espèce \N
8 2 1 2025-07-11 18:27:55.231199 Test Vendeur DUPONT Jean 01/01/1980 Paris Carte d'identité ABC123456 Préfecture de Paris 01/01/2020 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== Carte bancaire data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
9 1 1 2025-07-11 18:38:21.913752 Test Vendeur TEST Photo 1990-01-01 Test Passeport 123456789 Test 2025-01-01 \N \N Espèce \N
10 1 1 2025-07-11 18:38:45.251595 Test Vendeur 2 TEST2 Photo2 1990-01-01 Test Passeport 123456789 Test 2025-01-01 \N \N Espèce \N
11 1 1 2025-07-11 18:39:03.438134 Test Debug DEBUG Test 1990-01-01 Test Passeport 123456789 Test 2025-01-01 \N \N Espèce data:image/jpeg;base64,TEST_TICKET_DATA
12 1 1 2025-07-11 18:48:31.159612 Test Final FINAL Test 1990-01-01 Test Passeport 123456789 Test 2025-01-01 \N \N Espèce data:image/jpeg;base64,TICKET_TEST_FINAL
13 1 1 2025-07-11 19:02:01.423938 Production Test PROD Test 1990-01-01 Test Passeport 123456789 Test 2025-01-01 \N \N Espèce data:image/jpeg;base64,PRODUCTION_TEST_TICKET
14 1 1 2025-07-13 11:24:50.830933 TEST PRODUCTION PRODUCTION Ready 1990-01-01 Test CNI 123456789 Test 2025-01-01 \N \N Espèce data:image/jpeg;base64,PRODUCTION_READY_TEST
16 2 1 2025-07-19 10:35:03.315083 Michael SCHAL SCHAL Michael 2025-07-04 Bar le duc CNI qsdfqsdgc23445 Frouard 2025-07-02 \N \N Espèce \N
17 2 1 2025-07-19 10:36:48.91106 Michael SCHAL SCHAL Michael 2025-07-10 Bar le duc CNI qsdfqsdgc23445 Frouard 2025-07-23 data:image/jpeg;base64,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 truncated
\.
--
-- Data for Name: sessions; Type: TABLE DATA; Schema: public; Owner: -
--
COPY public.sessions (sid, sess, expire) FROM stdin;
JCyX3kHULmnhgSEZU0FaKOzcDWM1b-Mf {"cookie": {"path": "/", "secure": false, "expires": "2025-07-20T10:33:27.610Z", "httpOnly": true, "originalMaxAge": 86400000}, "userId": 1} 2025-07-20 10:33:28
q_4oSSI08wziMN8PqUr-eoLkdu9QBCk_ {"cookie": {"path": "/", "secure": false, "expires": "2025-07-20T11:17:09.311Z", "httpOnly": true, "originalMaxAge": 86400000}, "userId": 1} 2025-07-20 11:17:10
galt5a8EH_CKmEsKVf1_eMrR7UlxNoYI {"cookie": {"path": "/", "secure": false, "expires": "2025-07-20T10:02:28.332Z", "httpOnly": true, "originalMaxAge": 86400000}, "userId": 1} 2025-07-20 12:25:32
\.
--
-- Data for Name: stores; Type: TABLE DATA; Schema: public; Owner: -
--
COPY public.stores (id, name, address, phone, email, is_active, created_at, updated_at) FROM stdin;
1 Frouard À définir t 2025-07-09 18:37:53.432996 2025-07-09 18:37:53.432996
2 Houdemont t 2025-07-09 19:00:30.782965 2025-07-09 19:00:30.782965
\.
--
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: -
--
COPY public.users (id, username, password, email, first_name, last_name, role, store_id, is_active, created_at, updated_at) FROM stdin;
2 FFNANCY michaelschal@gmail.com manager 1 t 2025-07-09 18:55:01.413195 2025-07-09 18:55:01.413195
3 gael $2b$12$G0BTV6XVgO5B9DdWeU4nzO8N6x1m7jsEUw2VUnrb2G4mmq8Tj1lVi \N administrator \N t 2025-07-10 08:47:04.063397 2025-07-10 08:47:04.063397
4 ff292 $2b$12$vQYWysmorDWUUp6bXsqMHOj//087IiwIZQzkB9gABqXFyISaiqZMK \N employee 1 t 2025-07-10 09:48:06.549645 2025-07-10 09:48:06.549645
5 ff579 $2b$12$wKO1Ihm44GcsaSaAKFfD7uO7q8gCzTNbG7vdW0SFT8Z7RGshO9o2W \N employee 2 t 2025-07-10 09:48:22.256912 2025-07-10 09:48:22.256912
1 admin $2b$12$Q6bxVsV0.uE78jgh9LxnYukLB6oS9jPkFeIVdrlkAM5Dwr7YQ5qTK michaelschal@ffest.fr Administrateur Principal admin 1 t 2025-07-09 18:37:53.865533 2025-07-09 18:37:53.865533
\.
--
-- Name: sale_products_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
--
SELECT pg_catalog.setval('public.sale_products_id_seq', 17, true);
--
-- Name: sales_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
--
SELECT pg_catalog.setval('public.sales_id_seq', 17, true);
--
-- Name: stores_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
--
SELECT pg_catalog.setval('public.stores_id_seq', 2, true);
--
-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
--
SELECT pg_catalog.setval('public.users_id_seq', 5, true);
--
-- Name: sale_products sale_products_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sale_products
ADD CONSTRAINT sale_products_pkey PRIMARY KEY (id);
--
-- Name: sales sales_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sales
ADD CONSTRAINT sales_pkey PRIMARY KEY (id);
--
-- Name: sessions sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sessions
ADD CONSTRAINT sessions_pkey PRIMARY KEY (sid);
--
-- Name: stores stores_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.stores
ADD CONSTRAINT stores_pkey PRIMARY KEY (id);
--
-- Name: users users_email_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_email_unique UNIQUE (email);
--
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
--
-- Name: users users_username_unique; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_username_unique UNIQUE (username);
--
-- Name: IDX_session_expire; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX "IDX_session_expire" ON public.sessions USING btree (expire);
--
-- Name: sale_products sale_products_sale_id_sales_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sale_products
ADD CONSTRAINT sale_products_sale_id_sales_id_fk FOREIGN KEY (sale_id) REFERENCES public.sales(id) ON DELETE CASCADE;
--
-- Name: sales sales_store_id_stores_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sales
ADD CONSTRAINT sales_store_id_stores_id_fk FOREIGN KEY (store_id) REFERENCES public.stores(id);
--
-- Name: sales sales_user_id_users_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.sales
ADD CONSTRAINT sales_user_id_users_id_fk FOREIGN KEY (user_id) REFERENCES public.users(id);
--
-- Name: users users_store_id_stores_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT users_store_id_stores_id_fk FOREIGN KEY (store_id) REFERENCES public.stores(id);
--
-- PostgreSQL database dump complete
--
+36 -50
View File
@@ -308,52 +308,44 @@ export default function Administration() {
deleteStoreMutation.mutate(store.id);
};
// SQL Backup mutations
// Backup mutations
const exportBackupMutation = useMutation({
mutationFn: async () => {
const response = await fetch('/api/admin/backup/export', {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
throw new Error('Failed to export SQL backup');
}
return response.text(); // Get SQL content as text
const response = await apiRequest('/api/admin/backup/export');
return response.json();
},
onSuccess: (sqlContent) => {
// Create and download SQL file
const blob = new Blob([sqlContent], { type: 'application/sql' });
onSuccess: (data) => {
// Créer et télécharger le fichier JSON
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `backup_${new Date().toISOString().split('T')[0]}_${Date.now()}.sql`;
link.download = `backup_${new Date().toISOString().split('T')[0]}_${Date.now()}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast({
title: "Sauvegarde SQL exportée",
description: "Le fichier de sauvegarde SQL a été téléchargé",
title: "Sauvegarde exportée",
description: "Le fichier de sauvegarde a été téléchargé",
variant: "success",
});
},
onError: (error) => {
onError: () => {
toast({
title: "Erreur d'export SQL",
description: error instanceof Error ? error.message : "Impossible d'exporter la sauvegarde SQL",
title: "Erreur",
description: "Impossible d'exporter la sauvegarde",
variant: "destructive",
});
},
});
const importBackupMutation = useMutation({
mutationFn: async (sqlContent: string) => {
mutationFn: async (backupData: any) => {
const response = await apiRequest('/api/admin/backup/import', {
method: 'POST',
body: JSON.stringify({ sqlContent }),
body: JSON.stringify(backupData),
});
return response.json();
},
@@ -361,15 +353,15 @@ export default function Administration() {
// Invalider toutes les queries pour forcer un refresh
queryClient.clear();
toast({
title: "Sauvegarde SQL importée",
description: "La base de données a été restaurée avec succès",
title: "Sauvegarde importée",
description: "Les données ont été restaurées avec succès",
variant: "success",
});
},
onError: (error) => {
onError: () => {
toast({
title: "Erreur d'import SQL",
description: error instanceof Error ? error.message : "Impossible d'importer la sauvegarde SQL",
title: "Erreur",
description: "Impossible d'importer la sauvegarde",
variant: "destructive",
});
},
@@ -406,22 +398,19 @@ export default function Administration() {
const handleImportBackup = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.sql';
input.accept = '.json';
input.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const sqlContent = e.target?.result as string;
if (!sqlContent.trim()) {
throw new Error('Le fichier SQL est vide');
}
importBackupMutation.mutate(sqlContent);
const backupData = JSON.parse(e.target?.result as string);
importBackupMutation.mutate(backupData);
} catch (error) {
toast({
title: "Erreur",
description: error instanceof Error ? error.message : "Fichier de sauvegarde SQL invalide",
description: "Fichier de sauvegarde invalide",
variant: "destructive",
});
}
@@ -1235,7 +1224,7 @@ export default function Administration() {
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Téléchargez une sauvegarde SQL complète de toutes les données (utilisateurs, magasins, ventes) compatible avec PostgreSQL.
Téléchargez une sauvegarde complète de toutes les données (utilisateurs, magasins, ventes).
</p>
<Button
onClick={handleExportBackup}
@@ -1243,7 +1232,7 @@ export default function Administration() {
className="w-full"
>
<Download className="h-4 w-4 mr-2" />
{exportBackupMutation.isPending ? "Export SQL en cours..." : "Télécharger la Sauvegarde SQL"}
{exportBackupMutation.isPending ? "Export en cours..." : "Télécharger la Sauvegarde"}
</Button>
</CardContent>
</Card>
@@ -1258,7 +1247,7 @@ export default function Administration() {
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Restaurez la base de données à partir d'un fichier de sauvegarde SQL PostgreSQL.
Restaurez les données à partir d'un fichier de sauvegarde.
<strong className="text-red-600"> Attention : cette action remplacera toutes les données existantes.</strong>
</p>
<AlertDialog>
@@ -1269,18 +1258,18 @@ export default function Administration() {
className="w-full"
>
<Upload className="h-4 w-4 mr-2" />
{importBackupMutation.isPending ? "Import SQL en cours..." : "Restaurer depuis un Fichier SQL"}
{importBackupMutation.isPending ? "Import en cours..." : "Restaurer depuis un Fichier"}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmer la restauration SQL</AlertDialogTitle>
<AlertDialogTitle>Confirmer la restauration</AlertDialogTitle>
<AlertDialogDescription>
Êtes-vous sûr de vouloir restaurer la base de données depuis un fichier de sauvegarde SQL ?
Êtes-vous sûr de vouloir restaurer les données depuis un fichier de sauvegarde ?
<br /><br />
<strong className="text-red-600">
⚠️ ATTENTION : Cette action exécutera le script SQL qui remplacera complètement
la structure et les données de la base de données PostgreSQL.
⚠️ ATTENTION : Cette action supprimera définitivement toutes les données actuelles
(utilisateurs, magasins, ventes) et les remplacera par celles du fichier de sauvegarde.
</strong>
<br /><br />
Cette action est irréversible. Assurez-vous d'avoir une sauvegarde des données actuelles
@@ -1293,7 +1282,7 @@ export default function Administration() {
onClick={handleImportBackup}
className="bg-red-600 hover:bg-red-700"
>
Confirmer la Restauration SQL
Confirmer la Restauration
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -1310,19 +1299,16 @@ export default function Administration() {
<CardContent>
<div className="space-y-3 text-sm">
<div>
<strong>Sauvegarde automatique :</strong> Le système crée automatiquement une sauvegarde SQL toutes les 12 heures (00:00 et 12:00) et conserve les 10 plus récentes.
<strong>Sauvegarde automatique :</strong> Le système crée automatiquement une sauvegarde toutes les 12 heures (00:00 et 12:00) et conserve les 10 plus récentes.
</div>
<div>
<strong>Format des fichiers :</strong> Les sauvegardes sont au format SQL PostgreSQL (.sql) et contiennent la structure complète et les données de la base.
<strong>Format des fichiers :</strong> Les sauvegardes sont au format JSON et contiennent toutes les données de l'application.
</div>
<div>
<strong>Contenu inclus :</strong> Schéma de base de données, utilisateurs, magasins, ventes, et toutes les relations entre ces données.
<strong>Contenu inclus :</strong> Utilisateurs, magasins, ventes, et toutes les relations entre ces données.
</div>
<div>
<strong>Avantages SQL :</strong> Sauvegarde native PostgreSQL compatible avec pg_dump/pg_restore, structure et données préservées.
</div>
<div>
<strong>Restauration :</strong> Remplace complètement la base de données existante par le contenu du fichier SQL importé.
<strong>Sécurité :</strong> Les mots de passe sont hachés et ne peuvent pas être récupérés depuis une sauvegarde.
</div>
<div>
<strong>Recommandation :</strong> Les sauvegardes automatiques couvrent la plupart des besoins, mais vous pouvez créer des sauvegardes manuelles avant des modifications importantes.
+11 -16
View File
@@ -64,7 +64,7 @@ export default function NewSaleForm() {
const form = useForm<FormData>({
defaultValues: {
vendeur: '',
products: [{ typeArticle: '', categorie: '', quantite: '1', gencode: '' }],
products: [{ typeArticle: '', categorie: '', quantite: '', gencode: '' }],
nom: '',
prenom: '',
dateNaissance: '',
@@ -85,7 +85,7 @@ export default function NewSaleForm() {
const addProduct = () => {
const currentProducts = form.getValues('products');
form.setValue('products', [...currentProducts, { typeArticle: '', categorie: '', quantite: '1', gencode: '' }]);
form.setValue('products', [...currentProducts, { typeArticle: '', categorie: '', quantite: '', gencode: '' }]);
};
const removeProduct = (index: number) => {
@@ -370,7 +370,7 @@ export default function NewSaleForm() {
<div className="section-vendor">
<div className="section-header">
<User className="h-5 w-5" />
<span>Informations vendeur</span>
<span>Informations Vendeur</span>
</div>
<div className="grid grid-cols-1 gap-4">
<FormField
@@ -378,7 +378,7 @@ export default function NewSaleForm() {
name="vendeur"
render={({ field }) => (
<FormItem>
<FormLabel>Nom du vendeur <span className="text-destructive">*</span></FormLabel>
<FormLabel>Vendeur <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input placeholder="Nom du vendeur" className="modern-input" {...field} />
</FormControl>
@@ -389,14 +389,11 @@ export default function NewSaleForm() {
</div>
</div>
{/* Section Produits */}
{/* Section Produit */}
<div className="section-product">
<div className="section-header">
<Package className="h-5 w-5" />
<span>Produits</span>
{products.length > 1 && (
<span className="text-sm text-gray-600 ml-2">({products.length} produits)</span>
)}
<span>Informations Produit</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
@@ -457,7 +454,7 @@ export default function NewSaleForm() {
<FormItem>
<FormLabel>Quantité <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input type="number" min="1" placeholder="1" className="modern-input" {...field} />
<Input type="number" min="1" placeholder="Entrez la quantité" className="modern-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -490,7 +487,7 @@ export default function NewSaleForm() {
<div className="section-client">
<div className="section-header">
<Users className="h-5 w-5" />
<span>Informations client</span>
<span>Informations Client</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
@@ -576,7 +573,7 @@ export default function NewSaleForm() {
<div className="section-identity">
<div className="section-header">
<BadgeCheck className="h-5 w-5" />
<span>Pièce d'identité</span>
<span>Pièce d'Identité</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
@@ -647,8 +644,7 @@ export default function NewSaleForm() {
{/* Section Photos */}
<div className="mt-6 space-y-4">
<div>
<h4 className="text-sm font-medium text-gray-700">Photos de la pièce d'identité (optionnel)</h4>
<p className="text-xs text-gray-500 mt-1">Les photos d'identité ne sont pas obligatoires</p>
<h4 className="text-sm font-medium text-gray-700">Photos de la pièce d'identité</h4>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@@ -759,8 +755,7 @@ export default function NewSaleForm() {
{/* Photo Ticket de caisse */}
<div className="mt-6 space-y-4">
<div>
<h4 className="text-sm font-medium text-gray-700">Photo du ticket de caisse (optionnel)</h4>
<p className="text-xs text-gray-500 mt-1">La photo du ticket n'est pas obligatoire</p>
<h4 className="text-sm font-medium text-gray-700">Photo du ticket de caisse</h4>
</div>
<div className="space-y-2">
+5 -5
View File
@@ -409,21 +409,21 @@ body {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
/* Fonds de couleur unis harmonisés selon la nouvelle interface */
/* Fonds de couleur unis harmonisés avec des teintes distinctes */
.section-vendor {
background: #e0f2fe !important; /* Fond bleu clair comme l'image */
background: #f0f9ff !important; /* Fond bleu très clair */
}
.section-product {
background: #e8f5e8 !important; /* Fond vert clair comme l'image */
background: #f0fdf4 !important; /* Fond vert très clair */
}
.section-client {
background: #f3e8ff !important; /* Fond violet clair comme l'image */
background: #fef3c7 !important; /* Fond jaune clair */
}
.section-identity {
background: #fff3e0 !important; /* Fond orange clair comme l'image */
background: #fce7f3 !important; /* Fond rose très clair */
}
/* S'assurer que tous les inputs ont un fond blanc dans toutes les sections */
-22
View File
@@ -315,28 +315,6 @@ Preferred communication style: Simple, everyday language.
- ✅ **Database Schema**: Sales and sale_products tables properly configured for multi-product sales
- ✅ **Export Compatibility**: All export functions (Excel, PDF, CSV) support multiple products per sale
### SQL Backup System Implementation (January 19, 2025)
- ✅ **SQL Backup Format**: Migrated from JSON to native PostgreSQL SQL format using pg_dump
- ✅ **Database Restoration**: Implemented SQL import capability using psql for complete database restoration
- ✅ **Automatic SQL Backups**: Modified scheduler to create SQL dumps instead of JSON exports
- ✅ **Manual Export/Import**: Updated administration interface to handle SQL files (.sql)
- ✅ **Buffer Optimization**: Increased maxBuffer to 50MB for handling large databases with photo data
- ✅ **Production Compatibility**: Native PostgreSQL backup format ensures full compatibility with production systems
- ✅ **Data Integrity**: SQL format preserves all database structure, constraints, and relationships
- ✅ **User Interface**: Updated backup management UI with SQL-specific messaging and file extensions
### Photo Requirements Optimization (January 19, 2025)
- ✅ **Optional Identity Photos**: Made identity document photos (recto/verso) optional as per user request
- ✅ **Optional Receipt Photos**: Clarified that receipt/ticket photos are also optional
- ✅ **UI Updates**: Updated form labels to clearly indicate photo fields are not mandatory
- ✅ **Validation Adjustment**: Photos no longer required for form submission compliance
- ✅ **User Experience**: Added explanatory text to reduce confusion about photo requirements
### Form Usability Improvements (January 19, 2025)
- ✅ **Default Quantity**: Set product quantity field to default value of 1 for better user experience
- ✅ **Streamlined Input**: Users no longer need to manually enter quantity for single item purchases
- ✅ **Placeholder Update**: Changed placeholder text to reflect default value
### Docker Configuration & Cleanup (January 9, 2025)
- ✅ Complete Docker setup optimized for external PostgreSQL database
- ✅ Docker Compose configuration with RegisFlow application only
+42 -105
View File
@@ -2,22 +2,10 @@ import cron from 'node-cron';
import fs from 'fs';
import path from 'path';
import { storage } from './storage';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
const BACKUP_DIR = path.join(process.cwd(), 'backups');
const MAX_BACKUPS = 10;
// Database connection info from environment variables
const DB_HOST = process.env.PGHOST || 'localhost';
const DB_PORT = process.env.PGPORT || '5432';
const DB_NAME = process.env.PGDATABASE || 'postgres';
const DB_USER = process.env.PGUSER || 'postgres';
const DB_PASSWORD = process.env.PGPASSWORD || '';
const DATABASE_URL = process.env.DATABASE_URL;
// Ensure backup directory exists
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
@@ -25,117 +13,66 @@ if (!fs.existsSync(BACKUP_DIR)) {
export async function createAutomaticBackup() {
try {
console.log('Creating automatic SQL backup...');
console.log('Creating automatic backup...');
// Create filename with timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `auto-backup-${timestamp}.sql`;
const filepath = path.join(BACKUP_DIR, filename);
// Create SQL dump using pg_dump
await createSQLBackup(filepath);
// Get stats for logging
// Get all data from the database
const users = await storage.getAllUsers();
const stores = await storage.getAllStores();
let totalSales = 0;
// Get all sales from all stores
const allSales = [];
for (const store of stores) {
const storeSales = await storage.getSalesByStore(store.id);
totalSales += storeSales.length;
allSales.push(...storeSales);
}
const backupData = {
timestamp: new Date().toISOString(),
version: "1.0",
type: "automatic",
data: {
users: users.map(user => ({
...user,
// Don't include the password hash in backup for security
password: undefined
})),
stores,
sales: allSales
},
metadata: {
totalUsers: users.length,
totalStores: stores.length,
totalSales: allSales.length,
exportedBy: 'system-auto-backup'
}
};
// Create filename with timestamp
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `auto-backup-${timestamp}.json`;
const filepath = path.join(BACKUP_DIR, filename);
// Write backup to file
fs.writeFileSync(filepath, JSON.stringify(backupData, null, 2));
console.log(`✅ Automatic SQL backup created: ${filename}`);
console.log(`📊 Backup stats: ${users.length} users, ${stores.length} stores, ${totalSales} sales`);
console.log(`✅ Automatic backup created: ${filename}`);
console.log(`📊 Backup stats: ${users.length} users, ${stores.length} stores, ${allSales.length} sales`);
// Clean up old backups
await cleanupOldBackups();
return {
success: true,
filename,
stats: {
totalUsers: users.length,
totalStores: stores.length,
totalSales: totalSales,
exportedBy: 'system-auto-backup'
}
};
return { success: true, filename, stats: backupData.metadata };
} catch (error) {
console.error('❌ Failed to create automatic SQL backup:', error);
console.error('❌ Failed to create automatic backup:', error);
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}
export async function createSQLBackup(filepath: string): Promise<void> {
try {
// Use pg_dump to create SQL backup
let command: string;
if (DATABASE_URL) {
// Use DATABASE_URL if available
command = `pg_dump "${DATABASE_URL}" --no-owner --no-privileges --clean --if-exists`;
} else {
// Use individual connection parameters
command = `PGPASSWORD="${DB_PASSWORD}" pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" --no-owner --no-privileges --clean --if-exists`;
}
const { stdout, stderr } = await execAsync(command, {
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large databases
});
if (stderr && !stderr.includes('NOTICE')) {
console.warn('pg_dump warnings:', stderr);
}
// Write SQL dump to file
fs.writeFileSync(filepath, stdout);
console.log(`SQL backup created: ${path.basename(filepath)}`);
} catch (error) {
console.error('Failed to create SQL backup:', error);
throw new Error(`SQL backup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
export async function restoreSQLBackup(filepath: string): Promise<void> {
try {
console.log(`Restoring SQL backup from: ${path.basename(filepath)}`);
// Read SQL file
const sqlContent = fs.readFileSync(filepath, 'utf8');
// Use psql to restore the backup
let command: string;
if (DATABASE_URL) {
// Use DATABASE_URL if available
command = `psql "${DATABASE_URL}"`;
} else {
// Use individual connection parameters
command = `PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}"`;
}
const { stderr } = await execAsync(command, {
input: sqlContent,
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large databases
});
if (stderr && !stderr.includes('NOTICE')) {
console.warn('psql warnings:', stderr);
}
console.log(`✅ SQL backup restored successfully from: ${path.basename(filepath)}`);
} catch (error) {
console.error('Failed to restore SQL backup:', error);
throw new Error(`SQL restore failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async function cleanupOldBackups() {
try {
// Get all backup files (now SQL files)
// Get all backup files
const files = fs.readdirSync(BACKUP_DIR)
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.sql'))
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'))
.map(file => ({
name: file,
path: path.join(BACKUP_DIR, file),
@@ -162,7 +99,7 @@ async function cleanupOldBackups() {
export function getBackupStats() {
try {
const files = fs.readdirSync(BACKUP_DIR)
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.sql'));
.filter(file => file.startsWith('auto-backup-') && file.endsWith('.json'));
return {
totalBackups: files.length,
+118 -46
View File
@@ -5,9 +5,7 @@ import session from "express-session";
import connectPg from "connect-pg-simple";
import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema";
import { z } from "zod";
import fs from "fs";
import path from "path";
import { createAutomaticBackup, getBackupStats, createSQLBackup, restoreSQLBackup } from "./backup-scheduler";
import { createAutomaticBackup, getBackupStats } from "./backup-scheduler";
import { executePurgeManually, getPurgeStats } from "./data-purge";
declare module 'express-session' {
@@ -466,65 +464,139 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// SQL Backup routes
// Backup routes
app.get('/api/admin/backup/export', requireRole(['admin']), async (req, res) => {
try {
// Create a temporary SQL backup
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `manual-backup-${timestamp}.sql`;
const filepath = path.join(process.cwd(), 'backups', filename);
await createSQLBackup(filepath);
// Send the SQL file
res.setHeader('Content-Type', 'application/sql');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
const sqlContent = fs.readFileSync(filepath, 'utf8');
res.send(sqlContent);
// Optionally delete the temporary file after sending
// fs.unlinkSync(filepath);
// Get all data from the database
const users = await storage.getAllUsers();
const stores = await storage.getAllStores();
// Get all sales from all stores
const allSales = [];
for (const store of stores) {
const storeSales = await storage.getSalesByStore(store.id);
allSales.push(...storeSales);
}
const backupData = {
timestamp: new Date().toISOString(),
version: "1.0",
data: {
users: users.map(user => ({
...user,
// Don't include the password hash in backup for security
password: undefined
})),
stores,
sales: allSales
},
metadata: {
totalUsers: users.length,
totalStores: stores.length,
totalSales: allSales.length,
exportedBy: req.user.username
}
};
res.json(backupData);
} catch (error) {
console.error('Export SQL backup error:', error);
res.status(500).json({ error: "Failed to export SQL backup" });
console.error('Export backup error:', error);
res.status(500).json({ error: "Failed to export backup" });
}
});
// SQL Backup import route (receives SQL file content)
app.post('/api/admin/backup/import', requireRole(['admin']), async (req, res) => {
try {
const { sqlContent } = req.body;
const backupData = req.body;
if (!sqlContent || typeof sqlContent !== 'string') {
return res.status(400).json({ error: "SQL content is required" });
// Validate backup structure
if (!backupData.data || !backupData.data.users || !backupData.data.stores || !backupData.data.sales) {
return res.status(400).json({ error: "Invalid backup file format" });
}
// Create a temporary file for the SQL content
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `restore-${timestamp}.sql`;
const filepath = path.join(process.cwd(), 'backups', filename);
// Write SQL content to temporary file
fs.writeFileSync(filepath, sqlContent);
// Restore the backup
await restoreSQLBackup(filepath);
// Clean up temporary file
fs.unlinkSync(filepath);
// Clear existing data (in reverse order due to foreign keys)
// First delete all sales
const existingSales = [];
const existingStores = await storage.getAllStores();
for (const store of existingStores) {
const storeSales = await storage.getSalesByStore(store.id);
existingSales.push(...storeSales);
}
for (const sale of existingSales) {
await storage.deleteSale(sale.id);
}
// Then delete users (except admin to avoid locking out)
const existingUsers = await storage.getAllUsers();
for (const user of existingUsers) {
if (user.role !== 'administrator' || user.username !== 'admin') {
await storage.deleteUser(user.id);
}
}
// Then delete stores
for (const store of existingStores) {
await storage.deleteStore(store.id);
}
// Import new data
// First import stores
const storeIdMapping = new Map();
for (const storeData of backupData.data.stores) {
const { id, ...storeWithoutId } = storeData;
const newStore = await storage.createStore(storeWithoutId);
storeIdMapping.set(id, newStore.id);
}
// Then import users (excluding admin and users with passwords)
const userIdMapping = new Map();
for (const userData of backupData.data.users) {
if (userData.username === 'admin') continue; // Skip admin user
const { id, password, ...userWithoutIdAndPassword } = userData;
// Map store ID if it exists
if (userData.storeId && storeIdMapping.has(userData.storeId)) {
userWithoutIdAndPassword.storeId = storeIdMapping.get(userData.storeId);
}
// Set a default password since we can't restore the original
const userWithPassword = {
...userWithoutIdAndPassword,
password: 'restored123' // Users will need to change this
};
const newUser = await storage.createUser(userWithPassword);
userIdMapping.set(id, newUser.id);
}
// Finally import sales
for (const saleData of backupData.data.sales) {
const { id, ...saleWithoutId } = saleData;
// Map store and user IDs
if (saleData.storeId && storeIdMapping.has(saleData.storeId)) {
saleWithoutId.storeId = storeIdMapping.get(saleData.storeId);
}
if (saleData.userId && userIdMapping.has(saleData.userId)) {
saleWithoutId.userId = userIdMapping.get(saleData.userId);
}
await storage.createSale(saleWithoutId);
}
res.json({
message: "SQL backup restored successfully",
timestamp: new Date().toISOString()
message: "Backup imported successfully",
imported: {
stores: backupData.data.stores.length,
users: backupData.data.users.filter((u: any) => u.username !== 'admin').length,
sales: backupData.data.sales.length
}
});
} catch (error) {
console.error('Import SQL backup error:', error);
res.status(500).json({
error: "Failed to restore SQL backup",
details: error instanceof Error ? error.message : 'Unknown error'
});
console.error('Import backup error:', error);
res.status(500).json({ error: "Failed to import backup", details: error instanceof Error ? error.message : 'Unknown error' });
}
});