EL GHAFRAOUI AYOUB commited on
Commit
53f16ad
·
1 Parent(s): 67199a6
app/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/__init__.cpython-312.pyc and b/app/__pycache__/__init__.cpython-312.pyc differ
 
app/__pycache__/main.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ
 
app/db/__pycache__/database.cpython-312.pyc CHANGED
Binary files a/app/db/__pycache__/database.cpython-312.pyc and b/app/db/__pycache__/database.cpython-312.pyc differ
 
app/db/__pycache__/models.cpython-312.pyc CHANGED
Binary files a/app/db/__pycache__/models.cpython-312.pyc and b/app/db/__pycache__/models.cpython-312.pyc differ
 
app/main.py CHANGED
@@ -50,4 +50,9 @@ async def root(request: Request):
50
  # Health check endpoint
51
  @app.get("/health")
52
  async def health_check():
53
- return {"status": "healthy"}
 
 
 
 
 
 
50
  # Health check endpoint
51
  @app.get("/health")
52
  async def health_check():
53
+ return {"status": "healthy"}
54
+
55
+ @app.get("/history")
56
+ async def history_page(request: Request):
57
+ return templates.TemplateResponse("history.html", {"request": request})
58
+
app/routes/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/app/routes/__pycache__/__init__.cpython-312.pyc and b/app/routes/__pycache__/__init__.cpython-312.pyc differ
 
app/routes/__pycache__/invoices.cpython-312.pyc CHANGED
Binary files a/app/routes/__pycache__/invoices.cpython-312.pyc and b/app/routes/__pycache__/invoices.cpython-312.pyc differ
 
app/routes/invoices.py CHANGED
@@ -1,6 +1,6 @@
1
  from fastapi import APIRouter, Depends, HTTPException, Request
2
  from sqlalchemy.ext.asyncio import AsyncSession
3
- from sqlalchemy import select, func, Sequence
4
  from typing import List, Optional
5
  from sqlalchemy.orm import selectinload, joinedload, Session
6
  from fastapi.responses import StreamingResponse
@@ -11,7 +11,6 @@ from app.db.database import get_db
11
  from app.schemas.invoice import InvoiceCreate, InvoiceResponse
12
  from app.db.models import Invoice, InvoiceItem
13
  from app.services.invoice_service import InvoiceService
14
- from app.services.excel_service import ExcelService
15
 
16
  # Set up logging
17
  logger = logging.getLogger(__name__)
@@ -45,7 +44,6 @@ async def create_new_invoice(
45
  client_name=invoice.client_name,
46
  client_phone=invoice.client_phone,
47
  address=invoice.address,
48
- ville=invoice.ville, # Add ville field
49
  total_ht=invoice.total_ht,
50
  tax=invoice.tax,
51
  total_ttc=invoice.total_ttc,
@@ -143,48 +141,6 @@ async def generate_invoice_pdf(
143
  logger.error(f"Error generating PDF: {str(e)}", exc_info=True)
144
  raise HTTPException(status_code=500, detail=str(e))
145
 
146
- @router.post("/{invoice_id}/generate-excel")
147
- async def generate_invoice_excel(
148
- invoice_id: int,
149
- request: Request,
150
- db: AsyncSession = Depends(get_db)
151
- ):
152
- try:
153
- logger.info(f"Starting Excel generation for invoice ID: {invoice_id}")
154
- logger.info(f"Request from IP: {request.client.host}")
155
-
156
- result = await db.execute(
157
- select(Invoice)
158
- .options(selectinload(Invoice.items))
159
- .filter(Invoice.id == invoice_id)
160
- )
161
- invoice = result.scalar_one_or_none()
162
-
163
- if not invoice:
164
- logger.error(f"Invoice not found: {invoice_id}")
165
- raise HTTPException(status_code=404, detail="Invoice not found")
166
-
167
- logger.info(f"Found invoice: {invoice.invoice_number}")
168
- logger.info(f"Client: {invoice.client_name}")
169
- logger.info(f"Number of items: {len(invoice.items)}")
170
-
171
- logger.info("Starting Excel file generation")
172
- excel_bytes = ExcelService.generate_excel(invoice)
173
- logger.info("Excel file generated successfully")
174
-
175
- filename = f"devis_{invoice.invoice_number}.xlsx"
176
- logger.info(f"Sending Excel file: {filename}")
177
-
178
- return StreamingResponse(
179
- iter([excel_bytes]),
180
- media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
181
- headers={"Content-Disposition": f"attachment; filename={filename}"}
182
- )
183
- except Exception as e:
184
- logger.error(f"Error generating Excel for invoice {invoice_id}: {str(e)}", exc_info=True)
185
- logger.error("Stack trace:", exc_info=True)
186
- raise HTTPException(status_code=500, detail=str(e))
187
-
188
  @router.get("/last-number/{client_type}", response_model=dict)
189
  async def get_last_invoice_number(
190
  client_type: str,
@@ -219,4 +175,71 @@ def reset_invoice_sequence(db: Session = Depends(get_db)):
219
  return {"message": "Sequence reset successfully"}
220
  except Exception as e:
221
  db.rollback()
222
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import APIRouter, Depends, HTTPException, Request
2
  from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlalchemy import select, func, Sequence, delete
4
  from typing import List, Optional
5
  from sqlalchemy.orm import selectinload, joinedload, Session
6
  from fastapi.responses import StreamingResponse
 
11
  from app.schemas.invoice import InvoiceCreate, InvoiceResponse
12
  from app.db.models import Invoice, InvoiceItem
13
  from app.services.invoice_service import InvoiceService
 
14
 
15
  # Set up logging
16
  logger = logging.getLogger(__name__)
 
44
  client_name=invoice.client_name,
45
  client_phone=invoice.client_phone,
46
  address=invoice.address,
 
47
  total_ht=invoice.total_ht,
48
  tax=invoice.tax,
49
  total_ttc=invoice.total_ttc,
 
141
  logger.error(f"Error generating PDF: {str(e)}", exc_info=True)
142
  raise HTTPException(status_code=500, detail=str(e))
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  @router.get("/last-number/{client_type}", response_model=dict)
145
  async def get_last_invoice_number(
146
  client_type: str,
 
175
  return {"message": "Sequence reset successfully"}
176
  except Exception as e:
177
  db.rollback()
178
+ raise HTTPException(status_code=500, detail=str(e))
179
+
180
+ @router.put("/{invoice_id}", response_model=InvoiceResponse)
181
+ async def update_invoice(
182
+ invoice_id: int,
183
+ invoice_update: InvoiceCreate,
184
+ db: AsyncSession = Depends(get_db)
185
+ ):
186
+ try:
187
+ # Get existing invoice
188
+ result = await db.execute(
189
+ select(Invoice)
190
+ .options(selectinload(Invoice.items))
191
+ .filter(Invoice.id == invoice_id)
192
+ )
193
+ invoice = result.scalar_one_or_none()
194
+
195
+ if not invoice:
196
+ raise HTTPException(status_code=404, detail="Invoice not found")
197
+
198
+ # Update invoice fields
199
+ for field, value in invoice_update.dict(exclude={'items'}).items():
200
+ setattr(invoice, field, value)
201
+
202
+ # Delete existing items
203
+ await db.execute(delete(InvoiceItem).where(InvoiceItem.invoice_id == invoice_id))
204
+
205
+ # Create new items
206
+ for item_data in invoice_update.items:
207
+ db_item = InvoiceItem(
208
+ invoice_id=invoice_id,
209
+ **item_data.dict()
210
+ )
211
+ db.add(db_item)
212
+
213
+ await db.commit()
214
+ await db.refresh(invoice)
215
+
216
+ return invoice
217
+ except Exception as e:
218
+ await db.rollback()
219
+ raise HTTPException(status_code=500, detail=str(e))
220
+
221
+ @router.put("/{invoice_id}/status")
222
+ async def update_invoice_status(
223
+ invoice_id: int,
224
+ status_update: dict,
225
+ db: AsyncSession = Depends(get_db)
226
+ ):
227
+ try:
228
+ # Get existing invoice
229
+ result = await db.execute(
230
+ select(Invoice)
231
+ .filter(Invoice.id == invoice_id)
232
+ )
233
+ invoice = result.scalar_one_or_none()
234
+
235
+ if not invoice:
236
+ raise HTTPException(status_code=404, detail="Invoice not found")
237
+
238
+ # Update status
239
+ invoice.status = status_update.get("status")
240
+ await db.commit()
241
+
242
+ return {"message": "Status updated successfully"}
243
+ except Exception as e:
244
+ await db.rollback()
245
+ raise HTTPException(status_code=500, detail=str(e))
app/schemas/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/app/schemas/__pycache__/__init__.cpython-312.pyc and b/app/schemas/__pycache__/__init__.cpython-312.pyc differ
 
app/schemas/__pycache__/invoice.cpython-312.pyc CHANGED
Binary files a/app/schemas/__pycache__/invoice.cpython-312.pyc and b/app/schemas/__pycache__/invoice.cpython-312.pyc differ
 
app/schemas/invoice.py CHANGED
@@ -20,14 +20,15 @@ class InvoiceCreate(BaseModel):
20
  client_name: str
21
  client_phone: str
22
  address: str
23
- ville: str # Add ville field
24
  total_ht: float
25
  tax: float
26
  total_ttc: float
27
  frame_number: Optional[str] = None
28
  items: List[InvoiceItemCreate]
29
  commercial: Optional[str] = "divers"
30
-
 
31
  @computed_field
32
  def customer_name(self) -> str:
33
  return self.client_name
 
20
  client_name: str
21
  client_phone: str
22
  address: str
23
+ ville: Optional[str] = None
24
  total_ht: float
25
  tax: float
26
  total_ttc: float
27
  frame_number: Optional[str] = None
28
  items: List[InvoiceItemCreate]
29
  commercial: Optional[str] = "divers"
30
+ status: Optional[str] = "pending"
31
+
32
  @computed_field
33
  def customer_name(self) -> str:
34
  return self.client_name
app/services/__pycache__/excel_service.cpython-312.pyc CHANGED
Binary files a/app/services/__pycache__/excel_service.cpython-312.pyc and b/app/services/__pycache__/excel_service.cpython-312.pyc differ
 
app/services/__pycache__/invoice_service.cpython-312.pyc CHANGED
Binary files a/app/services/__pycache__/invoice_service.cpython-312.pyc and b/app/services/__pycache__/invoice_service.cpython-312.pyc differ
 
app/templates/history.html ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Historique des Devis</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
9
+ </head>
10
+ <body class="bg-light">
11
+ <header class="bg-primary py-4 mb-4 shadow-sm">
12
+ <div class="container">
13
+ <div class="row align-items-center">
14
+ <div class="col-md-6">
15
+ <h1 class="text-white mb-0">Historique des Devis</h1>
16
+ </div>
17
+ <div class="col-md-6 text-end">
18
+ <a href="/" class="btn btn-light me-2">
19
+ <i class="fas fa-plus"></i> Nouveau Devis
20
+ </a>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ </header>
25
+
26
+ <div class="container">
27
+ <div class="card shadow-sm">
28
+ <div class="card-header bg-white py-3">
29
+ <div class="row align-items-center">
30
+ <div class="col-md-4">
31
+ <input type="text" id="searchInput" class="form-control" placeholder="Rechercher...">
32
+ </div>
33
+ <div class="col-md-3">
34
+ <select id="statusFilter" class="form-select">
35
+ <option value="">Tous les statuts</option>
36
+ <option value="pending">En attente</option>
37
+ <option value="completed">Complété</option>
38
+ </select>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <div class="card-body">
43
+ <div class="table-responsive">
44
+ <table class="table table-hover">
45
+ <thead>
46
+ <tr>
47
+ <th>N° Devis</th>
48
+ <th>Date</th>
49
+ <th>Client</th>
50
+ <th>Projet</th>
51
+ <th>Total TTC</th>
52
+ <th>Status</th>
53
+ <th>Actions</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody id="invoicesTableBody"></tbody>
57
+ </table>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
64
+ <script>
65
+ document.addEventListener('DOMContentLoaded', async () => {
66
+ const tableBody = document.getElementById('invoicesTableBody');
67
+ const searchInput = document.getElementById('searchInput');
68
+ const statusFilter = document.getElementById('statusFilter');
69
+
70
+ async function loadInvoices() {
71
+ try {
72
+ const response = await fetch('/api/invoices/');
73
+ if (!response.ok) throw new Error('Failed to load invoices');
74
+ return await response.json();
75
+ } catch (error) {
76
+ console.error('Error:', error);
77
+ return [];
78
+ }
79
+ }
80
+
81
+ function formatDate(dateString) {
82
+ return new Date(dateString).toLocaleDateString('fr-FR');
83
+ }
84
+
85
+ function renderInvoices(invoices) {
86
+ const searchTerm = searchInput.value.toLowerCase();
87
+ const statusValue = statusFilter.value;
88
+
89
+ const filteredInvoices = invoices.filter(invoice => {
90
+ const matchesSearch = (
91
+ invoice.invoice_number.toLowerCase().includes(searchTerm) ||
92
+ invoice.client_name.toLowerCase().includes(searchTerm) ||
93
+ invoice.project.toLowerCase().includes(searchTerm)
94
+ );
95
+ const matchesStatus = !statusValue || invoice.status === statusValue;
96
+ return matchesSearch && matchesStatus;
97
+ });
98
+
99
+ tableBody.innerHTML = filteredInvoices.map(invoice => `
100
+ <tr>
101
+ <td>${invoice.invoice_number}</td>
102
+ <td>${formatDate(invoice.date)}</td>
103
+ <td>${invoice.client_name}</td>
104
+ <td>${invoice.project}</td>
105
+ <td>${invoice.total_ttc.toFixed(2)} DH</td>
106
+ <td>
107
+ <span class="badge ${invoice.status === 'pending' ? 'bg-warning' : 'bg-success'}">
108
+ ${invoice.status === 'pending' ? 'En attente' : 'Complété'}
109
+ </span>
110
+ </td>
111
+ <td>
112
+ <div class="btn-group">
113
+ <button onclick="editInvoice(${invoice.id})" class="btn btn-sm btn-warning">
114
+ <i class="fas fa-edit"></i>
115
+ </button>
116
+ <button onclick="previewPDF(${invoice.id})" class="btn btn-sm btn-primary">
117
+ <i class="fas fa-eye"></i>
118
+ </button>
119
+ <button onclick="downloadPDF(${invoice.id})" class="btn btn-sm btn-success">
120
+ <i class="fas fa-download"></i>
121
+ </button>
122
+ </div>
123
+ </td>
124
+ </tr>
125
+ `).join('');
126
+ }
127
+
128
+ // Load initial data
129
+ const invoices = await loadInvoices();
130
+ renderInvoices(invoices);
131
+
132
+ // Set up event listeners
133
+ searchInput.addEventListener('input', () => renderInvoices(invoices));
134
+ statusFilter.addEventListener('change', () => renderInvoices(invoices));
135
+
136
+ // Global functions for actions
137
+ window.editInvoice = (id) => {
138
+ window.location.href = `/?edit=${id}`;
139
+ };
140
+
141
+ window.previewPDF = async (id) => {
142
+ const response = await fetch(`/api/invoices/${id}/generate-pdf`, {
143
+ method: 'POST'
144
+ });
145
+ const blob = await response.blob();
146
+ const url = URL.createObjectURL(blob);
147
+ window.open(url, '_blank');
148
+ };
149
+
150
+ window.downloadPDF = async (id) => {
151
+ const response = await fetch(`/api/invoices/${id}/generate-pdf`, {
152
+ method: 'POST'
153
+ });
154
+ const blob = await response.blob();
155
+ const url = URL.createObjectURL(blob);
156
+ const a = document.createElement('a');
157
+ a.href = url;
158
+ a.download = `devis_${id}.pdf`;
159
+ document.body.appendChild(a);
160
+ a.click();
161
+ document.body.removeChild(a);
162
+ URL.revokeObjectURL(url);
163
+ };
164
+ });
165
+ </script>
166
+ </body>
167
+ </html>
app/templates/index.html CHANGED
@@ -6,6 +6,7 @@
6
  <title>Invoice Generator</title>
7
  <!-- Bootstrap CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
 
9
  <style>
10
  :root {
11
  --primary-color: #2c3e50;
@@ -177,6 +178,12 @@
177
  padding: 0.5rem 1rem;
178
  }
179
  }
 
 
 
 
 
 
180
  </style>
181
  </head>
182
  <body class="bg-light">
@@ -187,7 +194,10 @@
187
  <div class="col-md-6">
188
  <h1 class="text-white mb-0">Générateur de Devis</h1>
189
  </div>
190
- <div class="col-md-6 text-end">
 
 
 
191
  <img src="/static/logo.png" alt="Logo" height="50" class="img-fluid">
192
  </div>
193
  </div>
@@ -607,62 +617,76 @@
607
  };
608
 
609
  // Function to add a new item row
610
- function addItemRow(item, tableBody) {
611
- // Vérifier si l'article existe déjà
612
- const existingRow = Array.from(tableBody.querySelectorAll('tr')).find(row => {
613
- const description = row.querySelector('input[name="description"]').value;
614
- return description === item.description;
615
- });
 
616
 
617
  if (existingRow) {
618
- // Afficher la modal de confirmation
619
- const confirmationModal = new bootstrap.Modal(document.getElementById('confirmationModal'));
620
- confirmationModal.show();
621
-
622
- // Gérer la confirmation de remplacement
623
- document.getElementById('confirmReplace').onclick = function() {
624
- existingRow.remove();
625
- creerNouvelleRow();
626
- confirmationModal.hide();
627
- };
628
  } else {
629
- creerNouvelleRow();
 
630
  }
 
631
 
632
- function creerNouvelleRow() {
633
- const row = document.createElement("tr");
634
- const total = (item.quantity * item.length * item.unit_price).toFixed(2);
635
-
636
- row.innerHTML = `
637
- <td><input type="text" class="form-control" name="description" value="${item.description}" readonly></td>
638
- <td><input type="text" class="form-control" name="unit" value="${item.unit}" readonly></td>
639
- <td><input type="number" class="form-control" name="quantity" value="${item.quantity}" required></td>
640
- <td><input type="number" class="form-control" name="length" value="${item.length}" step="0.01" required></td>
641
- <td><input type="number" class="form-control" name="unitPrice" value="${item.unit_price}" step="0.01" required></td>
642
- <td><input type="number" class="form-control" name="totalHT" value="${total}" readonly></td>
643
- <td><button type="button" class="btn btn-danger btn-sm remove-item">×</button></td>
644
- `;
645
-
646
- row.querySelector(".remove-item").addEventListener("click", () => {
647
- row.remove();
648
- updateTotals();
649
- });
650
-
651
- // Ajouter les écouteurs d'événements pour mettre à jour le total
652
- ['quantity', 'length', 'unitPrice'].forEach(field => {
653
- row.querySelector(`input[name='${field}']`).addEventListener('input', () => {
654
- const quantite = parseFloat(row.querySelector('input[name="quantity"]').value) || 0;
655
- const longueur = parseFloat(row.querySelector('input[name="length"]').value) || 0;
656
- const prix = parseFloat(row.querySelector('input[name="unitPrice"]').value) || 0;
657
- const totalLigne = (quantite * longueur * prix).toFixed(2);
658
- row.querySelector('input[name="totalHT"]').value = totalLigne;
659
- updateTotals();
660
- });
661
- });
662
 
663
- tableBody.appendChild(row);
664
- updateTotals();
665
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
  }
667
 
668
  // Function to update totals
@@ -786,6 +810,9 @@
786
  invoiceForm.addEventListener("submit", async (event) => {
787
  event.preventDefault();
788
 
 
 
 
789
  const getAllItems = () => {
790
  const items = [];
791
  [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
@@ -822,28 +849,29 @@
822
  console.log("Sending invoice data:", data);
823
 
824
  try {
825
- // First create the invoice
826
- const createResponse = await fetch("/api/invoices/", {
827
- method: "POST",
 
 
 
828
  headers: { "Content-Type": "application/json" },
829
  body: JSON.stringify(data)
830
  });
831
 
832
- if (!createResponse.ok) {
833
- const errorData = await createResponse.json();
834
  console.error("Server error:", errorData);
835
- throw new Error(`Failed to create invoice: ${JSON.stringify(errorData)}`);
836
  }
837
 
838
- const invoice = await createResponse.json();
839
- console.log("Created invoice:", invoice);
840
 
841
- // Then generate the PDF
842
  const pdfResponse = await fetch(`/api/invoices/${invoice.id}/generate-pdf`, {
843
  method: "POST",
844
- headers: {
845
- "Content-Type": "application/json"
846
- },
847
  body: JSON.stringify(invoice)
848
  });
849
 
@@ -855,25 +883,41 @@
855
 
856
  // Handle PDF download
857
  const blob = await pdfResponse.blob();
858
- const url = window.URL.createObjectURL(blob);
859
  const a = document.createElement("a");
860
- a.href = url;
861
  a.download = `devis_${invoice.invoice_number}.pdf`;
862
  document.body.appendChild(a);
863
  a.click();
864
  document.body.removeChild(a);
865
- window.URL.revokeObjectURL(url);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
866
 
867
  } catch (error) {
868
  console.error("Error:", error);
869
  alert(`Error: ${error.message}`);
870
  }
871
-
872
- // After successful submission, update the invoice number for the current type
873
- const selectedType = document.querySelector('input[name="clientType"]:checked')?.value;
874
- if (selectedType) {
875
- await updateInvoiceNumber(selectedType);
876
- }
877
  });
878
 
879
  // Set today's date automatically
@@ -1020,6 +1064,62 @@
1020
  alert('Error generating Excel file: ' + error.message);
1021
  }
1022
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
  });
1024
  </script>
1025
  </body>
 
6
  <title>Invoice Generator</title>
7
  <!-- Bootstrap CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
  <style>
11
  :root {
12
  --primary-color: #2c3e50;
 
178
  padding: 0.5rem 1rem;
179
  }
180
  }
181
+
182
+ .preview-modal .modal-dialog {
183
+ max-width: 80%;
184
+ }
185
+ .status-pending { color: #ffc107; }
186
+ .status-completed { color: #28a745; }
187
  </style>
188
  </head>
189
  <body class="bg-light">
 
194
  <div class="col-md-6">
195
  <h1 class="text-white mb-0">Générateur de Devis</h1>
196
  </div>
197
+ <div class="col-md-6 text-end d-flex justify-content-end align-items-center gap-3">
198
+ <a href="/history" class="btn btn-light">
199
+ <i class="fas fa-history"></i> Historique
200
+ </a>
201
  <img src="/static/logo.png" alt="Logo" height="50" class="img-fluid">
202
  </div>
203
  </div>
 
617
  };
618
 
619
  // Function to add a new item row
620
+ function addItemRow(item, table) {
621
+ console.log('Adding item to table:', item);
622
+
623
+ // Check for existing item
624
+ const existingRow = Array.from(table.querySelectorAll('tr')).find(row =>
625
+ row.querySelector('input[name="description"]').value === item.description
626
+ );
627
 
628
  if (existingRow) {
629
+ console.log('Found existing item:', existingRow);
630
+ const replace = confirm("Article Existant\n\nCet article existe déjà dans le tableau. Voulez-vous le remplacer ?");
631
+ if (replace) {
632
+ console.log('Replacing existing item');
633
+ table.removeChild(existingRow);
634
+ insertNewRow(item, table);
635
+ } else {
636
+ console.log('User chose not to replace item');
637
+ }
 
638
  } else {
639
+ console.log('Adding new item');
640
+ insertNewRow(item, table);
641
  }
642
+ }
643
 
644
+ function updateGrandTotal() {
645
+ const totalHT = Array.from(document.querySelectorAll('input[name="totalHT"]'))
646
+ .reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0);
647
+
648
+ const formattedTotalHT = totalHT.toFixed(2);
649
+ const tax = (totalHT * 0.20).toFixed(2);
650
+ const totalTTC = (totalHT * 1.20).toFixed(2);
651
+
652
+ document.querySelector("#totalHT").value = formattedTotalHT;
653
+ document.querySelector("#tax").value = tax;
654
+ document.querySelector("#totalTTC").value = totalTTC;
655
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
 
657
+ function insertNewRow(item, table) {
658
+ console.log('Inserting new row with item:', item);
659
+ const row = document.createElement('tr');
660
+ const total = ((item.quantity || 0) * (item.length || 0) * (item.unit_price || 0)).toFixed(2);
661
+
662
+ row.innerHTML = `
663
+ <td>
664
+ <input type="text" class="form-control" name="description" value="${item.description || ''}" readonly>
665
+ </td>
666
+ <td>
667
+ <input type="text" class="form-control" name="unit" value="${item.unit || ''}" readonly>
668
+ </td>
669
+ <td>
670
+ <input type="number" class="form-control" name="quantity" value="${item.quantity || 0}" onchange="updateTotal(this)">
671
+ </td>
672
+ <td>
673
+ <input type="number" class="form-control" name="length" value="${item.length || 0}" onchange="updateTotal(this)">
674
+ </td>
675
+ <td>
676
+ <input type="number" class="form-control" name="unitPrice" value="${item.unit_price || 0}" onchange="updateTotal(this)">
677
+ </td>
678
+ <td>
679
+ <input type="number" class="form-control" name="totalHT" value="${total}" readonly>
680
+ </td>
681
+ <td>
682
+ <button type="button" class="btn btn-danger btn-sm" onclick="this.closest('tr').remove(); updateGrandTotal();">
683
+ <i class="fas fa-trash"></i>
684
+ </button>
685
+ </td>
686
+ `;
687
+ table.appendChild(row);
688
+ console.log('Row inserted successfully');
689
+ updateGrandTotal();
690
  }
691
 
692
  // Function to update totals
 
810
  invoiceForm.addEventListener("submit", async (event) => {
811
  event.preventDefault();
812
 
813
+ const urlParams = new URLSearchParams(window.location.search);
814
+ const editId = urlParams.get('edit');
815
+
816
  const getAllItems = () => {
817
  const items = [];
818
  [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
 
849
  console.log("Sending invoice data:", data);
850
 
851
  try {
852
+ // Determine if we're creating or updating
853
+ const url = editId ? `/api/invoices/${editId}` : "/api/invoices/";
854
+ const method = editId ? "PUT" : "POST";
855
+
856
+ const response = await fetch(url, {
857
+ method: method,
858
  headers: { "Content-Type": "application/json" },
859
  body: JSON.stringify(data)
860
  });
861
 
862
+ if (!response.ok) {
863
+ const errorData = await response.json();
864
  console.error("Server error:", errorData);
865
+ throw new Error(`Failed to ${editId ? 'update' : 'create'} invoice: ${JSON.stringify(errorData)}`);
866
  }
867
 
868
+ const invoice = await response.json();
869
+ console.log(editId ? "Updated invoice:" : "Created invoice:", invoice);
870
 
871
+ // Generate PDF
872
  const pdfResponse = await fetch(`/api/invoices/${invoice.id}/generate-pdf`, {
873
  method: "POST",
874
+ headers: { "Content-Type": "application/json" },
 
 
875
  body: JSON.stringify(invoice)
876
  });
877
 
 
883
 
884
  // Handle PDF download
885
  const blob = await pdfResponse.blob();
886
+ const downloadUrl = window.URL.createObjectURL(blob);
887
  const a = document.createElement("a");
888
+ a.href = downloadUrl;
889
  a.download = `devis_${invoice.invoice_number}.pdf`;
890
  document.body.appendChild(a);
891
  a.click();
892
  document.body.removeChild(a);
893
+ window.URL.revokeObjectURL(downloadUrl);
894
+
895
+ // Update status to success after PDF generation
896
+ try {
897
+ const updateStatusResponse = await fetch(`/api/invoices/${invoice.id}/status`, {
898
+ method: "PUT",
899
+ headers: { "Content-Type": "application/json" },
900
+ body: JSON.stringify({ status: "completed" })
901
+ });
902
+
903
+ if (!updateStatusResponse.ok) {
904
+ console.error("Failed to update status:", await updateStatusResponse.text());
905
+ } else {
906
+ console.log("Status updated successfully");
907
+ }
908
+ } catch (error) {
909
+ console.error("Error updating status:", error);
910
+ }
911
+
912
+ // Redirect to history page after successful update
913
+ if (editId) {
914
+ window.location.href = '/history';
915
+ }
916
 
917
  } catch (error) {
918
  console.error("Error:", error);
919
  alert(`Error: ${error.message}`);
920
  }
 
 
 
 
 
 
921
  });
922
 
923
  // Set today's date automatically
 
1064
  alert('Error generating Excel file: ' + error.message);
1065
  }
1066
  });
1067
+
1068
+ // Check for edit mode
1069
+ const urlParams = new URLSearchParams(window.location.search);
1070
+ const editId = urlParams.get('edit');
1071
+
1072
+ if (editId) {
1073
+ console.log('Loading invoice for editing, ID:', editId);
1074
+ fetch(`/api/invoices/${editId}`)
1075
+ .then(response => response.json())
1076
+ .then(invoice => {
1077
+ console.log('Loaded invoice data:', invoice);
1078
+
1079
+ // Populate form fields
1080
+ document.querySelector("#clientName").value = invoice.client_name;
1081
+ document.querySelector("#clientPhone").value = invoice.client_phone;
1082
+ document.querySelector("#clientAddress").value = invoice.address;
1083
+ document.querySelector("#ville").value = invoice.ville || '';
1084
+ document.querySelector("#project").value = invoice.project;
1085
+ document.querySelector("#invoiceNumber").value = invoice.invoice_number;
1086
+ document.querySelector("#date").value = invoice.date.split('T')[0];
1087
+ document.querySelector("#plancher").value = invoice.frame_number || 'PH RDC';
1088
+ document.querySelector("#commercial").value = invoice.commercial || 'divers';
1089
+ document.querySelector("#totalHT").value = invoice.total_ht;
1090
+ document.querySelector("#tax").value = invoice.tax;
1091
+ document.querySelector("#totalTTC").value = invoice.total_ttc;
1092
+
1093
+ // Clear existing tables first
1094
+ console.log('Clearing existing tables');
1095
+ [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
1096
+ table.innerHTML = '';
1097
+ });
1098
+
1099
+ // Populate items
1100
+ console.log('Populating items:', invoice.items);
1101
+ invoice.items.forEach(item => {
1102
+ console.log('Processing item:', item);
1103
+ if (item.description.includes('PCP')) {
1104
+ console.log('Adding to poutrelles table');
1105
+ addItemRow(item, poutrellesTable);
1106
+ } else if (item.description.includes('HOURDIS')) {
1107
+ console.log('Adding to hourdis table');
1108
+ addItemRow(item, hourdisTable);
1109
+ } else if (item.description.includes('PTS')) {
1110
+ console.log('Adding to panneau table');
1111
+ addItemRow(item, panneauTable);
1112
+ } else {
1113
+ console.log('Adding to agglos table');
1114
+ addItemRow(item, agglosTable);
1115
+ }
1116
+ });
1117
+ })
1118
+ .catch(error => {
1119
+ console.error("Error loading invoice:", error);
1120
+ alert("Error loading invoice data");
1121
+ });
1122
+ }
1123
  });
1124
  </script>
1125
  </body>
sql_app.db CHANGED
Binary files a/sql_app.db and b/sql_app.db differ