Add option to include and display a photo of the sales receipt

Implements `photoTicket` field in `Sale` and `FormData` types, adds UI elements in `NewSaleForm` and `SaleDetailsModal` components, and updates schema.ts.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: dec54137-d5a2-4406-8ad4-5f2f9fb110bf
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/7df9e24f-d78b-4589-9a08-c70e02d6d7d9.jpg
This commit is contained in:
michaelschal committed 2025-07-11 18:15:16 +00:00
1 parent e48337925c
commit 4f04428dea
5 files changed
+118 -5

No files matched your search

+81 -3
View File
@@ -35,6 +35,7 @@ interface FormData {
dateDelivrance: string;
photoRecto?: string;
photoVerso?: string;
photoTicket?: string;
}
export default function NewSaleForm() {
@@ -75,6 +76,7 @@ export default function NewSaleForm() {
dateDelivrance: '',
photoRecto: '',
photoVerso: '',
photoTicket: '',
}
});
@@ -119,6 +121,7 @@ export default function NewSaleForm() {
dateDelivrance: '',
photoRecto: '',
photoVerso: '',
photoTicket: '',
});
},
onError: (error: any) => {
@@ -235,7 +238,13 @@ export default function NewSaleForm() {
const photoData = await capturePhoto();
console.log('Photo captured successfully, data length:', photoData.length);
form.setValue(currentPhotoType === 'recto' ? 'photoRecto' : 'photoVerso', photoData);
if (currentPhotoType === 'recto') {
form.setValue('photoRecto', photoData);
} else if (currentPhotoType === 'verso') {
form.setValue('photoVerso', photoData);
} else if (currentPhotoType === 'ticket') {
form.setValue('photoTicket', photoData);
}
toast({
title: "Photo capturée",
@@ -255,7 +264,13 @@ export default function NewSaleForm() {
};
const handleRemovePhoto = (photoType: PhotoType) => {
form.setValue(photoType === 'recto' ? 'photoRecto' : 'photoVerso', '');
if (photoType === 'recto') {
form.setValue('photoRecto', '');
} else if (photoType === 'verso') {
form.setValue('photoVerso', '');
} else if (photoType === 'ticket') {
form.setValue('photoTicket', '');
}
toast({
title: "Photo supprimée",
description: `Photo ${photoType} supprimée`,
@@ -288,7 +303,13 @@ export default function NewSaleForm() {
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result as string;
form.setValue(photoType === 'recto' ? 'photoRecto' : 'photoVerso', result);
if (photoType === 'recto') {
form.setValue('photoRecto', result);
} else if (photoType === 'verso') {
form.setValue('photoVerso', result);
} else if (photoType === 'ticket') {
form.setValue('photoTicket', result);
}
toast({
title: "Photo ajoutée",
description: `Photo ${photoType} ajoutée avec succès`,
@@ -715,6 +736,63 @@ export default function NewSaleForm() {
</div>
</div>
</div>
{/* 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</h4>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-gray-600">Photo Ticket</label>
{form.watch('photoTicket') ? (
<div className="relative">
<img
src={form.watch('photoTicket')}
alt="Photo ticket"
className="w-full h-32 object-cover rounded border"
/>
<Button
type="button"
variant="destructive"
size="sm"
className="absolute top-2 right-2"
onClick={() => handleRemovePhoto('ticket')}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
) : (
<div className="space-y-2">
<Button
type="button"
variant="outline"
className="w-full h-24 border-dashed border-2"
onClick={() => handlePhotoCapture('ticket')}
>
<Camera className="h-5 w-5 mr-2" />
Prendre photo
</Button>
<div className="relative">
<input
type="file"
accept="image/*"
onChange={(e) => handleFileUpload('ticket', e)}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<Button
type="button"
variant="outline"
className="w-full h-8 text-xs"
>
<Upload className="h-3 w-3 mr-1" />
Ou choisir un fichier
</Button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Boutons d'action */}
+29 -1
View File
@@ -87,8 +87,14 @@ export default function SaleDetailsModal({ isOpen, sale, onClose }: SaleDetailsM
</div>
{/* Photos */}
{(sale.photoRecto || sale.photoVerso || sale.photoTicket) && (
<div className="mobile-form-section border-l-4 border-purple-500 pl-4">
<h4 className="responsive-subtitle text-gray-900 mb-3">Photos de la Pièce d'Identité</h4>
<h4 className="responsive-subtitle text-gray-900 mb-3">Photos</h4>
{/* Photos de la pièce d'identité */}
{(sale.photoRecto || sale.photoVerso) && (
<div className="mb-4">
<h5 className="text-sm font-semibold text-gray-700 mb-3">Pièce d'identité</h5>
<div className="mobile-form-row">
<div>
<span className="text-sm font-medium">Photo Recto:</span>
@@ -130,6 +136,28 @@ export default function SaleDetailsModal({ isOpen, sale, onClose }: SaleDetailsM
</div>
</div>
</div>
)}
{/* Photo du ticket de caisse */}
{sale.photoTicket && (
<div>
<h5 className="text-sm font-semibold text-gray-700 mb-3">Ticket de caisse</h5>
<div className="max-w-xs">
<img src={sale.photoTicket} alt="Photo ticket" className="w-full h-auto rounded border mb-2" />
<Button
variant="outline"
size="sm"
onClick={() => downloadPhoto(sale.photoTicket!, `vente-${sale.id}-ticket.jpg`)}
className="w-full"
>
<Download className="h-4 w-4 mr-2" />
Télécharger Ticket
</Button>
</div>
</div>
)}
</div>
)}
{/* System Information */}
<div className="border-l-4 border-gray-400 pl-4">
+3 -1
View File
@@ -27,6 +27,7 @@ export interface Sale {
// Photos
photoRecto?: string;
photoVerso?: string;
photoTicket?: string;
}
export interface FormData {
@@ -46,9 +47,10 @@ export interface FormData {
dateDelivrance: string;
photoRecto?: string;
photoVerso?: string;
photoTicket?: string;
}
export type PhotoType = 'recto' | 'verso';
export type PhotoType = 'recto' | 'verso' | 'ticket';
export interface BackupData {
timestamp: string;
+4
View File
@@ -264,6 +264,10 @@ Preferred communication style: Simple, everyday language.
- ✅ Comprehensive error handling with specific messages for different failure types
- ✅ HTTPS requirement detection for secure camera access
- ✅ Multiple capture methods: camera → file upload for maximum compatibility
- ✅ Added receipt/ticket photo capture as third photo type alongside identity documents
- ✅ PayloadTooLargeError resolved by increasing server limit to 10MB for photo uploads
- ✅ Complete photo management system with separate sections for identity and receipt photos
- ✅ Enhanced SaleDetailsModal with organized photo display and download functionality
### Production Deployment System (January 11, 2025)
- ✅ Complete production deployment configuration with Docker Compose
+1
View File
@@ -83,6 +83,7 @@ export const sales = pgTable("sales", {
// Photos
photoRecto: text("photo_recto"),
photoVerso: text("photo_verso"),
photoTicket: text("photo_ticket"),
});
// Relations