EL GHAFRAOUI AYOUB commited on
Commit
eadae2e
·
1 Parent(s): b636606
alembic.ini CHANGED
@@ -1,64 +1,28 @@
1
  # A generic, single database configuration.
2
 
3
  [alembic]
4
- # path to migration scripts
5
  script_location = alembic
6
 
7
- # template used to generate migration files
8
  # file_template = %%(rev)s_%%(slug)s
9
 
10
- # sys.path path, will be prepended to sys.path if present.
11
- # defaults to the current working directory.
12
  prepend_sys_path = .
13
 
14
- # timezone to use when rendering the date within the migration file
15
- # as well as the filename.
16
- # If specified, requires the python-dateutil library that can be
17
- # installed by adding `alembic[tz]` to the pip requirements
18
- # timezone =
19
-
20
- # max length of characters to apply to the
21
- # "slug" field
22
- # truncate_slug_length = 40
23
-
24
- # set to 'true' to run the environment during
25
- # the 'revision' command, regardless of autogenerate
26
- # revision_environment = false
27
-
28
- # set to 'true' to allow .pyc and .pyo files without
29
- # a source .py file to be detected as revisions in the
30
- # versions/ directory
31
- # sourceless = false
32
-
33
- # version location specification; This defaults
34
- # to alembic/versions. When using multiple version
35
- # directories, initial revisions must be specified with --version-path.
36
- # The path separator used here should be the separator specified by "version_path_separator" below.
37
- # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
38
-
39
- # version path separator; As mentioned above, this is the character used to split
40
- # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
41
- # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or colons.
42
- # Valid values for version_path_separator are:
43
- #
44
- # version_path_separator = :
45
- # version_path_separator = ;
46
- # version_path_separator = space
47
  version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
48
 
49
- # the output encoding used when revision files
50
- # are written from script.py.mako
51
- # output_encoding = utf-8
52
-
53
  sqlalchemy.url = sqlite+aiosqlite:///./sql_app.db
54
 
 
 
 
55
 
 
56
  [post_write_hooks]
57
- # post_write_hooks defines scripts or Python functions that are run
58
- # on newly generated revision scripts. See the documentation for further
59
- # detail and examples
60
-
61
- # format using "black" - use the console_scripts runner, against the "black" entrypoint
62
  # hooks = black
63
  # black.type = console_scripts
64
  # black.entrypoint = black
@@ -97,4 +61,4 @@ formatter = generic
97
 
98
  [formatter_generic]
99
  format = %(levelname)-5.5s [%(name)s] %(message)s
100
- datefmt = %H:%M:%S
 
1
  # A generic, single database configuration.
2
 
3
  [alembic]
4
+ # Path to migration scripts
5
  script_location = alembic
6
 
7
+ # Template used to generate migration files
8
  # file_template = %%(rev)s_%%(slug)s
9
 
10
+ # Sys.path path, will be prepended to sys.path if present.
11
+ # Defaults to the current working directory.
12
  prepend_sys_path = .
13
 
14
+ # Version path separator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
16
 
17
+ # Database connection string (SQLite persistent database)
 
 
 
18
  sqlalchemy.url = sqlite+aiosqlite:///./sql_app.db
19
 
20
+ # Explicitly set pooling to avoid unnecessary resets
21
+ [sqlalchemy]
22
+ poolclass = QueuePool
23
 
24
+ # Post-migration hooks (optional, can be customized)
25
  [post_write_hooks]
 
 
 
 
 
26
  # hooks = black
27
  # black.type = console_scripts
28
  # black.entrypoint = black
 
61
 
62
  [formatter_generic]
63
  format = %(levelname)-5.5s [%(name)s] %(message)s
64
+ datefmt = %H:%M:%S
alembic/__pycache__/env.cpython-312.pyc CHANGED
Binary files a/alembic/__pycache__/env.cpython-312.pyc and b/alembic/__pycache__/env.cpython-312.pyc differ
 
alembic/versions/3ac87142c0f1_add_invoice_sequence.py DELETED
@@ -1,28 +0,0 @@
1
- """add invoice sequence and client type
2
-
3
- Revision ID: xxxx
4
- Revises: previous_revision_id
5
- Create Date: 2024-xx-xx
6
- """
7
- from alembic import op
8
- import sqlalchemy as sa
9
-
10
- def upgrade():
11
- # Create sequence for invoice IDs
12
- op.execute('CREATE SEQUENCE IF NOT EXISTS invoice_id_seq START 1')
13
-
14
- # Add client_type column if not exists
15
- op.add_column('invoices', sa.Column('client_type', sa.String(10), nullable=True))
16
-
17
- # Make invoice_number unique
18
- op.create_unique_constraint('uq_invoice_number', 'invoices', ['invoice_number'])
19
-
20
- def downgrade():
21
- # Remove unique constraint
22
- op.drop_constraint('uq_invoice_number', 'invoices')
23
-
24
- # Drop client_type column
25
- op.drop_column('invoices', 'client_type')
26
-
27
- # Drop sequence
28
- op.execute('DROP SEQUENCE IF EXISTS invoice_id_seq')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
alembic/versions/59c6283aeb7f_add_invoice_sequence.py DELETED
@@ -1,30 +0,0 @@
1
- """add invoice sequence
2
-
3
- Revision ID: 59c6283aeb7f
4
- Revises: f8ee2909b4a1
5
- Create Date: 2025-01-28 23:00:46.031532
6
-
7
- """
8
- from typing import Sequence, Union
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
-
13
-
14
- # revision identifiers, used by Alembic.
15
- revision: str = '59c6283aeb7f'
16
- down_revision: Union[str, None] = 'f8ee2909b4a1'
17
- branch_labels: Union[str, Sequence[str], None] = None
18
- depends_on: Union[str, Sequence[str], None] = None
19
-
20
-
21
- def upgrade() -> None:
22
- # ### commands auto generated by Alembic - please adjust! ###
23
- pass
24
- # ### end Alembic commands ###
25
-
26
-
27
- def downgrade() -> None:
28
- # ### commands auto generated by Alembic - please adjust! ###
29
- pass
30
- # ### end Alembic commands ###
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
alembic/versions/9db9ce94286f_add_commercial_field.py DELETED
@@ -1,65 +0,0 @@
1
- """add_commercial_field
2
-
3
- Revision ID: 9db9ce94286f
4
- Revises:
5
- Create Date: 2025-01-29 13:45:40.493950
6
-
7
- """
8
- from typing import Sequence, Union
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
-
13
-
14
- # revision identifiers, used by Alembic.
15
- revision: str = '9db9ce94286f'
16
- down_revision: Union[str, None] = None
17
- branch_labels: Union[str, Sequence[str], None] = None
18
- depends_on: Union[str, Sequence[str], None] = None
19
-
20
-
21
- def upgrade() -> None:
22
- # ### commands auto generated by Alembic - please adjust! ###
23
- op.create_table('invoices',
24
- sa.Column('id', sa.Integer(), nullable=False),
25
- sa.Column('invoice_number', sa.String(), nullable=True),
26
- sa.Column('date', sa.DateTime(), nullable=True),
27
- sa.Column('project', sa.String(), nullable=True),
28
- sa.Column('client_name', sa.String(), nullable=True),
29
- sa.Column('client_phone', sa.String(), nullable=True),
30
- sa.Column('address', sa.String(), nullable=True),
31
- sa.Column('total_ht', sa.Float(), nullable=True),
32
- sa.Column('tax', sa.Float(), nullable=True),
33
- sa.Column('total_ttc', sa.Float(), nullable=True),
34
- sa.Column('frame_number', sa.String(), nullable=True),
35
- sa.Column('customer_name', sa.String(), nullable=True),
36
- sa.Column('amount', sa.Float(), nullable=True),
37
- sa.Column('status', sa.String(), nullable=True),
38
- sa.Column('created_at', sa.DateTime(), nullable=True),
39
- sa.Column('commercial', sa.String(), nullable=True),
40
- sa.PrimaryKeyConstraint('id')
41
- )
42
- op.create_index(op.f('ix_invoices_id'), 'invoices', ['id'], unique=False)
43
- op.create_table('invoice_items',
44
- sa.Column('id', sa.Integer(), nullable=False),
45
- sa.Column('invoice_id', sa.Integer(), nullable=True),
46
- sa.Column('description', sa.String(), nullable=True),
47
- sa.Column('unit', sa.String(), nullable=True),
48
- sa.Column('quantity', sa.Integer(), nullable=True),
49
- sa.Column('length', sa.Float(), nullable=True),
50
- sa.Column('unit_price', sa.Float(), nullable=True),
51
- sa.Column('total_price', sa.Float(), nullable=True),
52
- sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ),
53
- sa.PrimaryKeyConstraint('id')
54
- )
55
- op.create_index(op.f('ix_invoice_items_id'), 'invoice_items', ['id'], unique=False)
56
- # ### end Alembic commands ###
57
-
58
-
59
- def downgrade() -> None:
60
- # ### commands auto generated by Alembic - please adjust! ###
61
- op.drop_index(op.f('ix_invoice_items_id'), table_name='invoice_items')
62
- op.drop_table('invoice_items')
63
- op.drop_index(op.f('ix_invoices_id'), table_name='invoices')
64
- op.drop_table('invoices')
65
- # ### end Alembic commands ###
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
alembic/versions/__pycache__/3ac87142c0f1_add_invoice_sequence.cpython-312.pyc DELETED
Binary file (1.39 kB)
 
alembic/versions/__pycache__/59c6283aeb7f_add_invoice_sequence.cpython-312.pyc DELETED
Binary file (1.01 kB)
 
alembic/versions/__pycache__/9db9ce94286f_add_commercial_field.cpython-312.pyc DELETED
Binary file (4.86 kB)
 
alembic/versions/__pycache__/f8ee2909b4a1_add_client_type.cpython-312.pyc DELETED
Binary file (974 Bytes)
 
alembic/versions/f8ee2909b4a1_add_client_type.py DELETED
@@ -1,30 +0,0 @@
1
- """add client type
2
-
3
- Revision ID: f8ee2909b4a1
4
- Revises:
5
- Create Date: 2025-01-28 22:54:07.661020
6
-
7
- """
8
- from typing import Sequence, Union
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
-
13
-
14
- # revision identifiers, used by Alembic.
15
- revision: str = 'f8ee2909b4a1'
16
- down_revision: Union[str, None] = None
17
- branch_labels: Union[str, Sequence[str], None] = None
18
- depends_on: Union[str, Sequence[str], None] = None
19
-
20
-
21
- def upgrade() -> None:
22
- # ### commands auto generated by Alembic - please adjust! ###
23
- pass
24
- # ### end Alembic commands ###
25
-
26
-
27
- def downgrade() -> None:
28
- # ### commands auto generated by Alembic - please adjust! ###
29
- pass
30
- # ### end Alembic commands ###
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/data/sql_app.db DELETED
Binary file (28.7 kB)
 
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/db/database.py CHANGED
@@ -25,4 +25,7 @@ async def get_db() -> AsyncSession:
25
  await session.rollback()
26
  raise
27
  finally:
28
- await session.close()
 
 
 
 
25
  await session.rollback()
26
  raise
27
  finally:
28
+ await session.close()
29
+
30
+
31
+ ###sdf
app/models/invoice.py DELETED
@@ -1,10 +0,0 @@
1
- from sqlalchemy import Column, Integer, String, DateTime, Float, ForeignKey, Sequence
2
- from sqlalchemy.orm import relationship
3
- from ..database import Base
4
-
5
- class Invoice(Base):
6
- __tablename__ = "invoices"
7
-
8
- id = Column(Integer, Sequence('invoice_id_seq'), primary_key=True)
9
- invoice_number = Column(String, unique=True)
10
- # ... rest of your columns ...
 
 
 
 
 
 
 
 
 
 
 
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/invoice.py DELETED
@@ -1,47 +0,0 @@
1
- from fastapi import APIRouter, Depends, HTTPException
2
- from sqlalchemy.orm import Session
3
- from sqlalchemy import func
4
- from typing import Optional
5
- from ..database import get_db
6
- from ..models.invoice import Invoice, ClientType
7
-
8
- router = APIRouter()
9
-
10
- @router.get("/api/invoices/next-number/{client_type}")
11
- async def get_next_invoice_number(client_type: str, db: Session = Depends(get_db)):
12
- if client_type not in ClientType.__members__:
13
- raise HTTPException(status_code=400, detail="Invalid client type")
14
-
15
- # Get the last invoice number for this client type
16
- last_invoice = db.query(Invoice)\
17
- .filter(Invoice.client_type == ClientType[client_type])\
18
- .order_by(Invoice.id.desc())\
19
- .first()
20
-
21
- if last_invoice:
22
- # Extract the number from the last invoice number (DCP/TYPE/NUMBER)
23
- try:
24
- last_number = int(last_invoice.invoice_number.split('/')[-1])
25
- next_number = last_number + 1
26
- except (ValueError, IndexError):
27
- next_number = 1
28
- else:
29
- next_number = 1
30
-
31
- return {"next_number": f"{next_number:04d}"} # Format as 4 digits
32
-
33
- @router.post("/api/invoices/")
34
- async def create_invoice(invoice_data: InvoiceCreate, db: Session = Depends(get_db)):
35
- # Extract client type from invoice number
36
- try:
37
- client_type = invoice_data.invoice_number.split('/')[1]
38
- if client_type not in ClientType.__members__:
39
- raise HTTPException(status_code=400, detail="Invalid client type in invoice number")
40
- except IndexError:
41
- raise HTTPException(status_code=400, detail="Invalid invoice number format")
42
-
43
- db_invoice = Invoice(
44
- client_type=ClientType[client_type],
45
- **invoice_data.dict()
46
- )
47
- # ... rest of your existing create logic ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/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/services/invoice_service.py CHANGED
@@ -20,7 +20,7 @@ class InvoiceService:
20
 
21
  # Constants
22
  HEADER_BLUE = (0.29, 0.45, 0.68)
23
- BLUE_LIGHT = (0.8, 0.8, 1)
24
  WHITE = (1, 1, 1)
25
  BLACK = (0, 0, 0)
26
  MARGIN = 30
@@ -121,8 +121,10 @@ class InvoiceService:
121
  table_x = (page_width - total_width) / 2 # Center table
122
  draw_box(pdf, table_x, table_y, total_width, LINE_HEIGHT, fill_color=HEADER_BLUE)
123
  pdf.setFillColorRGB(*WHITE)
124
-
125
- current_x = table_x
 
 
126
  for title, width in headers:
127
  draw_box(pdf, current_x, table_y, width, LINE_HEIGHT)
128
  draw_centered_text(pdf, title, current_x, table_y + 6, width)
@@ -130,7 +132,7 @@ class InvoiceService:
130
 
131
 
132
  # Draw sections and items
133
- current_y = table_y - LINE_HEIGHT
134
 
135
  def draw_section_header(title):
136
  nonlocal current_y
@@ -186,10 +188,10 @@ class InvoiceService:
186
 
187
  # Draw sections
188
  sections = [
189
- ("POUTRELLES", "PCP"),
190
- ("HOURDIS", "HOURDIS"),
191
- ("PANNEAU TREILLIS SOUDES", "PTS"),
192
- ("AGGLOS", "AGGLOS")
193
  ]
194
 
195
  for section_title, keyword in sections:
@@ -251,17 +253,28 @@ class InvoiceService:
251
  footer_text = "Douar Ait Laarassi Tidili, Cercle El Kelâa, Route de Safi, Km 14-40000 Marrakech"
252
  pdf.drawCentredString(page_width / 2 + 20, 30, footer_text)
253
 
254
-
 
 
 
 
 
 
 
255
  # Add commercial info to footer
256
  print(f"Commercial value: {data.commercial}") # Add this debug line
257
  if data.commercial and data.commercial.lower() != 'divers':
258
  commercial_text = f"Commercial: {data.commercial.upper()}"
259
- pdf.drawString(MARGIN, 20, commercial_text)
 
 
 
 
260
  footer_contact = "Tél: 05 24 01 55 54 Fax : 05 24 01 55 29 E-mail : [email protected]"
261
- pdf.drawCentredString(page_width / 2, 20, footer_contact)
262
  else:
263
  footer_contact = "Tél: 05 24 01 55 54 Fax : 05 24 01 55 29 E-mail : [email protected]"
264
- pdf.drawCentredString(page_width / 2, 20, footer_contact)
265
 
266
  pdf.save()
267
  buffer.seek(0)
 
20
 
21
  # Constants
22
  HEADER_BLUE = (0.29, 0.45, 0.68)
23
+ BLUE_LIGHT = (1.5, 1.5, 1)
24
  WHITE = (1, 1, 1)
25
  BLACK = (0, 0, 0)
26
  MARGIN = 30
 
121
  table_x = (page_width - total_width) / 2 # Center table
122
  draw_box(pdf, table_x, table_y, total_width, LINE_HEIGHT, fill_color=HEADER_BLUE)
123
  pdf.setFillColorRGB(*WHITE)
124
+ # add little bit of space
125
+
126
+
127
+ current_x = table_x
128
  for title, width in headers:
129
  draw_box(pdf, current_x, table_y, width, LINE_HEIGHT)
130
  draw_centered_text(pdf, title, current_x, table_y + 6, width)
 
132
 
133
 
134
  # Draw sections and items
135
+ current_y = table_y - LINE_HEIGHT - 10
136
 
137
  def draw_section_header(title):
138
  nonlocal current_y
 
188
 
189
  # Draw sections
190
  sections = [
191
+ ("POUTRELLES :", "PCP"),
192
+ ("HOURDIS :", "HOURDIS"),
193
+ ("PANNEAU TREILLIS SOUDES :", "PTS"),
194
+ ("AGGLOS :", "AGGLOS")
195
  ]
196
 
197
  for section_title, keyword in sections:
 
253
  footer_text = "Douar Ait Laarassi Tidili, Cercle El Kelâa, Route de Safi, Km 14-40000 Marrakech"
254
  pdf.drawCentredString(page_width / 2 + 20, 30, footer_text)
255
 
256
+ # add the commercial phone number
257
+ # i have salah with 0666666666 and khaled with 077777777 and ismale with 08888888 and jamal with 099999999
258
+ commercial_info = dict(
259
+ salah = "06 62 29 99 78",
260
+ khaled= "06 66 24 80 94",
261
+ ismail= "06 66 24 50 15",
262
+ jamal = "06 70 08 36 50"
263
+ )
264
  # Add commercial info to footer
265
  print(f"Commercial value: {data.commercial}") # Add this debug line
266
  if data.commercial and data.commercial.lower() != 'divers':
267
  commercial_text = f"Commercial: {data.commercial.upper()}"
268
+ commercial_phone = f"Tél: {commercial_info.get(data.commercial.lower(), '')}"
269
+
270
+ pdf.drawString(MARGIN + 10, 30, commercial_text)
271
+ #draw under the commercial text :
272
+ pdf.drawString(MARGIN + 10, 20, commercial_phone)
273
  footer_contact = "Tél: 05 24 01 55 54 Fax : 05 24 01 55 29 E-mail : [email protected]"
274
+ pdf.drawCentredString(page_width / 2 + 10, 20, footer_contact)
275
  else:
276
  footer_contact = "Tél: 05 24 01 55 54 Fax : 05 24 01 55 29 E-mail : [email protected]"
277
+ pdf.drawCentredString(page_width / 2 + 10, 20, footer_contact)
278
 
279
  pdf.save()
280
  buffer.seek(0)
app/templates/index copy 2.html ADDED
@@ -0,0 +1,853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>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;
12
+ --secondary-color: #34495e;
13
+ --accent-color: #3498db;
14
+ --light-gray: #f8f9fa;
15
+ --border-radius: 8px;
16
+ }
17
+
18
+ body {
19
+ background-color: #f5f6fa;
20
+ color: var(--primary-color);
21
+ }
22
+
23
+ .container {
24
+ max-width: 1400px;
25
+ padding: 2rem;
26
+ }
27
+
28
+ .card {
29
+ border: none;
30
+ box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
31
+ border-radius: var(--border-radius);
32
+ margin-bottom: 1.5rem;
33
+ }
34
+
35
+ .card-header {
36
+ background-color: var(--primary-color);
37
+ color: white;
38
+ border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
39
+ padding: 1rem;
40
+ font-weight: 500;
41
+ }
42
+
43
+ .card-body {
44
+ padding: 1.5rem;
45
+ }
46
+
47
+ .form-control {
48
+ border: 1px solid #dee2e6;
49
+ border-radius: var(--border-radius);
50
+ padding: 0.75rem;
51
+ transition: all 0.3s ease;
52
+ }
53
+
54
+ .form-control:focus {
55
+ border-color: var(--accent-color);
56
+ box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
57
+ }
58
+
59
+ .btn-primary {
60
+ background-color: var(--accent-color);
61
+ border: none;
62
+ padding: 0.75rem 1.5rem;
63
+ border-radius: var(--border-radius);
64
+ transition: all 0.3s ease;
65
+ }
66
+
67
+ .btn-primary:hover {
68
+ background-color: #2980b9;
69
+ transform: translateY(-1px);
70
+ }
71
+
72
+ .btn-success {
73
+ background-color: #2ecc71;
74
+ border: none;
75
+ padding: 1rem 2rem;
76
+ border-radius: var(--border-radius);
77
+ font-weight: 500;
78
+ transition: all 0.3s ease;
79
+ }
80
+
81
+ .btn-success:hover {
82
+ background-color: #27ae60;
83
+ transform: translateY(-2px);
84
+ }
85
+
86
+ .table {
87
+ background-color: white;
88
+ border-radius: var(--border-radius);
89
+ overflow: hidden;
90
+ }
91
+
92
+ .table-primary {
93
+ background-color: var(--primary-color);
94
+ color: white;
95
+ }
96
+
97
+ .table th {
98
+ font-weight: 500;
99
+ padding: 1rem;
100
+ }
101
+
102
+ .table td {
103
+ padding: 0.75rem;
104
+ vertical-align: middle;
105
+ }
106
+
107
+ .section-title {
108
+ background-color: var(--secondary-color);
109
+ color: white;
110
+ padding: 1rem;
111
+ margin: 2rem 0 1rem;
112
+ border-radius: var(--border-radius);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: space-between;
116
+ }
117
+
118
+ .section-title h4 {
119
+ margin: 0;
120
+ font-weight: 500;
121
+ }
122
+
123
+ .btn-group {
124
+ background-color: white;
125
+ border-radius: var(--border-radius);
126
+ padding: 0.25rem;
127
+ }
128
+
129
+ .btn-check + .btn-outline-primary {
130
+ color: var(--primary-color);
131
+ border-color: var(--primary-color);
132
+ }
133
+
134
+ .btn-check:checked + .btn-outline-primary {
135
+ background-color: var(--primary-color);
136
+ color: white;
137
+ }
138
+
139
+ .modal-content {
140
+ border-radius: var(--border-radius);
141
+ border: none;
142
+ box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
143
+ }
144
+
145
+ .modal-header {
146
+ background-color: var(--primary-color);
147
+ color: white;
148
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
149
+ }
150
+
151
+ .btn-close {
152
+ filter: brightness(0) invert(1);
153
+ }
154
+
155
+ .btn-danger {
156
+ background-color: #e74c3c;
157
+ border: none;
158
+ border-radius: var(--border-radius);
159
+ transition: all 0.3s ease;
160
+ }
161
+
162
+ .btn-danger:hover {
163
+ background-color: #c0392b;
164
+ }
165
+
166
+ /* Responsive adjustments */
167
+ @media (max-width: 768px) {
168
+ .container {
169
+ padding: 1rem;
170
+ }
171
+
172
+ .card-body {
173
+ padding: 1rem;
174
+ }
175
+
176
+ .btn {
177
+ padding: 0.5rem 1rem;
178
+ }
179
+ }
180
+ </style>
181
+ </head>
182
+ <body class="bg-light">
183
+ <!-- Header -->
184
+ <header class="bg-primary py-4 mb-4 shadow-sm">
185
+ <div class="container">
186
+ <div class="row align-items-center">
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>
194
+ </div>
195
+ </header>
196
+
197
+ <div class="container">
198
+ <!-- Main Content -->
199
+ <div class="row">
200
+ <div class="col-12">
201
+ <div class="card shadow-sm mb-4">
202
+ <div class="card-body">
203
+ <form id="invoiceForm">
204
+ <!-- Client and Invoice Information -->
205
+ <div class="row mb-4">
206
+ <!-- Client Information Card -->
207
+ <div class="col-md-6">
208
+ <div class="card h-100">
209
+ <div class="card-header d-flex justify-content-between align-items-center">
210
+ <h5 class="mb-0">Information Client</h5>
211
+ <i class="fas fa-user"></i>
212
+ </div>
213
+ <div class="card-body">
214
+ <div class="mb-3">
215
+ <label for="clientName" class="form-label">Nom du Client</label>
216
+ <input type="text" class="form-control" id="clientName" required>
217
+ </div>
218
+ <div class="mb-3">
219
+ <label for="clientPhone" class="form-label">Téléphone</label>
220
+ <input type="tel" class="form-control" id="clientPhone" required>
221
+ </div>
222
+ <div class="mb-3">
223
+ <label for="clientAddress" class="form-label">Adresse</label>
224
+ <input type="text" class="form-control" id="clientAddress" required>
225
+ </div>
226
+ <div class="mb-3">
227
+ <label class="form-label">Type Client</label>
228
+ <div class="btn-group w-100" role="group">
229
+ <input type="radio" class="btn-check" name="clientType" id="EE" value="EE" autocomplete="off">
230
+ <label class="btn btn-outline-primary" for="EE">EE</label>
231
+
232
+ <input type="radio" class="btn-check" name="clientType" id="MED" value="MED" autocomplete="off">
233
+ <label class="btn btn-outline-primary" for="MED">MED</label>
234
+
235
+ <input type="radio" class="btn-check" name="clientType" id="AM" value="AM" autocomplete="off">
236
+ <label class="btn btn-outline-primary" for="AM">AM</label>
237
+
238
+ <input type="radio" class="btn-check" name="clientType" id="DIV" value="DIV" autocomplete="off">
239
+ <label class="btn btn-outline-primary" for="DIV">DIV</label>
240
+ </div>
241
+ </div>
242
+ <div class="mb-3">
243
+ <label class="form-label">Commercial</label>
244
+ <select class="form-select" id="commercial" required>
245
+ <option value="">Sélectionner un commercial</option>
246
+ <option value="khaled">Khaled</option>
247
+ <option value="salah">Salah</option>
248
+ <option value="ismail">Ismail</option>
249
+ <option value="jamal">Jamal</option>
250
+ <option value="divers">Divers</option>
251
+ </select>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ <!-- Invoice Information Card -->
258
+ <div class="col-md-6">
259
+ <div class="card h-100">
260
+ <div class="card-header d-flex justify-content-between align-items-center">
261
+ <h5 class="mb-0">Information Devis</h5>
262
+ <i class="fas fa-file-invoice"></i>
263
+ </div>
264
+ <div class="card-body">
265
+ <div class="mb-3">
266
+ <label for="invoiceNumber" class="form-label">N° Devis</label>
267
+ <input type="text" class="form-control" id="invoiceNumber" required>
268
+ </div>
269
+ <div class="mb-3">
270
+ <label for="date" class="form-label">Date</label>
271
+ <input type="date" class="form-control" id="date" readonly>
272
+ </div>
273
+ <div class="mb-3">
274
+ <label for="project" class="form-label">Type d'ouvrage</label>
275
+ <input type="text" class="form-control" id="project" required>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </div>
281
+
282
+ <!-- Items Sections -->
283
+ <div class="card shadow-sm mb-4">
284
+ <div class="card-body">
285
+ <!-- Poutrelles Section -->
286
+ <div class="section-title">
287
+ <h4>POUTRELLES</h4>
288
+ <button type="button" class="btn btn-light" id="addPoutrelles">
289
+ <i class="fas fa-plus"></i> Ajouter Poutrelle
290
+ </button>
291
+ </div>
292
+ <div class="table-responsive">
293
+ <table class="table table-bordered table-hover">
294
+ <thead class="table-primary">
295
+ <tr>
296
+ <th style="width: 30%">Description</th>
297
+ <th style="width: 10%">Unité</th>
298
+ <th style="width: 10%">Quantité</th>
299
+ <th style="width: 15%">Longueur</th>
300
+ <th style="width: 15%">P.U</th>
301
+ <th style="width: 15%">Total HT</th>
302
+ <th style="width: 5%"></th>
303
+ </tr>
304
+ </thead>
305
+ <tbody id="poutrellesTable"></tbody>
306
+ </table>
307
+ </div>
308
+
309
+ <!-- Hourdis Section -->
310
+ <div class="section-title mt-4">
311
+ <h4>HOURDIS</h4>
312
+ <button type="button" class="btn btn-light" id="addHourdis">
313
+ <i class="fas fa-plus"></i> Ajouter Hourdis
314
+ </button>
315
+ </div>
316
+ <div class="table-responsive">
317
+ <table class="table table-bordered table-hover">
318
+ <thead class="table-primary">
319
+ <tr>
320
+ <th style="width: 30%">Description</th>
321
+ <th style="width: 10%">Unité</th>
322
+ <th style="width: 10%">Quantité</th>
323
+ <th style="width: 15%">Longueur</th>
324
+ <th style="width: 15%">P.U</th>
325
+ <th style="width: 15%">Total HT</th>
326
+ <th style="width: 5%"></th>
327
+ </tr>
328
+ </thead>
329
+ <tbody id="hourdisTable"></tbody>
330
+ </table>
331
+ </div>
332
+
333
+ <!-- Panneau Section -->
334
+ <div class="section-title mt-4">
335
+ <h4>PANNEAU TREILLIS SOUDES</h4>
336
+ <button type="button" class="btn btn-light" id="addPanneau">
337
+ <i class="fas fa-plus"></i> Ajouter Panneau
338
+ </button>
339
+ </div>
340
+ <div class="table-responsive">
341
+ <table class="table table-bordered table-hover">
342
+ <thead class="table-primary">
343
+ <tr>
344
+ <th style="width: 30%">Description</th>
345
+ <th style="width: 10%">Unité</th>
346
+ <th style="width: 10%">Quantité</th>
347
+ <th style="width: 15%">Longueur</th>
348
+ <th style="width: 15%">P.U</th>
349
+ <th style="width: 15%">Total HT</th>
350
+ <th style="width: 5%"></th>
351
+ </tr>
352
+ </thead>
353
+ <tbody id="panneauTable"></tbody>
354
+ </table>
355
+ </div>
356
+
357
+ <!-- Agglos Section -->
358
+ <div class="section-title mt-4">
359
+ <h4>AGGLOS</h4>
360
+ <button type="button" class="btn btn-light" id="addAgglos">
361
+ <i class="fas fa-plus"></i> Ajouter Agglos
362
+ </button>
363
+ </div>
364
+ <div class="table-responsive">
365
+ <table class="table table-bordered table-hover">
366
+ <thead class="table-primary">
367
+ <tr>
368
+ <th style="width: 30%">Description</th>
369
+ <th style="width: 10%">Unité</th>
370
+ <th style="width: 10%">Quantité</th>
371
+ <th style="width: 15%">Longueur</th>
372
+ <th style="width: 15%">P.U</th>
373
+ <th style="width: 15%">Total HT</th>
374
+ <th style="width: 5%"></th>
375
+ </tr>
376
+ </thead>
377
+ <tbody id="agglosTable"></tbody>
378
+ </table>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <!-- Totals Section -->
384
+ <div class="row">
385
+ <div class="col-md-6">
386
+ <!-- Empty for spacing -->
387
+ </div>
388
+ <div class="col-md-6">
389
+ <div class="card shadow-sm">
390
+ <div class="card-header">
391
+ <h5 class="mb-0">Totaux</h5>
392
+ </div>
393
+ <div class="card-body">
394
+ <div class="mb-3">
395
+ <label for="totalHT" class="form-label">Total HT</label>
396
+ <input type="number" class="form-control" id="totalHT" readonly>
397
+ </div>
398
+ <div class="mb-3">
399
+ <label for="tax" class="form-label">TVA 20%</label>
400
+ <input type="number" class="form-control" id="tax" readonly>
401
+ </div>
402
+ <div class="mb-3">
403
+ <label for="totalTTC" class="form-label">Total TTC</label>
404
+ <input type="number" class="form-control" id="totalTTC" readonly>
405
+ </div>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <!-- Submit Button -->
412
+ <div class="d-grid gap-2 col-md-6 mx-auto mt-4">
413
+ <button type="submit" class="btn btn-success btn-lg">
414
+ <i class="fas fa-file-pdf"></i> Générer le Devis
415
+ </button>
416
+ </div>
417
+ </form>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </div>
423
+
424
+ <!-- Add Font Awesome for icons -->
425
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
426
+
427
+ <!-- Poutrelles Modal -->
428
+ <div class="modal fade" id="poutrellesModal" tabindex="-1">
429
+ <div class="modal-dialog modal-lg">
430
+ <div class="modal-content">
431
+ <div class="modal-header">
432
+ <h5 class="modal-title">Sélectionner Poutrelle</h5>
433
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
434
+ </div>
435
+ <div class="modal-body">
436
+ <div class="table-responsive">
437
+ <table class="table">
438
+ <thead>
439
+ <tr>
440
+ <th>Description</th>
441
+ <th>Unité</th>
442
+ <th>Quantité</th>
443
+ <th>Longueur</th>
444
+ <th>P.U</th>
445
+ <th>Action</th>
446
+ </tr>
447
+ </thead>
448
+ <tbody></tbody>
449
+ </table>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+
456
+ <!-- Hourdis Modal -->
457
+ <div class="modal fade" id="hourdisModal" tabindex="-1">
458
+ <div class="modal-dialog modal-lg">
459
+ <div class="modal-content">
460
+ <div class="modal-header">
461
+ <h5 class="modal-title">Sélectionner Hourdis</h5>
462
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
463
+ </div>
464
+ <div class="modal-body">
465
+ <div class="table-responsive">
466
+ <table class="table">
467
+ <thead>
468
+ <tr>
469
+ <th>Description</th>
470
+ <th>Unité</th>
471
+ <th>Quantité</th>
472
+ <th>Longueur</th>
473
+ <th>P.U</th>
474
+ <th>Action</th>
475
+ </tr>
476
+ </thead>
477
+ <tbody></tbody>
478
+ </table>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
485
+ <!-- Panneau Modal -->
486
+ <div class="modal fade" id="panneauModal" tabindex="-1">
487
+ <div class="modal-dialog modal-lg">
488
+ <div class="modal-content">
489
+ <div class="modal-header">
490
+ <h5 class="modal-title">Sélectionner Panneau</h5>
491
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
492
+ </div>
493
+ <div class="modal-body">
494
+ <div class="table-responsive">
495
+ <table class="table">
496
+ <thead>
497
+ <tr>
498
+ <th>Description</th>
499
+ <th>Unité</th>
500
+ <th>Quantité</th>
501
+ <th>Longueur</th>
502
+ <th>P.U</th>
503
+ <th>Action</th>
504
+ </tr>
505
+ </thead>
506
+ <tbody></tbody>
507
+ </table>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ </div>
513
+
514
+ <!-- Add Agglos Modal -->
515
+ <div class="modal fade" id="agglosModal" tabindex="-1">
516
+ <div class="modal-dialog modal-lg">
517
+ <div class="modal-content">
518
+ <div class="modal-header">
519
+ <h5 class="modal-title">Sélectionner Agglos</h5>
520
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
521
+ </div>
522
+ <div class="modal-body">
523
+ <div class="table-responsive">
524
+ <table class="table">
525
+ <thead>
526
+ <tr>
527
+ <th>Description</th>
528
+ <th>Unité</th>
529
+ <th>Quantité</th>
530
+ <th>Longueur</th>
531
+ <th>P.U</th>
532
+ <th>Action</th>
533
+ </tr>
534
+ </thead>
535
+ <tbody></tbody>
536
+ </table>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ <!-- Bootstrap JS -->
544
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
545
+
546
+ <script>
547
+ document.addEventListener("DOMContentLoaded", function() {
548
+ const poutrellesTable = document.getElementById("poutrellesTable");
549
+ const hourdisTable = document.getElementById("hourdisTable");
550
+ const panneauTable = document.getElementById("panneauTable");
551
+ const agglosTable = document.getElementById("agglosTable");
552
+ const invoiceForm = document.getElementById("invoiceForm");
553
+ const invoiceNumberInput = document.getElementById("invoiceNumber");
554
+
555
+ // Default items data
556
+ const defaultItems = {
557
+ poutrelles: [
558
+ { description: "PCP 114N", unit: "ML", quantity: 21, length: 3.00, unit_price: 26.00 },
559
+ { description: "PCP 113N", unit: "ML", quantity: 12, length: 4.00, unit_price: 26.00 }
560
+ ],
561
+ hourdis: [
562
+ { description: "HOURDIS TYPE 12", unit: "U", quantity: 1, length: 300, unit_price: 3.50 }
563
+ ],
564
+ panneaux: [
565
+ { description: "PTS SISMIQUE 5*3,5", unit: "U", quantity: 1, length: 9, unit_price: 195.00 }
566
+ ]
567
+ };
568
+
569
+ // Function to add a new item row
570
+ function addItemRow(item, tableBody) {
571
+ const row = document.createElement("tr");
572
+ const total = (item.quantity * item.length * item.unit_price).toFixed(2);
573
+
574
+ row.innerHTML = `
575
+ <td><input type="text" class="form-control" name="description" value="${item.description}" readonly></td>
576
+ <td><input type="text" class="form-control" name="unit" value="${item.unit}" readonly></td>
577
+ <td><input type="number" class="form-control" name="quantity" value="${item.quantity}" required></td>
578
+ <td><input type="number" class="form-control" name="length" value="${item.length}" step="0.01" required></td>
579
+ <td><input type="number" class="form-control" name="unitPrice" value="${item.unit_price}" step="0.01" required></td>
580
+ <td><input type="number" class="form-control" name="totalHT" value="${total}" readonly></td>
581
+ <td><button type="button" class="btn btn-danger btn-sm remove-item">×</button></td>
582
+ `;
583
+
584
+ row.querySelector(".remove-item").addEventListener("click", () => {
585
+ row.remove();
586
+ updateTotals();
587
+ });
588
+
589
+ // Add input event listeners to update total
590
+ ['quantity', 'length', 'unitPrice'].forEach(field => {
591
+ row.querySelector(`input[name='${field}']`).addEventListener('input', () => {
592
+ const qty = parseFloat(row.querySelector('input[name="quantity"]').value) || 0;
593
+ const length = parseFloat(row.querySelector('input[name="length"]').value) || 0;
594
+ const price = parseFloat(row.querySelector('input[name="unitPrice"]').value) || 0;
595
+ const rowTotal = (qty * length * price).toFixed(2);
596
+ row.querySelector('input[name="totalHT"]').value = rowTotal;
597
+ updateTotals();
598
+ });
599
+ });
600
+
601
+ tableBody.appendChild(row);
602
+ updateTotals();
603
+ }
604
+
605
+ // Function to update totals
606
+ function updateTotals() {
607
+ const totalHT = Array.from(document.querySelectorAll('input[name="totalHT"]'))
608
+ .reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0);
609
+
610
+ const formattedTotalHT = totalHT.toFixed(2);
611
+ const tax = (totalHT * 0.20).toFixed(2);
612
+ const totalTTC = (totalHT * 1.20).toFixed(2);
613
+
614
+ document.querySelector("#totalHT").value = formattedTotalHT;
615
+ document.querySelector("#tax").value = tax;
616
+ document.querySelector("#totalTTC").value = totalTTC;
617
+ }
618
+
619
+ // Initialize tables with default items
620
+ defaultItems.poutrelles.forEach(item => addItemRow(item, poutrellesTable));
621
+ defaultItems.hourdis.forEach(item => addItemRow(item, hourdisTable));
622
+ defaultItems.panneaux.forEach(item => addItemRow(item, panneauTable));
623
+
624
+ // Update initial totals
625
+ updateTotals();
626
+
627
+ // Predefined items data
628
+ const predefinedItems = {
629
+ poutrelles: [
630
+ { description: "PCP 114N", unit: "ML", quantity: 21, length: 3.00, unit_price: 26.00 },
631
+ { description: "PCP 113N", unit: "ML", quantity: 12, length: 4.00, unit_price: 26.00 },
632
+ { description: "PCP 113B SISMIQUE", unit: "ML", quantity: 22, length: 4.20, unit_price: 26.00 },
633
+ { description: "PCP 114B SISMIQUE", unit: "ML", quantity: 12, length: 4.90, unit_price: 30.00 },
634
+ { description: "PCP 135 SISMIQUE", unit: "ML", quantity: 8, length: 5.00, unit_price: 38.00 },
635
+ { description: "PCP 156 SISMIQUE", unit: "ML", quantity: 12, length: 5.10, unit_price: 38.00 },
636
+ { description: "PCP 158 SISMIQUE", unit: "ML", quantity: 12, length: 5.50, unit_price: 51.00 }
637
+ ],
638
+ hourdis: [
639
+ { description: "HOURDIS TYPE 08", unit: "U", quantity: 1, length: 200, unit_price: 3.40 },
640
+ { description: "HOURDIS TYPE 12", unit: "U", quantity: 1, length: 300, unit_price: 3.50 },
641
+ { description: "HOURDIS TYPE 16", unit: "U", quantity: 1, length: 0, unit_price: 0 },
642
+ { description: "HOURDIS TYPE 20", unit: "U", quantity: 1, length: 0, unit_price: 0 },
643
+ { description: "HOURDIS TYPE 25", unit: "U", quantity: 1, length: 450, unit_price: 4.80 },
644
+ { description: "HOURDIS TYPE 30", unit: "U", quantity: 1, length: 0, unit_price: 0 }
645
+ ],
646
+ panneaux: [
647
+ { description: "PTS Normal 3,5*3,5", unit: "U", quantity: 1, length: 0, unit_price: 0 },
648
+ { description: "PTS SISMIQUE 5*3,5", unit: "U", quantity: 1, length: 9, unit_price: 195.00 }
649
+ ],
650
+ agglos: [
651
+ { description: "AGGLOS 07", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
652
+ { description: "AGGLOS 10", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
653
+ { description: "AGGLOS 15", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
654
+ { description: "AGGLOS 20", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
655
+ { description: "AGGLOS 25", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
656
+ { description: "AGGLOS A BRANCHER 20", unit: "U", quantity: 1, length: 9, unit_price: 190.00 }
657
+ ]
658
+ };
659
+
660
+ // Function to populate modal with items
661
+ function populateModal(modalId, items, targetTable) {
662
+ const modal = document.querySelector(modalId);
663
+ const tbody = modal.querySelector('tbody');
664
+ tbody.innerHTML = '';
665
+
666
+ items.forEach(item => {
667
+ const row = document.createElement('tr');
668
+ row.innerHTML = `
669
+ <td>${item.description}</td>
670
+ <td>${item.unit}</td>
671
+ <td><input type="number" class="form-control form-control-sm" value="${item.quantity}" name="modal-quantity"></td>
672
+ <td><input type="number" class="form-control form-control-sm" value="${item.length}" step="0.01" name="modal-length"></td>
673
+ <td><input type="number" class="form-control form-control-sm" value="${item.unit_price}" step="0.01" name="modal-price"></td>
674
+ <td>
675
+ <button class="btn btn-primary btn-sm add-item">
676
+ Ajouter
677
+ </button>
678
+ </td>
679
+ `;
680
+
681
+ row.querySelector('.add-item').addEventListener('click', () => {
682
+ const newItem = {
683
+ ...item,
684
+ quantity: parseFloat(row.querySelector('[name="modal-quantity"]').value) || 0,
685
+ length: parseFloat(row.querySelector('[name="modal-length"]').value) || 0,
686
+ unit_price: parseFloat(row.querySelector('[name="modal-price"]').value) || 0
687
+ };
688
+ addItemRow(newItem, targetTable);
689
+ bootstrap.Modal.getInstance(modal).hide();
690
+ });
691
+
692
+ tbody.appendChild(row);
693
+ });
694
+ }
695
+
696
+ // Update button click handlers
697
+ document.querySelector("#addPoutrelles").addEventListener("click", () => {
698
+ populateModal('#poutrellesModal', predefinedItems.poutrelles, poutrellesTable);
699
+ new bootstrap.Modal('#poutrellesModal').show();
700
+ });
701
+
702
+ document.querySelector("#addHourdis").addEventListener("click", () => {
703
+ populateModal('#hourdisModal', predefinedItems.hourdis, hourdisTable);
704
+ new bootstrap.Modal('#hourdisModal').show();
705
+ });
706
+
707
+ document.querySelector("#addPanneau").addEventListener("click", () => {
708
+ populateModal('#panneauModal', predefinedItems.panneaux, panneauTable);
709
+ new bootstrap.Modal('#panneauModal').show();
710
+ });
711
+
712
+ document.querySelector("#addAgglos").addEventListener("click", () => {
713
+ populateModal('#agglosModal', predefinedItems.agglos, agglosTable);
714
+ new bootstrap.Modal('#agglosModal').show();
715
+ });
716
+
717
+ // Form submission
718
+ invoiceForm.addEventListener("submit", async (event) => {
719
+ event.preventDefault();
720
+
721
+ const getAllItems = () => {
722
+ const items = [];
723
+ [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
724
+ items.push(...Array.from(table.querySelectorAll("tr")).map(row => ({
725
+ description: row.querySelector("input[name='description']").value,
726
+ unit: row.querySelector("input[name='unit']").value,
727
+ quantity: parseInt(row.querySelector("input[name='quantity']").value, 10),
728
+ length: parseFloat(row.querySelector("input[name='length']").value),
729
+ unit_price: parseFloat(row.querySelector("input[name='unitPrice']").value),
730
+ total_price: parseFloat(row.querySelector("input[name='totalHT']").value)
731
+ })));
732
+ });
733
+ return items;
734
+ };
735
+
736
+ const data = {
737
+ invoice_number: document.querySelector("#invoiceNumber").value,
738
+ date: new Date(document.querySelector("#date").value).toISOString(),
739
+ project: document.querySelector("#project").value,
740
+ client_name: document.querySelector("#clientName").value,
741
+ client_phone: document.querySelector("#clientPhone").value,
742
+ address: document.querySelector("#clientAddress").value,
743
+ total_ht: parseFloat(document.querySelector("#totalHT").value || 0),
744
+ tax: parseFloat(document.querySelector("#tax").value || 0),
745
+ total_ttc: parseFloat(document.querySelector("#totalTTC").value || 0),
746
+ items: getAllItems(),
747
+ frame_number: "",
748
+ status: "pending",
749
+ created_at: new Date().toISOString(),
750
+ commercial: document.querySelector("#commercial").value || "divers"
751
+ };
752
+
753
+ console.log("Sending invoice data:", data);
754
+
755
+ try {
756
+ // First create the invoice
757
+ const createResponse = await fetch("/api/invoices/", {
758
+ method: "POST",
759
+ headers: { "Content-Type": "application/json" },
760
+ body: JSON.stringify(data)
761
+ });
762
+
763
+ if (!createResponse.ok) {
764
+ const errorData = await createResponse.json();
765
+ console.error("Server error:", errorData);
766
+ throw new Error(`Failed to create invoice: ${JSON.stringify(errorData)}`);
767
+ }
768
+
769
+ const invoice = await createResponse.json();
770
+ console.log("Created invoice:", invoice);
771
+
772
+ // Then generate the PDF
773
+ const pdfResponse = await fetch(`/api/invoices/${invoice.id}/generate-pdf`, {
774
+ method: "POST",
775
+ headers: {
776
+ "Content-Type": "application/json"
777
+ },
778
+ body: JSON.stringify(invoice)
779
+ });
780
+
781
+ if (!pdfResponse.ok) {
782
+ const errorData = await pdfResponse.text();
783
+ console.error("PDF generation error:", errorData);
784
+ throw new Error(`Failed to generate PDF: ${errorData}`);
785
+ }
786
+
787
+ // Handle PDF download
788
+ const blob = await pdfResponse.blob();
789
+ const url = window.URL.createObjectURL(blob);
790
+ const a = document.createElement("a");
791
+ a.href = url;
792
+ a.download = `devis_${invoice.invoice_number}.pdf`;
793
+ document.body.appendChild(a);
794
+ a.click();
795
+ document.body.removeChild(a);
796
+ window.URL.revokeObjectURL(url);
797
+
798
+ } catch (error) {
799
+ console.error("Error:", error);
800
+ alert(`Error: ${error.message}`);
801
+ }
802
+
803
+ // After successful submission, update the invoice number for the current type
804
+ const selectedType = document.querySelector('input[name="clientType"]:checked')?.value;
805
+ if (selectedType) {
806
+ await updateInvoiceNumber(selectedType);
807
+ }
808
+ });
809
+
810
+ // Set today's date automatically
811
+ const today = new Date();
812
+ const formattedDate = today.toISOString().split('T')[0];
813
+ document.querySelector("#date").value = formattedDate;
814
+
815
+ // Function to get and update the invoice number
816
+ async function updateInvoiceNumber(selectedType) {
817
+ try {
818
+ // Disable the input while fetching
819
+ invoiceNumberInput.disabled = true;
820
+
821
+ const response = await fetch(`/api/invoices/last-number/${selectedType}`);
822
+
823
+ if (response.ok) {
824
+ const data = await response.json();
825
+ invoiceNumberInput.value = data.formatted_number;
826
+ } else {
827
+ throw new Error('Failed to get invoice number');
828
+ }
829
+ } catch (error) {
830
+ console.error('Error:', error);
831
+ alert('Error getting invoice number');
832
+ } finally {
833
+ // Re-enable the input
834
+ invoiceNumberInput.disabled = false;
835
+ }
836
+ }
837
+
838
+ // Update invoice number when client type is selected
839
+ document.querySelectorAll('input[name="clientType"]').forEach(input => {
840
+ input.addEventListener('change', () => {
841
+ updateInvoiceNumber(input.value);
842
+ });
843
+ });
844
+
845
+ // Update the number when the page loads if a type is already selected
846
+ const selectedType = document.querySelector('input[name="clientType"]:checked')?.value;
847
+ if (selectedType) {
848
+ updateInvoiceNumber(selectedType);
849
+ }
850
+ });
851
+ </script>
852
+ </body>
853
+ </html>
app/templates/index copy 3.html ADDED
@@ -0,0 +1,853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>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;
12
+ --secondary-color: #34495e;
13
+ --accent-color: #3498db;
14
+ --light-gray: #f8f9fa;
15
+ --border-radius: 8px;
16
+ }
17
+
18
+ body {
19
+ background-color: #f5f6fa;
20
+ color: var(--primary-color);
21
+ }
22
+
23
+ .container {
24
+ max-width: 1400px;
25
+ padding: 2rem;
26
+ }
27
+
28
+ .card {
29
+ border: none;
30
+ box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
31
+ border-radius: var(--border-radius);
32
+ margin-bottom: 1.5rem;
33
+ }
34
+
35
+ .card-header {
36
+ background-color: var(--primary-color);
37
+ color: white;
38
+ border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
39
+ padding: 1rem;
40
+ font-weight: 500;
41
+ }
42
+
43
+ .card-body {
44
+ padding: 1.5rem;
45
+ }
46
+
47
+ .form-control {
48
+ border: 1px solid #dee2e6;
49
+ border-radius: var(--border-radius);
50
+ padding: 0.75rem;
51
+ transition: all 0.3s ease;
52
+ }
53
+
54
+ .form-control:focus {
55
+ border-color: var(--accent-color);
56
+ box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
57
+ }
58
+
59
+ .btn-primary {
60
+ background-color: var(--accent-color);
61
+ border: none;
62
+ padding: 0.75rem 1.5rem;
63
+ border-radius: var(--border-radius);
64
+ transition: all 0.3s ease;
65
+ }
66
+
67
+ .btn-primary:hover {
68
+ background-color: #2980b9;
69
+ transform: translateY(-1px);
70
+ }
71
+
72
+ .btn-success {
73
+ background-color: #2ecc71;
74
+ border: none;
75
+ padding: 1rem 2rem;
76
+ border-radius: var(--border-radius);
77
+ font-weight: 500;
78
+ transition: all 0.3s ease;
79
+ }
80
+
81
+ .btn-success:hover {
82
+ background-color: #27ae60;
83
+ transform: translateY(-2px);
84
+ }
85
+
86
+ .table {
87
+ background-color: white;
88
+ border-radius: var(--border-radius);
89
+ overflow: hidden;
90
+ }
91
+
92
+ .table-primary {
93
+ background-color: var(--primary-color);
94
+ color: white;
95
+ }
96
+
97
+ .table th {
98
+ font-weight: 500;
99
+ padding: 1rem;
100
+ }
101
+
102
+ .table td {
103
+ padding: 0.75rem;
104
+ vertical-align: middle;
105
+ }
106
+
107
+ .section-title {
108
+ background-color: var(--secondary-color);
109
+ color: white;
110
+ padding: 1rem;
111
+ margin: 2rem 0 1rem;
112
+ border-radius: var(--border-radius);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: space-between;
116
+ }
117
+
118
+ .section-title h4 {
119
+ margin: 0;
120
+ font-weight: 500;
121
+ }
122
+
123
+ .btn-group {
124
+ background-color: white;
125
+ border-radius: var(--border-radius);
126
+ padding: 0.25rem;
127
+ }
128
+
129
+ .btn-check + .btn-outline-primary {
130
+ color: var(--primary-color);
131
+ border-color: var(--primary-color);
132
+ }
133
+
134
+ .btn-check:checked + .btn-outline-primary {
135
+ background-color: var(--primary-color);
136
+ color: white;
137
+ }
138
+
139
+ .modal-content {
140
+ border-radius: var(--border-radius);
141
+ border: none;
142
+ box-shadow: 0 5px 25px rgba(0, 0, 0, 0.2);
143
+ }
144
+
145
+ .modal-header {
146
+ background-color: var(--primary-color);
147
+ color: white;
148
+ border-radius: var(--border-radius) var(--border-radius) 0 0;
149
+ }
150
+
151
+ .btn-close {
152
+ filter: brightness(0) invert(1);
153
+ }
154
+
155
+ .btn-danger {
156
+ background-color: #e74c3c;
157
+ border: none;
158
+ border-radius: var(--border-radius);
159
+ transition: all 0.3s ease;
160
+ }
161
+
162
+ .btn-danger:hover {
163
+ background-color: #c0392b;
164
+ }
165
+
166
+ /* Responsive adjustments */
167
+ @media (max-width: 768px) {
168
+ .container {
169
+ padding: 1rem;
170
+ }
171
+
172
+ .card-body {
173
+ padding: 1rem;
174
+ }
175
+
176
+ .btn {
177
+ padding: 0.5rem 1rem;
178
+ }
179
+ }
180
+ </style>
181
+ </head>
182
+ <body class="bg-light">
183
+ <!-- Header -->
184
+ <header class="bg-primary py-4 mb-4 shadow-sm">
185
+ <div class="container">
186
+ <div class="row align-items-center">
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>
194
+ </div>
195
+ </header>
196
+
197
+ <div class="container">
198
+ <!-- Main Content -->
199
+ <div class="row">
200
+ <div class="col-12">
201
+ <div class="card shadow-sm mb-4">
202
+ <div class="card-body">
203
+ <form id="invoiceForm">
204
+ <!-- Client and Invoice Information -->
205
+ <div class="row mb-4">
206
+ <!-- Client Information Card -->
207
+ <div class="col-md-6">
208
+ <div class="card h-100">
209
+ <div class="card-header d-flex justify-content-between align-items-center">
210
+ <h5 class="mb-0">Information Client</h5>
211
+ <i class="fas fa-user"></i>
212
+ </div>
213
+ <div class="card-body">
214
+ <div class="mb-3">
215
+ <label for="clientName" class="form-label">Nom du Client</label>
216
+ <input type="text" class="form-control" id="clientName" required>
217
+ </div>
218
+ <div class="mb-3">
219
+ <label for="clientPhone" class="form-label">Téléphone</label>
220
+ <input type="tel" class="form-control" id="clientPhone" required>
221
+ </div>
222
+ <div class="mb-3">
223
+ <label for="clientAddress" class="form-label">Adresse</label>
224
+ <input type="text" class="form-control" id="clientAddress" required>
225
+ </div>
226
+ <div class="mb-3">
227
+ <label class="form-label">Type Client</label>
228
+ <div class="btn-group w-100" role="group">
229
+ <input type="radio" class="btn-check" name="clientType" id="EE" value="EE" autocomplete="off">
230
+ <label class="btn btn-outline-primary" for="EE">EE</label>
231
+
232
+ <input type="radio" class="btn-check" name="clientType" id="MED" value="MED" autocomplete="off">
233
+ <label class="btn btn-outline-primary" for="MED">MED</label>
234
+
235
+ <input type="radio" class="btn-check" name="clientType" id="AM" value="AM" autocomplete="off">
236
+ <label class="btn btn-outline-primary" for="AM">AM</label>
237
+
238
+ <input type="radio" class="btn-check" name="clientType" id="DIV" value="DIV" autocomplete="off">
239
+ <label class="btn btn-outline-primary" for="DIV">DIV</label>
240
+ </div>
241
+ </div>
242
+ <div class="mb-3">
243
+ <label class="form-label">Commercial</label>
244
+ <select class="form-select" id="commercial" required>
245
+ <option value="">Sélectionner un commercial</option>
246
+ <option value="khaled">Khaled</option>
247
+ <option value="salah">Salah</option>
248
+ <option value="ismail">Ismail</option>
249
+ <option value="jamal">Jamal</option>
250
+ <option value="divers">Divers</option>
251
+ </select>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ <!-- Invoice Information Card -->
258
+ <div class="col-md-6">
259
+ <div class="card h-100">
260
+ <div class="card-header d-flex justify-content-between align-items-center">
261
+ <h5 class="mb-0">Information Devis</h5>
262
+ <i class="fas fa-file-invoice"></i>
263
+ </div>
264
+ <div class="card-body">
265
+ <div class="mb-3">
266
+ <label for="invoiceNumber" class="form-label">N° Devis</label>
267
+ <input type="text" class="form-control" id="invoiceNumber" required>
268
+ </div>
269
+ <div class="mb-3">
270
+ <label for="date" class="form-label">Date</label>
271
+ <input type="date" class="form-control" id="date" readonly>
272
+ </div>
273
+ <div class="mb-3">
274
+ <label for="project" class="form-label">Type d'ouvrage</label>
275
+ <input type="text" class="form-control" id="project" required>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </div>
281
+
282
+ <!-- Items Sections -->
283
+ <div class="card shadow-sm mb-4">
284
+ <div class="card-body">
285
+ <!-- Poutrelles Section -->
286
+ <div class="section-title">
287
+ <h4>POUTRELLES</h4>
288
+ <button type="button" class="btn btn-light" id="addPoutrelles">
289
+ <i class="fas fa-plus"></i> Ajouter Poutrelle
290
+ </button>
291
+ </div>
292
+ <div class="table-responsive">
293
+ <table class="table table-bordered table-hover">
294
+ <thead class="table-primary">
295
+ <tr>
296
+ <th style="width: 30%">Description</th>
297
+ <th style="width: 10%">Unité</th>
298
+ <th style="width: 10%">Quantité</th>
299
+ <th style="width: 15%">Longueur</th>
300
+ <th style="width: 15%">P.U</th>
301
+ <th style="width: 15%">Total HT</th>
302
+ <th style="width: 5%"></th>
303
+ </tr>
304
+ </thead>
305
+ <tbody id="poutrellesTable"></tbody>
306
+ </table>
307
+ </div>
308
+
309
+ <!-- Hourdis Section -->
310
+ <div class="section-title mt-4">
311
+ <h4>HOURDIS</h4>
312
+ <button type="button" class="btn btn-light" id="addHourdis">
313
+ <i class="fas fa-plus"></i> Ajouter Hourdis
314
+ </button>
315
+ </div>
316
+ <div class="table-responsive">
317
+ <table class="table table-bordered table-hover">
318
+ <thead class="table-primary">
319
+ <tr>
320
+ <th style="width: 30%">Description</th>
321
+ <th style="width: 10%">Unité</th>
322
+ <th style="width: 10%">Quantité</th>
323
+ <th style="width: 15%">Longueur</th>
324
+ <th style="width: 15%">P.U</th>
325
+ <th style="width: 15%">Total HT</th>
326
+ <th style="width: 5%"></th>
327
+ </tr>
328
+ </thead>
329
+ <tbody id="hourdisTable"></tbody>
330
+ </table>
331
+ </div>
332
+
333
+ <!-- Panneau Section -->
334
+ <div class="section-title mt-4">
335
+ <h4>PANNEAU TREILLIS SOUDES</h4>
336
+ <button type="button" class="btn btn-light" id="addPanneau">
337
+ <i class="fas fa-plus"></i> Ajouter Panneau
338
+ </button>
339
+ </div>
340
+ <div class="table-responsive">
341
+ <table class="table table-bordered table-hover">
342
+ <thead class="table-primary">
343
+ <tr>
344
+ <th style="width: 30%">Description</th>
345
+ <th style="width: 10%">Unité</th>
346
+ <th style="width: 10%">Quantité</th>
347
+ <th style="width: 15%">Longueur</th>
348
+ <th style="width: 15%">P.U</th>
349
+ <th style="width: 15%">Total HT</th>
350
+ <th style="width: 5%"></th>
351
+ </tr>
352
+ </thead>
353
+ <tbody id="panneauTable"></tbody>
354
+ </table>
355
+ </div>
356
+
357
+ <!-- Agglos Section -->
358
+ <div class="section-title mt-4">
359
+ <h4>AGGLOS</h4>
360
+ <button type="button" class="btn btn-light" id="addAgglos">
361
+ <i class="fas fa-plus"></i> Ajouter Agglos
362
+ </button>
363
+ </div>
364
+ <div class="table-responsive">
365
+ <table class="table table-bordered table-hover">
366
+ <thead class="table-primary">
367
+ <tr>
368
+ <th style="width: 30%">Description</th>
369
+ <th style="width: 10%">Unité</th>
370
+ <th style="width: 10%">Quantité</th>
371
+ <th style="width: 15%">Longueur</th>
372
+ <th style="width: 15%">P.U</th>
373
+ <th style="width: 15%">Total HT</th>
374
+ <th style="width: 5%"></th>
375
+ </tr>
376
+ </thead>
377
+ <tbody id="agglosTable"></tbody>
378
+ </table>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <!-- Totals Section -->
384
+ <div class="row">
385
+ <div class="col-md-6">
386
+ <!-- Empty for spacing -->
387
+ </div>
388
+ <div class="col-md-6">
389
+ <div class="card shadow-sm">
390
+ <div class="card-header">
391
+ <h5 class="mb-0">Totaux</h5>
392
+ </div>
393
+ <div class="card-body">
394
+ <div class="mb-3">
395
+ <label for="totalHT" class="form-label">Total HT</label>
396
+ <input type="number" class="form-control" id="totalHT" readonly>
397
+ </div>
398
+ <div class="mb-3">
399
+ <label for="tax" class="form-label">TVA 20%</label>
400
+ <input type="number" class="form-control" id="tax" readonly>
401
+ </div>
402
+ <div class="mb-3">
403
+ <label for="totalTTC" class="form-label">Total TTC</label>
404
+ <input type="number" class="form-control" id="totalTTC" readonly>
405
+ </div>
406
+ </div>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <!-- Submit Button -->
412
+ <div class="d-grid gap-2 col-md-6 mx-auto mt-4">
413
+ <button type="submit" class="btn btn-success btn-lg">
414
+ <i class="fas fa-file-pdf"></i> Générer le Devis
415
+ </button>
416
+ </div>
417
+ </form>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </div>
423
+
424
+ <!-- Add Font Awesome for icons -->
425
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
426
+
427
+ <!-- Poutrelles Modal -->
428
+ <div class="modal fade" id="poutrellesModal" tabindex="-1">
429
+ <div class="modal-dialog modal-lg">
430
+ <div class="modal-content">
431
+ <div class="modal-header">
432
+ <h5 class="modal-title">Sélectionner Poutrelle</h5>
433
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
434
+ </div>
435
+ <div class="modal-body">
436
+ <div class="table-responsive">
437
+ <table class="table">
438
+ <thead>
439
+ <tr>
440
+ <th>Description</th>
441
+ <th>Unité</th>
442
+ <th>Quantité</th>
443
+ <th>Longueur</th>
444
+ <th>P.U</th>
445
+ <th>Action</th>
446
+ </tr>
447
+ </thead>
448
+ <tbody></tbody>
449
+ </table>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </div>
455
+
456
+ <!-- Hourdis Modal -->
457
+ <div class="modal fade" id="hourdisModal" tabindex="-1">
458
+ <div class="modal-dialog modal-lg">
459
+ <div class="modal-content">
460
+ <div class="modal-header">
461
+ <h5 class="modal-title">Sélectionner Hourdis</h5>
462
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
463
+ </div>
464
+ <div class="modal-body">
465
+ <div class="table-responsive">
466
+ <table class="table">
467
+ <thead>
468
+ <tr>
469
+ <th>Description</th>
470
+ <th>Unité</th>
471
+ <th>Quantité</th>
472
+ <th>Longueur</th>
473
+ <th>P.U</th>
474
+ <th>Action</th>
475
+ </tr>
476
+ </thead>
477
+ <tbody></tbody>
478
+ </table>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
485
+ <!-- Panneau Modal -->
486
+ <div class="modal fade" id="panneauModal" tabindex="-1">
487
+ <div class="modal-dialog modal-lg">
488
+ <div class="modal-content">
489
+ <div class="modal-header">
490
+ <h5 class="modal-title">Sélectionner Panneau</h5>
491
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
492
+ </div>
493
+ <div class="modal-body">
494
+ <div class="table-responsive">
495
+ <table class="table">
496
+ <thead>
497
+ <tr>
498
+ <th>Description</th>
499
+ <th>Unité</th>
500
+ <th>Quantité</th>
501
+ <th>Longueur</th>
502
+ <th>P.U</th>
503
+ <th>Action</th>
504
+ </tr>
505
+ </thead>
506
+ <tbody></tbody>
507
+ </table>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ </div>
513
+
514
+ <!-- Add Agglos Modal -->
515
+ <div class="modal fade" id="agglosModal" tabindex="-1">
516
+ <div class="modal-dialog modal-lg">
517
+ <div class="modal-content">
518
+ <div class="modal-header">
519
+ <h5 class="modal-title">Sélectionner Agglos</h5>
520
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
521
+ </div>
522
+ <div class="modal-body">
523
+ <div class="table-responsive">
524
+ <table class="table">
525
+ <thead>
526
+ <tr>
527
+ <th>Description</th>
528
+ <th>Unité</th>
529
+ <th>Quantité</th>
530
+ <th>Longueur</th>
531
+ <th>P.U</th>
532
+ <th>Action</th>
533
+ </tr>
534
+ </thead>
535
+ <tbody></tbody>
536
+ </table>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ <!-- Bootstrap JS -->
544
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
545
+
546
+ <script>
547
+ document.addEventListener("DOMContentLoaded", function() {
548
+ const poutrellesTable = document.getElementById("poutrellesTable");
549
+ const hourdisTable = document.getElementById("hourdisTable");
550
+ const panneauTable = document.getElementById("panneauTable");
551
+ const agglosTable = document.getElementById("agglosTable");
552
+ const invoiceForm = document.getElementById("invoiceForm");
553
+ const invoiceNumberInput = document.getElementById("invoiceNumber");
554
+
555
+ // Default items data
556
+ const defaultItems = {
557
+ poutrelles: [
558
+ { description: "PCP 114N", unit: "ML", quantity: 21, length: 3.00, unit_price: 26.00 },
559
+ { description: "PCP 113N", unit: "ML", quantity: 12, length: 4.00, unit_price: 26.00 }
560
+ ],
561
+ hourdis: [
562
+ { description: "HOURDIS TYPE 12", unit: "U", quantity: 1, length: 300, unit_price: 3.50 }
563
+ ],
564
+ panneaux: [
565
+ { description: "PTS SISMIQUE 5*3,5", unit: "U", quantity: 1, length: 9, unit_price: 195.00 }
566
+ ]
567
+ };
568
+
569
+ // Function to add a new item row
570
+ function addItemRow(item, tableBody) {
571
+ const row = document.createElement("tr");
572
+ const total = (item.quantity * item.length * item.unit_price).toFixed(2);
573
+
574
+ row.innerHTML = `
575
+ <td><input type="text" class="form-control" name="description" value="${item.description}" readonly></td>
576
+ <td><input type="text" class="form-control" name="unit" value="${item.unit}" readonly></td>
577
+ <td><input type="number" class="form-control" name="quantity" value="${item.quantity}" required></td>
578
+ <td><input type="number" class="form-control" name="length" value="${item.length}" step="0.01" required></td>
579
+ <td><input type="number" class="form-control" name="unitPrice" value="${item.unit_price}" step="0.01" required></td>
580
+ <td><input type="number" class="form-control" name="totalHT" value="${total}" readonly></td>
581
+ <td><button type="button" class="btn btn-danger btn-sm remove-item">×</button></td>
582
+ `;
583
+
584
+ row.querySelector(".remove-item").addEventListener("click", () => {
585
+ row.remove();
586
+ updateTotals();
587
+ });
588
+
589
+ // Add input event listeners to update total
590
+ ['quantity', 'length', 'unitPrice'].forEach(field => {
591
+ row.querySelector(`input[name='${field}']`).addEventListener('input', () => {
592
+ const qty = parseFloat(row.querySelector('input[name="quantity"]').value) || 0;
593
+ const length = parseFloat(row.querySelector('input[name="length"]').value) || 0;
594
+ const price = parseFloat(row.querySelector('input[name="unitPrice"]').value) || 0;
595
+ const rowTotal = (qty * length * price).toFixed(2);
596
+ row.querySelector('input[name="totalHT"]').value = rowTotal;
597
+ updateTotals();
598
+ });
599
+ });
600
+
601
+ tableBody.appendChild(row);
602
+ updateTotals();
603
+ }
604
+
605
+ // Function to update totals
606
+ function updateTotals() {
607
+ const totalHT = Array.from(document.querySelectorAll('input[name="totalHT"]'))
608
+ .reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0);
609
+
610
+ const formattedTotalHT = totalHT.toFixed(2);
611
+ const tax = (totalHT * 0.20).toFixed(2);
612
+ const totalTTC = (totalHT * 1.20).toFixed(2);
613
+
614
+ document.querySelector("#totalHT").value = formattedTotalHT;
615
+ document.querySelector("#tax").value = tax;
616
+ document.querySelector("#totalTTC").value = totalTTC;
617
+ }
618
+
619
+ // Initialize tables with default items
620
+ defaultItems.poutrelles.forEach(item => addItemRow(item, poutrellesTable));
621
+ defaultItems.hourdis.forEach(item => addItemRow(item, hourdisTable));
622
+ defaultItems.panneaux.forEach(item => addItemRow(item, panneauTable));
623
+
624
+ // Update initial totals
625
+ updateTotals();
626
+
627
+ // Predefined items data
628
+ const predefinedItems = {
629
+ poutrelles: [
630
+ { description: "PCP 114N", unit: "ML", quantity: 21, length: 3.00, unit_price: 26.00 },
631
+ { description: "PCP 113N", unit: "ML", quantity: 12, length: 4.00, unit_price: 26.00 },
632
+ { description: "PCP 113B SISMIQUE", unit: "ML", quantity: 22, length: 4.20, unit_price: 26.00 },
633
+ { description: "PCP 114B SISMIQUE", unit: "ML", quantity: 12, length: 4.90, unit_price: 30.00 },
634
+ { description: "PCP 135 SISMIQUE", unit: "ML", quantity: 8, length: 5.00, unit_price: 38.00 },
635
+ { description: "PCP 156 SISMIQUE", unit: "ML", quantity: 12, length: 5.10, unit_price: 38.00 },
636
+ { description: "PCP 158 SISMIQUE", unit: "ML", quantity: 12, length: 5.50, unit_price: 51.00 }
637
+ ],
638
+ hourdis: [
639
+ { description: "HOURDIS TYPE 08", unit: "U", quantity: 1, length: 200, unit_price: 3.40 },
640
+ { description: "HOURDIS TYPE 12", unit: "U", quantity: 1, length: 300, unit_price: 3.50 },
641
+ { description: "HOURDIS TYPE 16", unit: "U", quantity: 1, length: 0, unit_price: 0 },
642
+ { description: "HOURDIS TYPE 20", unit: "U", quantity: 1, length: 0, unit_price: 0 },
643
+ { description: "HOURDIS TYPE 25", unit: "U", quantity: 1, length: 450, unit_price: 4.80 },
644
+ { description: "HOURDIS TYPE 30", unit: "U", quantity: 1, length: 0, unit_price: 0 }
645
+ ],
646
+ panneaux: [
647
+ { description: "PTS Normal 3,5*3,5", unit: "U", quantity: 1, length: 0, unit_price: 0 },
648
+ { description: "PTS SISMIQUE 5*3,5", unit: "U", quantity: 1, length: 9, unit_price: 195.00 }
649
+ ],
650
+ agglos: [
651
+ { description: "AGGLOS 07", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
652
+ { description: "AGGLOS 10", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
653
+ { description: "AGGLOS 15", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
654
+ { description: "AGGLOS 20", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
655
+ { description: "AGGLOS 25", unit: "U", quantity: 1, length: 9, unit_price: 190.00 },
656
+ { description: "AGGLOS A BRANCHER 20", unit: "U", quantity: 1, length: 9, unit_price: 190.00 }
657
+ ]
658
+ };
659
+
660
+ // Function to populate modal with items
661
+ function populateModal(modalId, items, targetTable) {
662
+ const modal = document.querySelector(modalId);
663
+ const tbody = modal.querySelector('tbody');
664
+ tbody.innerHTML = '';
665
+
666
+ items.forEach(item => {
667
+ const row = document.createElement('tr');
668
+ row.innerHTML = `
669
+ <td>${item.description}</td>
670
+ <td>${item.unit}</td>
671
+ <td><input type="number" class="form-control form-control-sm" value="${item.quantity}" name="modal-quantity"></td>
672
+ <td><input type="number" class="form-control form-control-sm" value="${item.length}" step="0.01" name="modal-length"></td>
673
+ <td><input type="number" class="form-control form-control-sm" value="${item.unit_price}" step="0.01" name="modal-price"></td>
674
+ <td>
675
+ <button class="btn btn-primary btn-sm add-item">
676
+ Ajouter
677
+ </button>
678
+ </td>
679
+ `;
680
+
681
+ row.querySelector('.add-item').addEventListener('click', () => {
682
+ const newItem = {
683
+ ...item,
684
+ quantity: parseFloat(row.querySelector('[name="modal-quantity"]').value) || 0,
685
+ length: parseFloat(row.querySelector('[name="modal-length"]').value) || 0,
686
+ unit_price: parseFloat(row.querySelector('[name="modal-price"]').value) || 0
687
+ };
688
+ addItemRow(newItem, targetTable);
689
+ bootstrap.Modal.getInstance(modal).hide();
690
+ });
691
+
692
+ tbody.appendChild(row);
693
+ });
694
+ }
695
+
696
+ // Update button click handlers
697
+ document.querySelector("#addPoutrelles").addEventListener("click", () => {
698
+ populateModal('#poutrellesModal', predefinedItems.poutrelles, poutrellesTable);
699
+ new bootstrap.Modal('#poutrellesModal').show();
700
+ });
701
+
702
+ document.querySelector("#addHourdis").addEventListener("click", () => {
703
+ populateModal('#hourdisModal', predefinedItems.hourdis, hourdisTable);
704
+ new bootstrap.Modal('#hourdisModal').show();
705
+ });
706
+
707
+ document.querySelector("#addPanneau").addEventListener("click", () => {
708
+ populateModal('#panneauModal', predefinedItems.panneaux, panneauTable);
709
+ new bootstrap.Modal('#panneauModal').show();
710
+ });
711
+
712
+ document.querySelector("#addAgglos").addEventListener("click", () => {
713
+ populateModal('#agglosModal', predefinedItems.agglos, agglosTable);
714
+ new bootstrap.Modal('#agglosModal').show();
715
+ });
716
+
717
+ // Form submission
718
+ invoiceForm.addEventListener("submit", async (event) => {
719
+ event.preventDefault();
720
+
721
+ const getAllItems = () => {
722
+ const items = [];
723
+ [poutrellesTable, hourdisTable, panneauTable, agglosTable].forEach(table => {
724
+ items.push(...Array.from(table.querySelectorAll("tr")).map(row => ({
725
+ description: row.querySelector("input[name='description']").value,
726
+ unit: row.querySelector("input[name='unit']").value,
727
+ quantity: parseInt(row.querySelector("input[name='quantity']").value, 10),
728
+ length: parseFloat(row.querySelector("input[name='length']").value),
729
+ unit_price: parseFloat(row.querySelector("input[name='unitPrice']").value),
730
+ total_price: parseFloat(row.querySelector("input[name='totalHT']").value)
731
+ })));
732
+ });
733
+ return items;
734
+ };
735
+
736
+ const data = {
737
+ invoice_number: document.querySelector("#invoiceNumber").value,
738
+ date: new Date(document.querySelector("#date").value).toISOString(),
739
+ project: document.querySelector("#project").value,
740
+ client_name: document.querySelector("#clientName").value,
741
+ client_phone: document.querySelector("#clientPhone").value,
742
+ address: document.querySelector("#clientAddress").value,
743
+ total_ht: parseFloat(document.querySelector("#totalHT").value || 0),
744
+ tax: parseFloat(document.querySelector("#tax").value || 0),
745
+ total_ttc: parseFloat(document.querySelector("#totalTTC").value || 0),
746
+ items: getAllItems(),
747
+ frame_number: "",
748
+ status: "pending",
749
+ created_at: new Date().toISOString(),
750
+ commercial: document.querySelector("#commercial").value || "divers"
751
+ };
752
+
753
+ console.log("Sending invoice data:", data);
754
+
755
+ try {
756
+ // First create the invoice
757
+ const createResponse = await fetch("/api/invoices/", {
758
+ method: "POST",
759
+ headers: { "Content-Type": "application/json" },
760
+ body: JSON.stringify(data)
761
+ });
762
+
763
+ if (!createResponse.ok) {
764
+ const errorData = await createResponse.json();
765
+ console.error("Server error:", errorData);
766
+ throw new Error(`Failed to create invoice: ${JSON.stringify(errorData)}`);
767
+ }
768
+
769
+ const invoice = await createResponse.json();
770
+ console.log("Created invoice:", invoice);
771
+
772
+ // Then generate the PDF
773
+ const pdfResponse = await fetch(`/api/invoices/${invoice.id}/generate-pdf`, {
774
+ method: "POST",
775
+ headers: {
776
+ "Content-Type": "application/json"
777
+ },
778
+ body: JSON.stringify(invoice)
779
+ });
780
+
781
+ if (!pdfResponse.ok) {
782
+ const errorData = await pdfResponse.text();
783
+ console.error("PDF generation error:", errorData);
784
+ throw new Error(`Failed to generate PDF: ${errorData}`);
785
+ }
786
+
787
+ // Handle PDF download
788
+ const blob = await pdfResponse.blob();
789
+ const url = window.URL.createObjectURL(blob);
790
+ const a = document.createElement("a");
791
+ a.href = url;
792
+ a.download = `devis_${invoice.invoice_number}.pdf`;
793
+ document.body.appendChild(a);
794
+ a.click();
795
+ document.body.removeChild(a);
796
+ window.URL.revokeObjectURL(url);
797
+
798
+ } catch (error) {
799
+ console.error("Error:", error);
800
+ alert(`Error: ${error.message}`);
801
+ }
802
+
803
+ // After successful submission, update the invoice number for the current type
804
+ const selectedType = document.querySelector('input[name="clientType"]:checked')?.value;
805
+ if (selectedType) {
806
+ await updateInvoiceNumber(selectedType);
807
+ }
808
+ });
809
+
810
+ // Set today's date automatically
811
+ const today = new Date();
812
+ const formattedDate = today.toISOString().split('T')[0];
813
+ document.querySelector("#date").value = formattedDate;
814
+
815
+ // Function to get and update the invoice number
816
+ async function updateInvoiceNumber(selectedType) {
817
+ try {
818
+ // Disable the input while fetching
819
+ invoiceNumberInput.disabled = true;
820
+
821
+ const response = await fetch(`/api/invoices/last-number/${selectedType}`);
822
+
823
+ if (response.ok) {
824
+ const data = await response.json();
825
+ invoiceNumberInput.value = data.formatted_number;
826
+ } else {
827
+ throw new Error('Failed to get invoice number');
828
+ }
829
+ } catch (error) {
830
+ console.error('Error:', error);
831
+ alert('Error getting invoice number');
832
+ } finally {
833
+ // Re-enable the input
834
+ invoiceNumberInput.disabled = false;
835
+ }
836
+ }
837
+
838
+ // Update invoice number when client type is selected
839
+ document.querySelectorAll('input[name="clientType"]').forEach(input => {
840
+ input.addEventListener('change', () => {
841
+ updateInvoiceNumber(input.value);
842
+ });
843
+ });
844
+
845
+ // Update the number when the page loads if a type is already selected
846
+ const selectedType = document.querySelector('input[name="clientType"]:checked')?.value;
847
+ if (selectedType) {
848
+ updateInvoiceNumber(selectedType);
849
+ }
850
+ });
851
+ </script>
852
+ </body>
853
+ </html>
app/templates/index.html CHANGED
@@ -819,6 +819,7 @@
819
  invoiceNumberInput.disabled = true;
820
 
821
  const response = await fetch(`/api/invoices/last-number/${selectedType}`);
 
822
  if (response.ok) {
823
  const data = await response.json();
824
  invoiceNumberInput.value = data.formatted_number;
 
819
  invoiceNumberInput.disabled = true;
820
 
821
  const response = await fetch(`/api/invoices/last-number/${selectedType}`);
822
+
823
  if (response.ok) {
824
  const data = await response.json();
825
  invoiceNumberInput.value = data.formatted_number;
sql_app.db CHANGED
Binary files a/sql_app.db and b/sql_app.db differ