Antoni09 commited on
Commit
e12f538
·
verified ·
1 Parent(s): cb44d3c

main/static

Browse files
Files changed (3) hide show
  1. index.html +209 -0
  2. main.js +984 -0
  3. styles.css +365 -0
index.html ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Generator faktur</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ </head>
9
+ <body>
10
+ <main class="container">
11
+ <h1>Generator faktur</h1>
12
+
13
+ <section id="setup-section" class="panel hidden">
14
+ <h2>Konfiguracja danych firmy</h2>
15
+ <form id="setup-form" class="form">
16
+ <div class="field-grid">
17
+ <label>
18
+ Nazwa firmy
19
+ <input type="text" name="company_name" required>
20
+ </label>
21
+ <label>
22
+ Imie i nazwisko wlasciciela
23
+ <input type="text" name="owner_name" required>
24
+ </label>
25
+ <label>
26
+ Ulica i numer
27
+ <input type="text" name="address_line" required>
28
+ </label>
29
+ <label>
30
+ Kod pocztowy
31
+ <input type="text" name="postal_code" required>
32
+ </label>
33
+ <label>
34
+ Miejscowosc
35
+ <input type="text" name="city" required>
36
+ </label>
37
+ <label>
38
+ NIP
39
+ <input type="text" name="tax_id" required>
40
+ </label>
41
+ <label>
42
+ Numer konta bankowego
43
+ <input type="text" name="bank_account" required>
44
+ </label>
45
+ <label>
46
+ Haslo
47
+ <input type="password" name="password" required>
48
+ </label>
49
+ <label>
50
+ Powtorz haslo
51
+ <input type="password" name="confirm_password" required>
52
+ </label>
53
+ </div>
54
+ <button type="submit">Zapisz dane</button>
55
+ <p class="hint">Dane przechowywane sa na serwerze lokalnym.</p>
56
+ </form>
57
+ <p id="setup-feedback" class="feedback"></p>
58
+ </section>
59
+
60
+ <section id="login-section" class="panel hidden">
61
+ <h2>Logowanie</h2>
62
+ <form id="login-form" class="form">
63
+ <label>
64
+ Haslo
65
+ <input type="password" name="password" required>
66
+ </label>
67
+ <button type="submit">Zaloguj</button>
68
+ </form>
69
+ <p id="login-feedback" class="feedback"></p>
70
+ </section>
71
+
72
+ <section id="app-section" class="panel hidden">
73
+ <header class="invoice-header">
74
+ <h2>Panel faktur</h2>
75
+ <button id="logout-button" type="button" class="link-button">Wyloguj</button>
76
+ </header>
77
+
78
+ <section class="business-section">
79
+ <div class="business-section-header">
80
+ <h3>Dane sprzedawcy</h3>
81
+ <button id="toggle-business-form" type="button" class="link-button">Edytuj dane</button>
82
+ </div>
83
+ <div id="business-display" class="business-display"></div>
84
+ <form id="business-form" class="form hidden">
85
+ <div class="field-grid">
86
+ <label>
87
+ Nazwa firmy
88
+ <input type="text" name="company_name" required>
89
+ </label>
90
+ <label>
91
+ Imie i nazwisko wlasciciela
92
+ <input type="text" name="owner_name" required>
93
+ </label>
94
+ <label>
95
+ Ulica i numer
96
+ <input type="text" name="address_line" required>
97
+ </label>
98
+ <label>
99
+ Kod pocztowy
100
+ <input type="text" name="postal_code" required>
101
+ </label>
102
+ <label>
103
+ Miejscowosc
104
+ <input type="text" name="city" required>
105
+ </label>
106
+ <label>
107
+ NIP
108
+ <input type="text" name="tax_id" required>
109
+ </label>
110
+ <label>
111
+ Numer konta bankowego
112
+ <input type="text" name="bank_account" required>
113
+ </label>
114
+ </div>
115
+ <div class="form-actions">
116
+ <button type="submit">Zapisz</button>
117
+ <button id="cancel-business-update" type="button" class="link-button">Anuluj</button>
118
+ </div>
119
+ <p id="business-feedback" class="feedback"></p>
120
+ </form>
121
+ </section>
122
+
123
+ <form id="invoice-form" class="form">
124
+ <fieldset>
125
+ <legend>Informacje o fakturze</legend>
126
+ <label>
127
+ Data sprzedazy / wykonania uslugi
128
+ <input type="date" name="saleDate">
129
+ </label>
130
+ </fieldset>
131
+
132
+ <fieldset>
133
+ <legend>Dane nabywcy</legend>
134
+ <div class="field-grid">
135
+ <label>
136
+ Nazwa / Imie i nazwisko
137
+ <input type="text" name="clientName">
138
+ </label>
139
+ <label>
140
+ NIP
141
+ <input type="text" name="clientTaxId">
142
+ </label>
143
+ <label>
144
+ Ulica i numer
145
+ <input type="text" name="clientAddress">
146
+ </label>
147
+ <label>
148
+ Kod pocztowy
149
+ <input type="text" name="clientPostalCode">
150
+ </label>
151
+ <label>
152
+ Miejscowosc
153
+ <input type="text" name="clientCity">
154
+ </label>
155
+ </div>
156
+ </fieldset>
157
+
158
+ <section class="items-section">
159
+ <header class="items-header">
160
+ <h3>Pozycje faktury</h3>
161
+ <button type="button" id="add-item-button">Dodaj pozycje</button>
162
+ </header>
163
+ <div class="items-table-wrapper">
164
+ <table class="items-table">
165
+ <thead>
166
+ <tr>
167
+ <th>Nazwa towaru/uslugi</th>
168
+ <th>Ilosc</th>
169
+ <th>Cena jedn. brutto (PLN)</th>
170
+ <th>Stawka VAT</th>
171
+ <th>Wartosc brutto (PLN)</th>
172
+ <th></th>
173
+ </tr>
174
+ </thead>
175
+ <tbody id="items-body"></tbody>
176
+ </table>
177
+ </div>
178
+ </section>
179
+
180
+ <div id="totals-container" class="totals">
181
+ <span id="total-net">Suma netto: 0.00 PLN</span>
182
+ <span id="total-vat">Kwota VAT: 0.00 PLN</span>
183
+ <span id="total-gross">Suma brutto: 0.00 PLN</span>
184
+ </div>
185
+
186
+ <section id="rate-summary" class="rate-summary"></section>
187
+
188
+ <div id="exemption-note-wrapper" class="hidden">
189
+ <label>
190
+ Podstawa prawna zwolnienia (stosowana dla pozycji ZW)
191
+ <textarea id="exemption-note" rows="3" placeholder="np. Art. 43 ust. 1 pkt 19 ustawy o VAT"></textarea>
192
+ </label>
193
+ </div>
194
+
195
+ <button type="submit">Generuj fakture</button>
196
+ </form>
197
+
198
+ <section id="invoice-result" class="panel hidden">
199
+ <h3>Podglad faktury</h3>
200
+ <div id="invoice-output" class="invoice-preview"></div>
201
+ <button id="download-button" type="button">Pobierz jako plik PDF</button>
202
+ </section>
203
+ </section>
204
+ </main>
205
+
206
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js" defer></script>
207
+ <script src="main.js" defer></script>
208
+ </body>
209
+ </html>
main.js ADDED
@@ -0,0 +1,984 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const VAT_OPTIONS = [
2
+ { value: "23", label: "23%" },
3
+ { value: "8", label: "8%" },
4
+ { value: "5", label: "5%" },
5
+ { value: "0", label: "0% (ZW)" },
6
+ { value: "ZW", label: "ZW - zwolnione" },
7
+ { value: "NP", label: "NP - poza zakresem" },
8
+ ];
9
+
10
+ const VAT_RATE_VALUES = {
11
+ "23": 0.23,
12
+ "8": 0.08,
13
+ "5": 0.05,
14
+ "0": 0,
15
+ ZW: 0,
16
+ NP: 0,
17
+ };
18
+
19
+ const setupSection = document.getElementById("setup-section");
20
+ const loginSection = document.getElementById("login-section");
21
+ const appSection = document.getElementById("app-section");
22
+
23
+ const setupForm = document.getElementById("setup-form");
24
+ const loginForm = document.getElementById("login-form");
25
+ const invoiceForm = document.getElementById("invoice-form");
26
+ const businessForm = document.getElementById("business-form");
27
+
28
+ const setupFeedback = document.getElementById("setup-feedback");
29
+ const loginFeedback = document.getElementById("login-feedback");
30
+ const businessFeedback = document.getElementById("business-feedback");
31
+
32
+ const businessDisplay = document.getElementById("business-display");
33
+ const toggleBusinessFormButton = document.getElementById("toggle-business-form");
34
+ const cancelBusinessUpdateButton = document.getElementById("cancel-business-update");
35
+
36
+ const itemsBody = document.getElementById("items-body");
37
+ const addItemButton = document.getElementById("add-item-button");
38
+
39
+ const totalNetLabel = document.getElementById("total-net");
40
+ const totalVatLabel = document.getElementById("total-vat");
41
+ const totalGrossLabel = document.getElementById("total-gross");
42
+ const rateSummaryContainer = document.getElementById("rate-summary");
43
+
44
+ const exemptionNoteWrapper = document.getElementById("exemption-note-wrapper");
45
+ const exemptionNoteInput = document.getElementById("exemption-note");
46
+
47
+ const invoiceResult = document.getElementById("invoice-result");
48
+ const invoiceOutput = document.getElementById("invoice-output");
49
+ const downloadButton = document.getElementById("download-button");
50
+ const logoutButton = document.getElementById("logout-button");
51
+
52
+ let authToken = sessionStorage.getItem("invoiceAuthToken") || null;
53
+ let currentBusiness = null;
54
+ let lastInvoice = null;
55
+ let pdfFontPromise = null;
56
+ let pdfFontBase64 = null;
57
+
58
+ function setState(state) {
59
+ setupSection.classList.add("hidden");
60
+ loginSection.classList.add("hidden");
61
+ appSection.classList.add("hidden");
62
+
63
+ if (state === "setup") {
64
+ setupSection.classList.remove("hidden");
65
+ } else if (state === "login") {
66
+ loginSection.classList.remove("hidden");
67
+ } else if (state === "app") {
68
+ appSection.classList.remove("hidden");
69
+ }
70
+ }
71
+
72
+ function clearFeedback(element) {
73
+ element.textContent = "";
74
+ element.classList.remove("error", "success");
75
+ }
76
+
77
+ function showFeedback(element, message, type = "error") {
78
+ element.textContent = message;
79
+ element.classList.remove("error", "success");
80
+ if (type) {
81
+ element.classList.add(type);
82
+ }
83
+ }
84
+
85
+ function parseNumber(value) {
86
+ if (typeof value === "number") {
87
+ return Number.isFinite(value) ? value : 0;
88
+ }
89
+ if (!value) {
90
+ return 0;
91
+ }
92
+ const normalized = value.toString().replace(",", ".");
93
+ const parsed = Number.parseFloat(normalized);
94
+ return Number.isFinite(parsed) ? parsed : 0;
95
+ }
96
+
97
+ function formatCurrency(value) {
98
+ const number = parseNumber(value);
99
+ return `${number.toFixed(2)} PLN`;
100
+ }
101
+
102
+ function vatLabelFromCode(code) {
103
+ if (code === "ZW" || code === "0") {
104
+ return "ZW";
105
+ }
106
+ if (code === "NP") {
107
+ return "NP";
108
+ }
109
+ return `${code}%`;
110
+ }
111
+
112
+ function requiresExemption(code) {
113
+ return code === "ZW" || code === "0";
114
+ }
115
+
116
+ async function apiRequest(path, { method = "GET", body, headers = {} } = {}, requireAuth = false) {
117
+ const options = {
118
+ method,
119
+ headers: {
120
+ "Content-Type": "application/json",
121
+ ...headers,
122
+ },
123
+ };
124
+
125
+ if (body !== undefined) {
126
+ options.body = JSON.stringify(body);
127
+ }
128
+
129
+ if (requireAuth) {
130
+ if (!authToken) {
131
+ throw new Error("Brak tokenu autoryzacyjnego.");
132
+ }
133
+ options.headers.Authorization = `Bearer ${authToken}`;
134
+ }
135
+
136
+ const response = await fetch(path, options);
137
+ const isJson = response.headers.get("content-type")?.includes("application/json");
138
+ const data = isJson ? await response.json() : {};
139
+
140
+ if (response.status === 401) {
141
+ authToken = null;
142
+ sessionStorage.removeItem("invoiceAuthToken");
143
+ setState("login");
144
+ throw new Error(data.error || "Sesja wygasla. Zaloguj sie ponownie.");
145
+ }
146
+
147
+ if (!response.ok) {
148
+ throw new Error(data.error || "Wystapil blad podczas komunikacji z serwerem.");
149
+ }
150
+
151
+ return data;
152
+ }
153
+
154
+ function renderBusinessDisplay(business) {
155
+ if (!business) {
156
+ businessDisplay.textContent = "Brak zapisanych danych firmy.";
157
+ return;
158
+ }
159
+
160
+ businessDisplay.innerHTML = `
161
+ <p><strong>${business.company_name}</strong></p>
162
+ <p>${business.owner_name}</p>
163
+ <p>${business.address_line}</p>
164
+ <p>${business.postal_code} ${business.city}</p>
165
+ <p>NIP: ${business.tax_id}</p>
166
+ <p>Konto: ${business.bank_account}</p>
167
+ `;
168
+ }
169
+
170
+ function fillBusinessForm(business) {
171
+ if (!business) {
172
+ return;
173
+ }
174
+ businessForm.elements.company_name.value = business.company_name || "";
175
+ businessForm.elements.owner_name.value = business.owner_name || "";
176
+ businessForm.elements.address_line.value = business.address_line || "";
177
+ businessForm.elements.postal_code.value = business.postal_code || "";
178
+ businessForm.elements.city.value = business.city || "";
179
+ businessForm.elements.tax_id.value = business.tax_id || "";
180
+ businessForm.elements.bank_account.value = business.bank_account || "";
181
+ }
182
+
183
+ function vatSelectElement(initialValue = "23") {
184
+ const select = document.createElement("select");
185
+ select.className = "item-vat";
186
+ VAT_OPTIONS.forEach((option) => {
187
+ const element = document.createElement("option");
188
+ element.value = option.value;
189
+ element.textContent = option.label;
190
+ select.appendChild(element);
191
+ });
192
+ select.value = VAT_OPTIONS.some((option) => option.value === initialValue) ? initialValue : "23";
193
+ return select;
194
+ }
195
+
196
+ function createItemRow(initialValues = {}) {
197
+ const row = document.createElement("tr");
198
+
199
+ const nameCell = document.createElement("td");
200
+ const nameInput = document.createElement("input");
201
+ nameInput.type = "text";
202
+ nameInput.className = "item-name";
203
+ nameInput.placeholder = "Nazwa towaru lub uslugi";
204
+ if (initialValues.name) {
205
+ nameInput.value = initialValues.name;
206
+ }
207
+ nameCell.appendChild(nameInput);
208
+
209
+ const quantityCell = document.createElement("td");
210
+ const quantityInput = document.createElement("input");
211
+ quantityInput.type = "number";
212
+ quantityInput.className = "item-quantity";
213
+ quantityInput.min = "0.01";
214
+ quantityInput.step = "0.01";
215
+ quantityInput.value = initialValues.quantity ?? "1";
216
+ quantityCell.appendChild(quantityInput);
217
+
218
+ const unitGrossCell = document.createElement("td");
219
+ const unitGrossInput = document.createElement("input");
220
+ unitGrossInput.type = "number";
221
+ unitGrossInput.className = "item-gross";
222
+ unitGrossInput.min = "0.01";
223
+ unitGrossInput.step = "0.01";
224
+ unitGrossInput.placeholder = "Brutto";
225
+ if (initialValues.unit_price_gross) {
226
+ unitGrossInput.value = initialValues.unit_price_gross;
227
+ }
228
+ unitGrossCell.appendChild(unitGrossInput);
229
+
230
+ const vatCell = document.createElement("td");
231
+ const vatSelect = vatSelectElement(initialValues.vat_code);
232
+ vatCell.appendChild(vatSelect);
233
+
234
+ const totalCell = document.createElement("td");
235
+ totalCell.className = "item-total";
236
+ totalCell.textContent = "0.00 PLN";
237
+
238
+ const actionsCell = document.createElement("td");
239
+ const removeButton = document.createElement("button");
240
+ removeButton.type = "button";
241
+ removeButton.className = "remove-item";
242
+ removeButton.textContent = "Usun";
243
+ actionsCell.appendChild(removeButton);
244
+
245
+ row.appendChild(nameCell);
246
+ row.appendChild(quantityCell);
247
+ row.appendChild(unitGrossCell);
248
+ row.appendChild(vatCell);
249
+ row.appendChild(totalCell);
250
+ row.appendChild(actionsCell);
251
+
252
+ const handleChange = () => updateTotals();
253
+ nameInput.addEventListener("input", handleChange);
254
+ quantityInput.addEventListener("input", handleChange);
255
+ unitGrossInput.addEventListener("input", handleChange);
256
+ vatSelect.addEventListener("change", handleChange);
257
+
258
+ removeButton.addEventListener("click", () => {
259
+ if (itemsBody.children.length === 1) {
260
+ nameInput.value = "";
261
+ quantityInput.value = "1";
262
+ unitGrossInput.value = "";
263
+ vatSelect.value = "23";
264
+ updateTotals();
265
+ return;
266
+ }
267
+ row.remove();
268
+ updateTotals();
269
+ });
270
+
271
+ itemsBody.appendChild(row);
272
+ updateTotals();
273
+ }
274
+
275
+ function calculateRowTotals(row) {
276
+ const name = row.querySelector(".item-name")?.value.trim() ?? "";
277
+ const quantity = parseNumber(row.querySelector(".item-quantity")?.value);
278
+ const unitGross = parseNumber(row.querySelector(".item-gross")?.value);
279
+ const vatCode = row.querySelector(".item-vat")?.value ?? "23";
280
+ const rate = VAT_RATE_VALUES[vatCode] ?? 0;
281
+
282
+ const hasValues = name || quantity > 0 || unitGross > 0;
283
+ if (!hasValues) {
284
+ return {
285
+ valid: false,
286
+ vatCode,
287
+ vatLabel: vatLabelFromCode(vatCode),
288
+ requiresExemption: requiresExemption(vatCode),
289
+ quantity,
290
+ unitGross,
291
+ unitNet: 0,
292
+ netTotal: 0,
293
+ vatAmount: 0,
294
+ grossTotal: 0,
295
+ };
296
+ }
297
+
298
+ if (quantity <= 0 || unitGross <= 0) {
299
+ return {
300
+ valid: false,
301
+ vatCode,
302
+ vatLabel: vatLabelFromCode(vatCode),
303
+ requiresExemption: requiresExemption(vatCode),
304
+ quantity,
305
+ unitGross,
306
+ unitNet: 0,
307
+ netTotal: 0,
308
+ vatAmount: 0,
309
+ grossTotal: quantity * unitGross,
310
+ };
311
+ }
312
+
313
+ const grossTotal = quantity * unitGross;
314
+ const netTotal = rate > 0 ? grossTotal / (1 + rate) : grossTotal;
315
+ const vatAmount = grossTotal - netTotal;
316
+ const unitNet = netTotal / quantity;
317
+
318
+ return {
319
+ valid: true,
320
+ vatCode,
321
+ vatLabel: vatLabelFromCode(vatCode),
322
+ requiresExemption: requiresExemption(vatCode),
323
+ quantity,
324
+ unitGross,
325
+ unitNet,
326
+ netTotal,
327
+ vatAmount,
328
+ grossTotal,
329
+ };
330
+ }
331
+
332
+ function updateTotals() {
333
+ let totalNet = 0;
334
+ let totalVat = 0;
335
+ let totalGross = 0;
336
+ const summary = new Map();
337
+ let exemptionNeeded = false;
338
+
339
+ const rows = Array.from(itemsBody.querySelectorAll("tr"));
340
+ rows.forEach((row) => {
341
+ const totals = calculateRowTotals(row);
342
+ if (totals.requiresExemption) {
343
+ exemptionNeeded = true;
344
+ }
345
+ const totalCell = row.querySelector(".item-total");
346
+ totalCell.textContent = formatCurrency(totals.grossTotal);
347
+
348
+ if (!totals.valid) {
349
+ return;
350
+ }
351
+
352
+ totalNet += totals.netTotal;
353
+ totalVat += totals.vatAmount;
354
+ totalGross += totals.grossTotal;
355
+
356
+ const existing = summary.get(totals.vatLabel) || { net: 0, vat: 0, gross: 0 };
357
+ existing.net += totals.netTotal;
358
+ existing.vat += totals.vatAmount;
359
+ existing.gross += totals.grossTotal;
360
+ summary.set(totals.vatLabel, existing);
361
+ });
362
+
363
+ totalNetLabel.textContent = `Suma netto: ${totalNet.toFixed(2)} PLN`;
364
+ totalVatLabel.textContent = `Kwota VAT: ${totalVat.toFixed(2)} PLN`;
365
+ totalGrossLabel.textContent = `Suma brutto: ${totalGross.toFixed(2)} PLN`;
366
+ renderRateSummary(summary);
367
+
368
+ if (exemptionNeeded) {
369
+ exemptionNoteWrapper.classList.remove("hidden");
370
+ } else {
371
+ exemptionNoteWrapper.classList.add("hidden");
372
+ exemptionNoteInput.value = "";
373
+ }
374
+ }
375
+
376
+ function renderRateSummary(summary) {
377
+ if (!summary || summary.size === 0) {
378
+ rateSummaryContainer.innerHTML = "";
379
+ return;
380
+ }
381
+
382
+ const entries = Array.from(summary.entries()).sort(([a], [b]) => a.localeCompare(b));
383
+ const markup = entries
384
+ .map(
385
+ ([label, totals]) =>
386
+ `<div class="rate-summary-item">
387
+ <span>${label}</span>
388
+ <span>Netto: ${totals.net.toFixed(2)} PLN</span>
389
+ <span>VAT: ${totals.vat.toFixed(2)} PLN</span>
390
+ <span>Brutto: ${totals.gross.toFixed(2)} PLN</span>
391
+ </div>`
392
+ )
393
+ .join("");
394
+ rateSummaryContainer.innerHTML = `<h4>Podsumowanie stawek</h4>${markup}`;
395
+ }
396
+
397
+ function collectInvoicePayload() {
398
+ const items = [];
399
+ const rows = Array.from(itemsBody.querySelectorAll("tr"));
400
+
401
+ rows.forEach((row) => {
402
+ const name = row.querySelector(".item-name")?.value.trim() ?? "";
403
+ const quantity = parseNumber(row.querySelector(".item-quantity")?.value);
404
+ const unitGross = parseNumber(row.querySelector(".item-gross")?.value);
405
+ const vatCode = row.querySelector(".item-vat")?.value ?? "23";
406
+
407
+ const hasValues = name || quantity > 0 || unitGross > 0;
408
+ if (!hasValues) {
409
+ return;
410
+ }
411
+
412
+ if (!name) {
413
+ throw new Error("Kazda pozycja musi miec nazwe.");
414
+ }
415
+ if (quantity <= 0) {
416
+ throw new Error("Ilosc musi byc wieksza od zera.");
417
+ }
418
+ if (unitGross <= 0) {
419
+ throw new Error("Cena brutto musi byc wieksza od zera.");
420
+ }
421
+
422
+ items.push({
423
+ name,
424
+ quantity: quantity.toFixed(2),
425
+ unit_price_gross: unitGross.toFixed(2),
426
+ vat_code: vatCode,
427
+ });
428
+ });
429
+
430
+ if (items.length === 0) {
431
+ throw new Error("Dodaj przynajmniej jedna pozycje.");
432
+ }
433
+
434
+ const saleDate = invoiceForm.elements.saleDate.value || null;
435
+ const exemptionNote = exemptionNoteInput.value.trim();
436
+ const requiresExemptionNote = items.some((item) => item.vat_code === "ZW" || item.vat_code === "0");
437
+ if (requiresExemptionNote && !exemptionNote) {
438
+ throw new Error("Podaj podstawe prawna zwolnienia dla pozycji rozliczanych jako ZW.");
439
+ }
440
+
441
+ const client = {
442
+ name: (invoiceForm.elements.clientName.value || "").trim(),
443
+ tax_id: (invoiceForm.elements.clientTaxId.value || "").trim(),
444
+ address_line: (invoiceForm.elements.clientAddress.value || "").trim(),
445
+ postal_code: (invoiceForm.elements.clientPostalCode.value || "").trim(),
446
+ city: (invoiceForm.elements.clientCity.value || "").trim(),
447
+ };
448
+
449
+ return {
450
+ sale_date: saleDate,
451
+ client,
452
+ items,
453
+ exemption_note: exemptionNote,
454
+ };
455
+ }
456
+
457
+ function renderInvoicePreview(invoice) {
458
+ if (!invoice || !currentBusiness) {
459
+ invoiceOutput.innerHTML = "<p>Brak danych faktury.</p>";
460
+ return;
461
+ }
462
+
463
+ const client = invoice.client || {};
464
+ const hasClientData = client.name || client.address_line || client.postal_code || client.city || client.tax_id;
465
+
466
+ const itemsRows = invoice.items
467
+ .map(
468
+ (item) => `
469
+ <tr>
470
+ <td>${item.name}</td>
471
+ <td>${parseNumber(item.quantity).toFixed(2)}</td>
472
+ <td>${formatCurrency(item.unit_price_net)}</td>
473
+ <td>${formatCurrency(item.net_total)}</td>
474
+ <td>${item.vat_label}</td>
475
+ <td>${formatCurrency(item.vat_amount)}</td>
476
+ <td>${formatCurrency(item.gross_total)}</td>
477
+ </tr>`
478
+ )
479
+ .join("");
480
+
481
+ const summaryRows = (invoice.summary || [])
482
+ .map(
483
+ (entry) =>
484
+ `<div class="rate-summary-item">
485
+ <span>${entry.vat_label}</span>
486
+ <span>Netto: ${formatCurrency(entry.net_total)}</span>
487
+ <span>VAT: ${formatCurrency(entry.vat_total)}</span>
488
+ <span>Brutto: ${formatCurrency(entry.gross_total)}</span>
489
+ </div>`
490
+ )
491
+ .join("");
492
+
493
+ invoiceOutput.innerHTML = `
494
+ <div class="invoice-preview-meta">
495
+ <span><strong>Numer:</strong> ${invoice.invoice_id}</span>
496
+ <span><strong>Data wystawienia:</strong> ${invoice.issued_at}</span>
497
+ <span><strong>Data sprzedazy:</strong> ${invoice.sale_date}</span>
498
+ </div>
499
+ <div class="invoice-preview-header">
500
+ <div class="invoice-preview-card">
501
+ <h4>Nabywca</h4>
502
+ ${
503
+ hasClientData
504
+ ? `
505
+ <p>${client.name || "---"}</p>
506
+ <p>${client.address_line || "---"}</p>
507
+ <p>${client.postal_code || ""} ${client.city || ""}</p>
508
+ <p>${client.tax_id ? `NIP: ${client.tax_id}` : ""}</p>
509
+ `
510
+ : "<p>Brak danych nabywcy.</p>"
511
+ }
512
+ </div>
513
+ <div class="invoice-preview-card">
514
+ <h4>Sprzedawca</h4>
515
+ <p>${currentBusiness.company_name}</p>
516
+ <p>${currentBusiness.owner_name}</p>
517
+ <p>${currentBusiness.address_line}</p>
518
+ <p>${currentBusiness.postal_code} ${currentBusiness.city}</p>
519
+ <p>NIP: ${currentBusiness.tax_id}</p>
520
+ <p>Konto: ${currentBusiness.bank_account}</p>
521
+ </div>
522
+ </div>
523
+ <table>
524
+ <thead>
525
+ <tr>
526
+ <th>Nazwa</th>
527
+ <th>Ilość</th>
528
+ <th>Cena jedn. netto</th>
529
+ <th>Wartość netto (pozycja)</th>
530
+ <th>Stawka VAT</th>
531
+ <th>Kwota VAT (pozycja)</th>
532
+ <th>Wartość brutto</th>
533
+ </tr>
534
+ </thead>
535
+ <tbody>${itemsRows}</tbody>
536
+ </table>
537
+ <div class="rate-summary">
538
+ <h4>Podsumowanie stawek</h4>
539
+ ${summaryRows}
540
+ </div>
541
+ <div class="invoice-preview-summary">
542
+ <span>Netto: ${formatCurrency(invoice.totals.net)}</span>
543
+ <span>VAT: ${formatCurrency(invoice.totals.vat)}</span>
544
+ <span>Brutto: ${formatCurrency(invoice.totals.gross)}</span>
545
+ </div>
546
+ ${
547
+ invoice.exemption_note
548
+ ? `<div class="invoice-preview-note"><strong>Podstawa prawna zwolnienia:</strong> ${invoice.exemption_note}</div>`
549
+ : ""
550
+ }
551
+ `;
552
+ }
553
+
554
+ function drawPartyBox(doc, title, lines, x, y, width) {
555
+ const lineHeight = 5;
556
+ const wrappedLines = lines.flatMap((line) => doc.splitTextToSize(line, width));
557
+ const boxHeight = wrappedLines.length * lineHeight + 14;
558
+
559
+ doc.roundedRect(x - 4, y - 8, width + 8, boxHeight, 2, 2);
560
+ doc.setFontSize(11);
561
+ doc.text(title, x, y);
562
+ doc.setFontSize(10);
563
+
564
+ let cursor = y + 5;
565
+ wrappedLines.forEach((line) => {
566
+ doc.text(line, x, cursor);
567
+ cursor += lineHeight;
568
+ });
569
+
570
+ return y - 8 + boxHeight;
571
+ }
572
+
573
+ function arrayBufferToBase64(buffer) {
574
+ const bytes = new Uint8Array(buffer);
575
+ const chunkSize = 0x8000;
576
+ let binary = "";
577
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
578
+ const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length));
579
+ binary += String.fromCharCode.apply(null, chunk);
580
+ }
581
+ return btoa(binary);
582
+ }
583
+
584
+ const PDF_FONT_FILE = "Roboto-VariableFont_wdth,wght.ttf";
585
+ const PDF_FONT_NAME = "RobotoPolish";
586
+
587
+ async function ensurePdfFont() {
588
+ if (pdfFontPromise) {
589
+ return pdfFontPromise;
590
+ }
591
+
592
+ if (!window.jspdf || !window.jspdf.jsPDF) {
593
+ throw new Error("Biblioteka jsPDF nie zostala zaladowana.");
594
+ }
595
+
596
+ const { jsPDF } = window.jspdf;
597
+ const loadBase64 = async () => {
598
+ if (typeof window !== "undefined" && window.PDF_FONT_BASE64) {
599
+ return window.PDF_FONT_BASE64;
600
+ }
601
+ const response = await fetch(`/${encodeURIComponent(PDF_FONT_FILE)}`);
602
+ if (!response.ok) {
603
+ throw new Error(`Nie udalo sie pobrac czcionki Roboto (status ${response.status}).`);
604
+ }
605
+ const buffer = await response.arrayBuffer();
606
+ return arrayBufferToBase64(buffer);
607
+ };
608
+
609
+ pdfFontPromise = loadBase64().then((data) => {
610
+ pdfFontBase64 = data;
611
+ return data;
612
+ });
613
+
614
+ return pdfFontPromise;
615
+ }
616
+
617
+ async function generatePdf(business, invoice) {
618
+ if (!window.jspdf || !window.jspdf.jsPDF) {
619
+ alert("Biblioteka jsPDF nie zostala zaladowana. Sprawdz polaczenie z internetem.");
620
+ return;
621
+ }
622
+
623
+ let fontBase64;
624
+ try {
625
+ fontBase64 = await ensurePdfFont();
626
+ } catch (error) {
627
+ alert(error.message || "Nie udalo sie przygotowac czcionki do PDF.");
628
+ return;
629
+ }
630
+
631
+ const { jsPDF } = window.jspdf;
632
+ const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
633
+ const marginX = 18;
634
+ let cursorY = 20;
635
+
636
+ if (!doc.getFontList()[PDF_FONT_NAME]) {
637
+ const embeddedFont = pdfFontBase64 || fontBase64;
638
+ doc.addFileToVFS(PDF_FONT_FILE, embeddedFont);
639
+ doc.addFont(PDF_FONT_FILE, PDF_FONT_NAME, "normal");
640
+ }
641
+
642
+ doc.setFont(PDF_FONT_NAME, "normal");
643
+ doc.setFontSize(16);
644
+ doc.text(`Faktura ${invoice.invoice_id}`, marginX, cursorY);
645
+ doc.setFontSize(10);
646
+ doc.text(`Data wystawienia: ${invoice.issued_at}`, marginX, cursorY + 6);
647
+ doc.text(`Data sprzedaży: ${invoice.sale_date}`, marginX, cursorY + 12);
648
+
649
+ cursorY += 22;
650
+ const columnWidth = 85;
651
+ const sellerX = marginX + columnWidth + 12;
652
+
653
+ const clientLines = invoice.client && (invoice.client.name || invoice.client.address_line || invoice.client.city || invoice.client.tax_id)
654
+ ? [
655
+ invoice.client.name || "---",
656
+ invoice.client.address_line || "",
657
+ `${(invoice.client.postal_code || "").trim()} ${(invoice.client.city || "").trim()}`.trim(),
658
+ invoice.client.tax_id ? `NIP: ${invoice.client.tax_id}` : "",
659
+ ].filter((line) => line && line.trim())
660
+ : ["Brak danych nabywcy"];
661
+
662
+ const sellerLines = [
663
+ business.company_name,
664
+ business.owner_name,
665
+ business.address_line,
666
+ `${business.postal_code} ${business.city}`.trim(),
667
+ `NIP: ${business.tax_id}`,
668
+ `Konto: ${business.bank_account}`,
669
+ ];
670
+
671
+ const buyerBottom = drawPartyBox(doc, "NABYWCA", clientLines, marginX, cursorY, columnWidth);
672
+ const sellerBottom = drawPartyBox(doc, "SPRZEDAWCA", sellerLines, sellerX, cursorY, columnWidth);
673
+ cursorY = Math.max(buyerBottom, sellerBottom) + 12;
674
+
675
+ const tableColumns = [
676
+ { key: "name", label: "Nazwa", width: 52 },
677
+ { key: "quantity", label: "Ilość", width: 16 },
678
+ { key: "unitNet", label: "Cena jedn. netto", width: 24 },
679
+ { key: "netTotal", label: "Wartość netto", width: 24 },
680
+ { key: "vatLabel", label: "Stawka VAT", width: 15 },
681
+ { key: "vatAmount", label: "Kwota VAT", width: 22 },
682
+ { key: "grossTotal", label: "Wartość brutto", width: 21 },
683
+ ];
684
+ const tableWidth = tableColumns.reduce((sum, col) => sum + col.width, 0);
685
+ const lineHeight = 5;
686
+ const headerLineHeight = 4.2;
687
+ tableColumns.forEach((column) => {
688
+ column.headerLines = doc.splitTextToSize(column.label, column.width - 4);
689
+ });
690
+ const headerHeight = Math.max(...tableColumns.map((column) => column.headerLines.length * headerLineHeight + 3));
691
+
692
+ doc.setFillColor(241, 243, 247);
693
+ doc.rect(marginX, cursorY, tableWidth, headerHeight, "F");
694
+ doc.rect(marginX, cursorY, tableWidth, headerHeight);
695
+ let offsetX = marginX;
696
+ doc.setFontSize(10);
697
+ tableColumns.forEach((column) => {
698
+ doc.rect(offsetX, cursorY, column.width, headerHeight);
699
+ column.headerLines.forEach((line, index) => {
700
+ const textY = cursorY + 4 + index * headerLineHeight;
701
+ doc.text((line || "").trim(), offsetX + 2, textY);
702
+ });
703
+ offsetX += column.width;
704
+ });
705
+ cursorY += headerHeight;
706
+
707
+ invoice.items.forEach((item) => {
708
+ const quantity = parseNumber(item.quantity).toFixed(2);
709
+ const unitNet = formatCurrency(item.unit_price_net);
710
+ const netTotal = formatCurrency(item.net_total);
711
+ const vatAmount = formatCurrency(item.vat_amount);
712
+ const grossTotal = formatCurrency(item.gross_total);
713
+
714
+ const wrapText = (text, width) =>
715
+ doc
716
+ .splitTextToSize(text ?? "", width)
717
+ .map((line) => line.trim());
718
+
719
+ const columnData = tableColumns.map((column) => {
720
+ switch (column.key) {
721
+ case "name":
722
+ return wrapText(item.name, column.width - 4);
723
+ case "quantity":
724
+ return [quantity];
725
+ case "unitNet":
726
+ return [unitNet];
727
+ case "netTotal":
728
+ return [netTotal];
729
+ case "vatLabel":
730
+ return [item.vat_label];
731
+ case "vatAmount":
732
+ return [vatAmount];
733
+ case "grossTotal":
734
+ return [grossTotal];
735
+ default:
736
+ return [""];
737
+ }
738
+ });
739
+
740
+ const rowHeight = Math.max(...columnData.map((data) => data.length)) * lineHeight + 2;
741
+ offsetX = marginX;
742
+ tableColumns.forEach((column, index) => {
743
+ doc.rect(offsetX, cursorY, column.width, rowHeight);
744
+ const lines = columnData[index];
745
+ lines.forEach((line, lineIndex) => {
746
+ const textY = cursorY + (lineIndex + 1) * lineHeight;
747
+ const content = (line || "").trim();
748
+ doc.text(content, offsetX + 2, textY);
749
+ });
750
+ offsetX += column.width;
751
+ });
752
+
753
+ cursorY += rowHeight;
754
+ });
755
+
756
+ cursorY += 10;
757
+ doc.setFontSize(11);
758
+ doc.text("Podsumowanie stawek:", marginX, cursorY);
759
+ cursorY += 6;
760
+
761
+ const summaryEntries = Array.isArray(invoice.summary) ? invoice.summary : [];
762
+ summaryEntries.forEach((entry) => {
763
+ const summaryLine = `${entry.vat_label} – Netto: ${formatCurrency(entry.net_total)} / VAT: ${formatCurrency(entry.vat_total)} / Brutto: ${formatCurrency(entry.gross_total)}`;
764
+ const wrapped = doc.splitTextToSize(summaryLine, 170);
765
+ wrapped.forEach((line) => {
766
+ doc.text((line || "").trim(), marginX, cursorY);
767
+ cursorY += lineHeight;
768
+ });
769
+ });
770
+
771
+ cursorY += 6;
772
+ doc.setFontSize(12);
773
+ doc.text(`Suma netto: ${formatCurrency(invoice.totals.net)}`, marginX, cursorY);
774
+ doc.text(`Suma VAT: ${formatCurrency(invoice.totals.vat)}`, marginX, cursorY + 6);
775
+ doc.text(`Suma brutto: ${formatCurrency(invoice.totals.gross)}`, marginX, cursorY + 12);
776
+ cursorY += 20;
777
+
778
+ if (invoice.exemption_note) {
779
+ doc.setFontSize(10);
780
+ const noteLines = doc.splitTextToSize(`Podstawa prawna zwolnienia: ${invoice.exemption_note}`, 170);
781
+ doc.text(noteLines, marginX, cursorY);
782
+ }
783
+
784
+ doc.save(`${invoice.invoice_id}.pdf`);
785
+ }
786
+
787
+ async function loadBusinessData() {
788
+ const data = await apiRequest("/api/business", {}, true);
789
+ currentBusiness = data.business;
790
+ renderBusinessDisplay(currentBusiness);
791
+ fillBusinessForm(currentBusiness);
792
+ }
793
+
794
+ function resetInvoiceForm() {
795
+ invoiceForm.reset();
796
+ exemptionNoteInput.value = "";
797
+ exemptionNoteWrapper.classList.add("hidden");
798
+ itemsBody.innerHTML = "";
799
+ createItemRow();
800
+ const today = new Date().toISOString().slice(0, 10);
801
+ invoiceForm.elements.saleDate.value = today;
802
+ }
803
+
804
+ async function bootstrapApp() {
805
+ try {
806
+ await loadBusinessData();
807
+ setState("app");
808
+ } catch (error) {
809
+ console.error(error);
810
+ authToken = null;
811
+ sessionStorage.removeItem("invoiceAuthToken");
812
+ showFeedback(loginFeedback, error.message || "Nie udalo sie pobrac danych firmy.");
813
+ setState("login");
814
+ }
815
+ }
816
+
817
+ async function initialize() {
818
+ resetInvoiceForm();
819
+ try {
820
+ const status = await apiRequest("/api/status");
821
+ if (!status.configured) {
822
+ setState("setup");
823
+ return;
824
+ }
825
+
826
+ if (authToken) {
827
+ await bootstrapApp();
828
+ } else {
829
+ setState("login");
830
+ }
831
+ } catch (error) {
832
+ console.error(error);
833
+ setState("setup");
834
+ showFeedback(setupFeedback, "Nie udalo sie nawiazac polaczenia z serwerem.");
835
+ }
836
+ }
837
+
838
+ setupForm.addEventListener("submit", async (event) => {
839
+ event.preventDefault();
840
+ clearFeedback(setupFeedback);
841
+
842
+ const formData = new FormData(setupForm);
843
+ const password = formData.get("password")?.toString() ?? "";
844
+ const confirmPassword = formData.get("confirm_password")?.toString() ?? "";
845
+
846
+ if (password !== confirmPassword) {
847
+ showFeedback(setupFeedback, "Hasla musza byc identyczne.");
848
+ return;
849
+ }
850
+
851
+ if (password.trim().length < 4) {
852
+ showFeedback(setupFeedback, "Haslo musi miec co najmniej 4 znaki.");
853
+ return;
854
+ }
855
+
856
+ const payload = {
857
+ company_name: formData.get("company_name")?.toString().trim(),
858
+ owner_name: formData.get("owner_name")?.toString().trim(),
859
+ address_line: formData.get("address_line")?.toString().trim(),
860
+ postal_code: formData.get("postal_code")?.toString().trim(),
861
+ city: formData.get("city")?.toString().trim(),
862
+ tax_id: formData.get("tax_id")?.toString().trim(),
863
+ bank_account: formData.get("bank_account")?.toString().trim(),
864
+ password,
865
+ };
866
+
867
+ try {
868
+ await apiRequest("/api/setup", { method: "POST", body: payload });
869
+ showFeedback(setupFeedback, "Dane zapisane. Mozesz sie zalogowac.", "success");
870
+ setTimeout(() => {
871
+ setState("login");
872
+ clearFeedback(setupFeedback);
873
+ setupForm.reset();
874
+ }, 1500);
875
+ } catch (error) {
876
+ showFeedback(setupFeedback, error.message || "Nie udalo sie zapisac danych.");
877
+ }
878
+ });
879
+
880
+ loginForm.addEventListener("submit", async (event) => {
881
+ event.preventDefault();
882
+ clearFeedback(loginFeedback);
883
+
884
+ const password = loginForm.elements.password.value;
885
+ if (!password) {
886
+ showFeedback(loginFeedback, "Podaj haslo.");
887
+ return;
888
+ }
889
+
890
+ try {
891
+ const response = await apiRequest("/api/login", { method: "POST", body: { password } });
892
+ authToken = response.token;
893
+ sessionStorage.setItem("invoiceAuthToken", authToken);
894
+ loginForm.reset();
895
+ await bootstrapApp();
896
+ } catch (error) {
897
+ showFeedback(loginFeedback, error.message || "Logowanie nie powiodlo sie.");
898
+ }
899
+ });
900
+
901
+ toggleBusinessFormButton.addEventListener("click", () => {
902
+ const isHidden = businessForm.classList.contains("hidden");
903
+ if (isHidden) {
904
+ fillBusinessForm(currentBusiness);
905
+ businessForm.classList.remove("hidden");
906
+ toggleBusinessFormButton.textContent = "Ukryj formularz";
907
+ } else {
908
+ businessForm.classList.add("hidden");
909
+ toggleBusinessFormButton.textContent = "Edytuj dane";
910
+ clearFeedback(businessFeedback);
911
+ }
912
+ });
913
+
914
+ cancelBusinessUpdateButton.addEventListener("click", () => {
915
+ businessForm.classList.add("hidden");
916
+ toggleBusinessFormButton.textContent = "Edytuj dane";
917
+ clearFeedback(businessFeedback);
918
+ });
919
+
920
+ businessForm.addEventListener("submit", async (event) => {
921
+ event.preventDefault();
922
+ clearFeedback(businessFeedback);
923
+
924
+ const formData = new FormData(businessForm);
925
+ const payload = {
926
+ company_name: formData.get("company_name")?.toString().trim(),
927
+ owner_name: formData.get("owner_name")?.toString().trim(),
928
+ address_line: formData.get("address_line")?.toString().trim(),
929
+ postal_code: formData.get("postal_code")?.toString().trim(),
930
+ city: formData.get("city")?.toString().trim(),
931
+ tax_id: formData.get("tax_id")?.toString().trim(),
932
+ bank_account: formData.get("bank_account")?.toString().trim(),
933
+ };
934
+
935
+ try {
936
+ const data = await apiRequest("/api/business", { method: "PUT", body: payload }, true);
937
+ currentBusiness = data.business;
938
+ renderBusinessDisplay(currentBusiness);
939
+ showFeedback(businessFeedback, "Dane sprzedawcy zaktualizowane.", "success");
940
+ setTimeout(() => clearFeedback(businessFeedback), 2000);
941
+ } catch (error) {
942
+ showFeedback(businessFeedback, error.message || "Nie udalo sie zaktualizowac danych.");
943
+ }
944
+ });
945
+
946
+ invoiceForm.addEventListener("submit", async (event) => {
947
+ event.preventDefault();
948
+ try {
949
+ const payload = collectInvoicePayload();
950
+ const response = await apiRequest("/api/invoices", { method: "POST", body: payload }, true);
951
+ lastInvoice = response.invoice;
952
+ renderInvoicePreview(lastInvoice);
953
+ invoiceResult.classList.remove("hidden");
954
+ resetInvoiceForm();
955
+ } catch (error) {
956
+ alert(error.message || "Nie udalo sie wygenerowac faktury.");
957
+ }
958
+ });
959
+
960
+ addItemButton.addEventListener("click", () => {
961
+ createItemRow();
962
+ });
963
+
964
+ downloadButton.addEventListener("click", async () => {
965
+ if (!lastInvoice || !currentBusiness) {
966
+ alert("Brak faktury do pobrania. Wygeneruj ja najpierw.");
967
+ return;
968
+ }
969
+ await generatePdf(currentBusiness, lastInvoice);
970
+ });
971
+
972
+ logoutButton.addEventListener("click", () => {
973
+ authToken = null;
974
+ sessionStorage.removeItem("invoiceAuthToken");
975
+ lastInvoice = null;
976
+ currentBusiness = null;
977
+ invoiceResult.classList.add("hidden");
978
+ setState("login");
979
+ });
980
+
981
+ initialize().catch((error) => {
982
+ console.error(error);
983
+ showFeedback(setupFeedback, "Nie udalo sie uruchomic aplikacji.");
984
+ });
styles.css ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #f5f5f5;
3
+ --panel-bg: #ffffff;
4
+ --text: #202124;
5
+ --muted: #5f6368;
6
+ --accent: #1a73e8;
7
+ --danger: #c5221f;
8
+ --border: #dadce0;
9
+ --radius: 10px;
10
+ --shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
11
+ font-family: "Segoe UI", Tahoma, sans-serif;
12
+ }
13
+
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ margin: 0;
20
+ background: linear-gradient(180deg, #f7f9fc 0%, #eef1f6 100%);
21
+ color: var(--text);
22
+ }
23
+
24
+ .container {
25
+ max-width: 980px;
26
+ margin: 0 auto;
27
+ padding: 40px 20px 64px;
28
+ }
29
+
30
+ h1 {
31
+ text-align: center;
32
+ font-size: 32px;
33
+ margin-bottom: 32px;
34
+ }
35
+
36
+ .panel {
37
+ background: var(--panel-bg);
38
+ border-radius: var(--radius);
39
+ box-shadow: var(--shadow);
40
+ padding: 28px 32px;
41
+ margin-bottom: 32px;
42
+ border: 1px solid rgba(32, 33, 36, 0.08);
43
+ }
44
+
45
+ .panel h2 {
46
+ margin-top: 0;
47
+ }
48
+
49
+ .hidden {
50
+ display: none;
51
+ }
52
+
53
+ .form {
54
+ display: grid;
55
+ gap: 20px;
56
+ }
57
+
58
+ .field-grid {
59
+ display: grid;
60
+ gap: 18px 24px;
61
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
62
+ }
63
+
64
+ label {
65
+ display: grid;
66
+ gap: 8px;
67
+ font-weight: 600;
68
+ font-size: 15px;
69
+ }
70
+
71
+ input,
72
+ textarea,
73
+ select {
74
+ padding: 12px 14px;
75
+ border-radius: 8px;
76
+ border: 1px solid var(--border);
77
+ font-size: 15px;
78
+ background: #fbfbff;
79
+ }
80
+
81
+ input:focus,
82
+ textarea:focus,
83
+ select:focus {
84
+ outline: 2px solid rgba(26, 115, 232, 0.35);
85
+ outline-offset: 1px;
86
+ border-color: var(--accent);
87
+ background: #ffffff;
88
+ }
89
+
90
+ textarea {
91
+ resize: vertical;
92
+ min-height: 96px;
93
+ }
94
+
95
+ button {
96
+ padding: 12px 20px;
97
+ border-radius: 8px;
98
+ border: none;
99
+ font-size: 15px;
100
+ font-weight: 600;
101
+ background: var(--accent);
102
+ color: white;
103
+ cursor: pointer;
104
+ transition: background 0.2s ease;
105
+ }
106
+
107
+ button:hover {
108
+ background: #0f5ec4;
109
+ }
110
+
111
+ button:disabled {
112
+ opacity: 0.6;
113
+ cursor: not-allowed;
114
+ }
115
+
116
+ .link-button {
117
+ background: none;
118
+ color: var(--accent);
119
+ padding: 0;
120
+ }
121
+
122
+ .link-button:hover {
123
+ color: #0f5ec4;
124
+ background: none;
125
+ }
126
+
127
+ .hint {
128
+ color: var(--muted);
129
+ font-size: 13px;
130
+ margin: 0;
131
+ }
132
+
133
+ .feedback {
134
+ color: var(--muted);
135
+ min-height: 20px;
136
+ font-size: 14px;
137
+ }
138
+
139
+ .feedback.error {
140
+ color: var(--danger);
141
+ }
142
+
143
+ .feedback.success {
144
+ color: #188038;
145
+ }
146
+
147
+ .invoice-header {
148
+ display: flex;
149
+ justify-content: space-between;
150
+ align-items: center;
151
+ margin-bottom: 24px;
152
+ }
153
+
154
+ .business-section {
155
+ border: 1px solid rgba(32, 33, 36, 0.08);
156
+ border-radius: var(--radius);
157
+ padding: 20px 24px;
158
+ background: #fbfcff;
159
+ margin-bottom: 24px;
160
+ }
161
+
162
+ .business-section-header {
163
+ display: flex;
164
+ justify-content: space-between;
165
+ align-items: center;
166
+ margin-bottom: 12px;
167
+ }
168
+
169
+ .business-display {
170
+ display: grid;
171
+ gap: 6px;
172
+ font-size: 15px;
173
+ line-height: 1.4;
174
+ }
175
+
176
+ .business-display strong {
177
+ font-weight: 600;
178
+ }
179
+
180
+ .form-actions {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 16px;
184
+ }
185
+
186
+ .items-section {
187
+ border: 1px solid rgba(32, 33, 36, 0.08);
188
+ border-radius: var(--radius);
189
+ padding: 20px 24px;
190
+ background: #ffffff;
191
+ display: grid;
192
+ gap: 18px;
193
+ }
194
+
195
+ .items-header {
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: center;
199
+ }
200
+
201
+ .items-table-wrapper {
202
+ overflow-x: auto;
203
+ }
204
+
205
+ .items-table {
206
+ width: 100%;
207
+ border-collapse: collapse;
208
+ font-size: 14px;
209
+ }
210
+
211
+ .items-table th,
212
+ .items-table td {
213
+ border: 1px solid var(--border);
214
+ padding: 10px 12px;
215
+ text-align: left;
216
+ }
217
+
218
+ .items-table th {
219
+ background: #f1f3f7;
220
+ font-weight: 600;
221
+ text-transform: uppercase;
222
+ letter-spacing: 0.02em;
223
+ }
224
+
225
+ .items-table input,
226
+ .items-table select {
227
+ width: 100%;
228
+ padding: 8px 10px;
229
+ border-radius: 6px;
230
+ border: 1px solid var(--border);
231
+ background: #ffffff;
232
+ }
233
+
234
+ .items-table .remove-item {
235
+ color: var(--danger);
236
+ background: none;
237
+ padding: 0;
238
+ }
239
+
240
+ .items-table .remove-item:hover {
241
+ text-decoration: underline;
242
+ }
243
+
244
+ .totals {
245
+ display: flex;
246
+ flex-wrap: wrap;
247
+ gap: 16px;
248
+ font-weight: 600;
249
+ margin-top: 8px;
250
+ padding: 12px 16px;
251
+ border-radius: 8px;
252
+ background: #f7f9ff;
253
+ }
254
+
255
+ .rate-summary {
256
+ display: grid;
257
+ gap: 10px;
258
+ margin-top: 8px;
259
+ }
260
+
261
+ .rate-summary-item {
262
+ display: flex;
263
+ flex-wrap: wrap;
264
+ gap: 12px;
265
+ font-weight: 600;
266
+ padding: 8px 12px;
267
+ border: 1px dashed rgba(26, 115, 232, 0.25);
268
+ border-radius: 6px;
269
+ background: #ffffff;
270
+ }
271
+
272
+ .invoice-preview {
273
+ display: grid;
274
+ gap: 24px;
275
+ }
276
+
277
+ .invoice-preview-meta {
278
+ display: flex;
279
+ flex-wrap: wrap;
280
+ gap: 24px;
281
+ font-size: 14px;
282
+ }
283
+
284
+ .invoice-preview-meta span {
285
+ display: inline-flex;
286
+ align-items: center;
287
+ gap: 6px;
288
+ }
289
+
290
+ .invoice-preview-header {
291
+ display: flex;
292
+ flex-wrap: wrap;
293
+ gap: 24px;
294
+ }
295
+
296
+ .invoice-preview-card {
297
+ flex: 1 1 280px;
298
+ border: 1px solid var(--border);
299
+ border-radius: var(--radius);
300
+ padding: 16px 20px;
301
+ background: #f9fafc;
302
+ }
303
+
304
+ .invoice-preview-card h4 {
305
+ margin: 0 0 12px;
306
+ font-size: 14px;
307
+ text-transform: uppercase;
308
+ letter-spacing: 0.05em;
309
+ }
310
+
311
+ .invoice-preview-card p {
312
+ margin: 4px 0;
313
+ font-size: 14px;
314
+ }
315
+
316
+ .invoice-preview table {
317
+ width: 100%;
318
+ border-collapse: collapse;
319
+ font-size: 14px;
320
+ }
321
+
322
+ .invoice-preview th,
323
+ .invoice-preview td {
324
+ border: 1px solid var(--border);
325
+ padding: 10px 12px;
326
+ text-align: left;
327
+ }
328
+
329
+ .invoice-preview th {
330
+ background: #f1f3f7;
331
+ font-weight: 600;
332
+ }
333
+
334
+ .invoice-preview-summary {
335
+ display: flex;
336
+ justify-content: flex-end;
337
+ flex-wrap: wrap;
338
+ gap: 16px;
339
+ font-weight: 600;
340
+ font-size: 15px;
341
+ }
342
+
343
+ .invoice-preview-note {
344
+ padding: 12px 16px;
345
+ border-left: 3px solid var(--accent);
346
+ background: #f4f8ff;
347
+ font-size: 14px;
348
+ }
349
+
350
+ @media (max-width: 640px) {
351
+ .panel {
352
+ padding: 20px;
353
+ }
354
+
355
+ .invoice-header {
356
+ flex-direction: column;
357
+ gap: 12px;
358
+ align-items: flex-start;
359
+ }
360
+
361
+ .business-section,
362
+ .items-section {
363
+ padding: 16px;
364
+ }
365
+ }