mirror of
https://github.com/R0m1k3/Regisflow.git
synced 2026-10-19 13:18:17 +02:00
Create a new sales registry app with identity verification features
Initial commit of the React application with UI components, form, camera, and indexedDB integration for sale data management. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 91318273-c764-4fd4-be04-bdc12c38af32 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/02867739-9ff7-4441-aca2-12e0153376c5.jpg
This commit is contained in:
1 parent
4969159ef9
commit
4fac1078a1
83 files changed
+16585
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { Sale } from '@/types/sale';
|
||||
|
||||
export function exportToCSV(sales: Sale[]): void {
|
||||
if (sales.length === 0) return;
|
||||
|
||||
const headers = [
|
||||
'Date de vente',
|
||||
'Vendeur',
|
||||
'Type d\'article',
|
||||
'Catégorie',
|
||||
'Quantité',
|
||||
'Code générique',
|
||||
'Nom client',
|
||||
'Prénom client',
|
||||
'Date de naissance',
|
||||
'Lieu de naissance',
|
||||
'Type et n° identité',
|
||||
'Autorité de délivrance',
|
||||
'Date de délivrance',
|
||||
'Photo recto (Oui/Non)',
|
||||
'Photo verso (Oui/Non)',
|
||||
'Horodatage'
|
||||
];
|
||||
|
||||
const rows = sales.map(sale => [
|
||||
sale.dateVente,
|
||||
sale.vendeur,
|
||||
sale.typeArticle,
|
||||
sale.categorie,
|
||||
sale.quantite.toString(),
|
||||
sale.gencode || '',
|
||||
sale.nom,
|
||||
sale.prenom,
|
||||
sale.dateNaissance || '',
|
||||
sale.lieuNaissance || '',
|
||||
sale.typeIdentite || '',
|
||||
sale.autoriteDelivrance || '',
|
||||
sale.dateDelivrance || '',
|
||||
sale.photoRecto ? 'Oui' : 'Non',
|
||||
sale.photoVerso ? 'Oui' : 'Non',
|
||||
sale.timestamp
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...rows]
|
||||
.map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(';'))
|
||||
.join('\n');
|
||||
|
||||
// Add BOM for UTF-8 Excel compatibility
|
||||
const bom = '\uFEFF';
|
||||
const blob = new Blob([bom + csvContent], {
|
||||
type: 'text/csv;charset=utf-8;'
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `ventes_petards_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
export function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('fr-FR');
|
||||
}
|
||||
|
||||
export function formatDateTime(dateString: string): string {
|
||||
return new Date(dateString).toLocaleString('fr-FR');
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { QueryClient, QueryFunction } from "@tanstack/react-query";
|
||||
|
||||
async function throwIfResNotOk(res: Response) {
|
||||
if (!res.ok) {
|
||||
const text = (await res.text()) || res.statusText;
|
||||
throw new Error(`${res.status}: ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
data?: unknown | undefined,
|
||||
): Promise<Response> {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: data ? { "Content-Type": "application/json" } : {},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
await throwIfResNotOk(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
type UnauthorizedBehavior = "returnNull" | "throw";
|
||||
export const getQueryFn: <T>(options: {
|
||||
on401: UnauthorizedBehavior;
|
||||
}) => QueryFunction<T> =
|
||||
({ on401: unauthorizedBehavior }) =>
|
||||
async ({ queryKey }) => {
|
||||
const res = await fetch(queryKey.join("/") as string, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await throwIfResNotOk(res);
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
queryFn: getQueryFn({ on401: "throw" }),
|
||||
refetchInterval: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Sale, BackupData } from '@/types/sale';
|
||||
|
||||
const DB_NAME = 'VentesPetardsDB';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'ventes';
|
||||
const BACKUP_KEY = 'petards_backup_csv';
|
||||
|
||||
export class FireworksDB {
|
||||
private db: IDBDatabase | null = null;
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
const store = db.createObjectStore(STORE_NAME, {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true
|
||||
});
|
||||
store.createIndex('nom', 'nom', { unique: false });
|
||||
store.createIndex('dateVente', 'dateVente', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveSale(sale: Omit<Sale, 'id'>): Promise<Sale> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
|
||||
const saleWithTimestamp = {
|
||||
...sale,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
const request = store.add(saleWithTimestamp);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
const savedSale = { ...saleWithTimestamp, id: request.result as number };
|
||||
resolve(savedSale);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getAllSales(): Promise<Sale[]> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSale(id: number): Promise<void> {
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAutoBackup(sales: Sale[]): Promise<void> {
|
||||
const backupData: BackupData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
sales,
|
||||
recordCount: sales.length
|
||||
};
|
||||
|
||||
localStorage.setItem(BACKUP_KEY, JSON.stringify(backupData));
|
||||
}
|
||||
|
||||
export function getAutoBackup(): BackupData | null {
|
||||
try {
|
||||
const backup = localStorage.getItem(BACKUP_KEY);
|
||||
return backup ? JSON.parse(backup) : null;
|
||||
} catch (error) {
|
||||
console.error('Error loading backup:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadBackup(): void {
|
||||
const backup = getAutoBackup();
|
||||
if (!backup) return;
|
||||
|
||||
const blob = new Blob([JSON.stringify(backup, null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `backup_petards_${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
export function validateEAN13(code: string): boolean {
|
||||
if (!code || code.length !== 13) return false;
|
||||
if (!/^\d{13}$/.test(code)) return false;
|
||||
|
||||
// Calculate checksum
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const digit = parseInt(code[i]);
|
||||
sum += i % 2 === 0 ? digit : digit * 3;
|
||||
}
|
||||
|
||||
const checksum = (10 - (sum % 10)) % 10;
|
||||
return checksum === parseInt(code[12]);
|
||||
}
|
||||
|
||||
export function validateRequiredFields(formData: any): string[] {
|
||||
const requiredFields = [
|
||||
{ key: 'vendeur', label: 'Vendeur' },
|
||||
{ key: 'typeArticle', label: 'Type d\'article' },
|
||||
{ key: 'categorie', label: 'Catégorie' },
|
||||
{ key: 'quantite', label: 'Quantité' },
|
||||
{ key: 'nom', label: 'Nom' },
|
||||
{ key: 'prenom', label: 'Prénom' }
|
||||
];
|
||||
|
||||
return requiredFields
|
||||
.filter(field => !formData[field.key] || formData[field.key].toString().trim() === '')
|
||||
.map(field => field.label);
|
||||
}
|
||||
|
||||
export const ARTICLE_CATEGORY_MAPPING = {
|
||||
"Pétard à mèche": ["F3"],
|
||||
"Batterie": ["F3"],
|
||||
"Batterie nécessitant un support externe": ["F3"],
|
||||
"Combinaison": ["F3"],
|
||||
"Combinaison nécessitant un support externe": ["F3"],
|
||||
"Pétard aérien": ["F2", "F3"],
|
||||
"Pétard à composition flash": ["F3"],
|
||||
"Fusée": ["F2", "F3"],
|
||||
"Chandelle romaine": ["F2", "F3"],
|
||||
"Chandelle monocoup": ["F2", "F3"]
|
||||
} as const;
|
||||
|
||||
export const IDENTITY_TYPES = [
|
||||
{ value: "CNI", label: "Carte Nationale d'Identité" },
|
||||
{ value: "Passeport", label: "Passeport" },
|
||||
{ value: "Permis de conduire", label: "Permis de conduire" }
|
||||
] as const;
|
||||
Reference in new issue
Block a user