Skip to content

Commit 77e5b40

Browse files
committed
implemented new logic for prodajno mjesto
1 parent d228b51 commit 77e5b40

11 files changed

Lines changed: 194 additions & 60 deletions

File tree

backend/fira.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@
1515
* @param {string} order.currency - Currency code (e.g. "EUR")
1616
* @param {Array} order.items - Array of receipt items
1717
*/
18-
export async function handleOrderFiscalization(order) {
18+
export async function handleOrderFiscalization(order, options = {}) {
1919
if (!order.paymentType) {
2020
throw new Error(`Order ${order.code} is missing paymentType. Must be KARTICA or GOTOVINA.`);
2121
}
22+
const { firaApiKey, prodajnoMjestoNaziv } = options;
23+
if (!firaApiKey) {
24+
throw new Error("FIRA API ključ nije proslijeđen. Odaberite prodajno mjesto u admin postavkama.");
25+
}
2226

2327
// Skip free orders - no invoice needed
2428
// if (order.total === 0) {
@@ -108,7 +112,7 @@ export async function handleOrderFiscalization(order) {
108112
lineItems,
109113
};
110114

111-
console.log(`Sending to FIRA: ${JSON.stringify(data, null, 2)}`);
115+
console.log(`Sending to FIRA [${prodajnoMjestoNaziv || "unknown"}]: ${JSON.stringify(data, null, 2)}`);
112116

113117
// MOCK response (comment out when using real FIRA)
114118
// const mockResponse = {

backend/index.js

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,22 @@ app.post("/api/receipts", requireAuth, async (req, res) => {
427427
return res.status(400).json({ error: "Invalid paymentType. Must be GOTOVINA, KARTICA, or TRANSAKCIJSKI" });
428428
}
429429

430+
// Validate active prodajno mjesto exists BEFORE creating anything in DB
431+
const appSettings = await prisma.appSettings.findUnique({
432+
where: { id: 1 },
433+
include: { prodajnoMjesto: true },
434+
});
435+
if (!appSettings?.prodajnoMjesto) {
436+
return res.status(400).json({ error: "Nije odabrano prodajno mjesto. Odaberite prodajno mjesto u admin postavkama." });
437+
}
438+
let firaApiKey;
439+
try {
440+
firaApiKey = decrypt(appSettings.prodajnoMjesto.firaApiKey);
441+
if (!firaApiKey) throw new Error("Prazan ključ");
442+
} catch {
443+
return res.status(500).json({ error: "Greška pri dešifriranju API ključa prodajnog mjesta." });
444+
}
445+
430446
// Create billing address if provided
431447
let billingAddressId = null;
432448
if (billingAddress) {
@@ -473,6 +489,7 @@ app.post("/api/receipts", requireAuth, async (req, res) => {
473489
internalNote,
474490
discountValue,
475491
shippingCost,
492+
prodajnoMjestoNaziv: appSettings.prodajnoMjesto.name,
476493
items: {
477494
create: items.map((item) => ({
478495
name: item.name,
@@ -509,7 +526,7 @@ app.post("/api/receipts", requireAuth, async (req, res) => {
509526
currency: receipt.currency,
510527
paymentType: receipt.paymentType,
511528
items: receipt.items,
512-
});
529+
}, { firaApiKey, prodajnoMjestoNaziv: appSettings.prodajnoMjesto.name });
513530

514531
if (firaResult && firaResult.invoiceNumber) {
515532
try {
@@ -547,7 +564,7 @@ app.post("/api/receipts", requireAuth, async (req, res) => {
547564
if (Math.abs(calculatedBrutto - brutto) > 0.01) {
548565
return res.status(400).json({ error: "Total amount mismatch." });
549566
}
550-
res.status(201).json(receipt);
567+
res.status(201).json({ ...receipt, prodajnoMjestoNaziv: appSettings.prodajnoMjesto.name });
551568
} catch (error) {
552569
res.status(400).json({ error: error.message });
553570
}
@@ -848,14 +865,62 @@ app.post("/api/reports", requireAuth, async (req, res) => {
848865
});
849866

850867

868+
// ========== APP SETTINGS API ==========
869+
870+
// GET trenutno odabrano prodajno mjesto
871+
app.get('/api/settings/active-location', requireAuth, async (req, res) => {
872+
if (req.user.role !== "ADMIN") return res.status(403).json({ error: "Unauthorized" });
873+
try {
874+
const settings = await prisma.appSettings.findUnique({
875+
where: { id: 1 },
876+
include: { prodajnoMjesto: true },
877+
});
878+
if (!settings || !settings.prodajnoMjesto) {
879+
return res.json({ selectedProdajnoMjestoId: null, prodajnoMjesto: null });
880+
}
881+
res.json({
882+
selectedProdajnoMjestoId: settings.selectedProdajnoMjestoId,
883+
prodajnoMjesto: { ...settings.prodajnoMjesto, firaApiKey: "********" },
884+
});
885+
} catch (error) {
886+
res.status(500).json({ error: "Greška" });
887+
}
888+
});
889+
890+
// PUT odabir aktivnog prodajnog mjesta
891+
app.put('/api/settings/active-location', requireAuth, async (req, res) => {
892+
if (req.user.role !== "ADMIN") return res.status(403).json({ error: "Unauthorized" });
893+
const { prodajnoMjestoId } = req.body;
894+
try {
895+
const settings = await prisma.appSettings.upsert({
896+
where: { id: 1 },
897+
update: { selectedProdajnoMjestoId: prodajnoMjestoId ?? null },
898+
create: { id: 1, selectedProdajnoMjestoId: prodajnoMjestoId ?? null },
899+
include: { prodajnoMjesto: true },
900+
});
901+
res.json({
902+
selectedProdajnoMjestoId: settings.selectedProdajnoMjestoId,
903+
prodajnoMjesto: settings.prodajnoMjesto
904+
? { ...settings.prodajnoMjesto, firaApiKey: "********" }
905+
: null,
906+
});
907+
} catch (error) {
908+
res.status(500).json({ error: "Greška pri ažuriranju" });
909+
}
910+
});
911+
851912
// GET all prodajna mjesta
852913
app.get('/api/prodajna-mjesta', async (req, res) => {
853914
try {
854915
const locations = await prisma.prodajnoMjesto.findMany();
855-
const safeLocations = locations.map(loc => ({
856-
...loc,
857-
firaApiKey: "********" // Don't send the real key back to the UI!
858-
}));
916+
const safeLocations = locations.map(loc => {
917+
let maskedKey = "********";
918+
try {
919+
const real = decrypt(loc.firaApiKey);
920+
maskedKey = `****${real.slice(-4)}`;
921+
} catch { /* leave as ******** if decryption fails */ }
922+
return { ...loc, firaApiKey: maskedKey };
923+
});
859924
res.json(safeLocations);
860925
} catch (error) {
861926
res.status(500).json({ error: "Greška" });
@@ -882,7 +947,7 @@ app.post('/api/prodajna-mjesta', async (req, res) => {
882947
}
883948
});
884949

885-
res.json(newLocation);
950+
res.json({ ...newLocation, firaApiKey: "********" });
886951
} catch (error) {
887952
// THIS LOG IS CRUCIAL: Check your terminal for this output!
888953
console.error("CRITICAL BACKEND ERROR:", error.message);
@@ -899,11 +964,15 @@ app.put('/api/prodajna-mjesta/:id', async (req, res) => {
899964
const { id } = req.params;
900965
const { name, businessSpace, paymentDevice, firaApiKey, active } = req.body;
901966
try {
967+
const data = { name, businessSpace, paymentDevice, active };
968+
if (firaApiKey && firaApiKey !== "********") {
969+
data.firaApiKey = encrypt(firaApiKey);
970+
}
902971
const updated = await prisma.prodajnoMjesto.update({
903972
where: { id: parseInt(id) },
904-
data: { name, businessSpace, paymentDevice, firaApiKey, active }
973+
data,
905974
});
906-
res.json(updated);
975+
res.json({ ...updated, firaApiKey: "********" });
907976
} catch (error) {
908977
res.status(500).json({ error: "Greška pri ažuriranju" });
909978
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- CreateTable
2+
CREATE TABLE "AppSettings" (
3+
"id" INTEGER NOT NULL DEFAULT 1,
4+
"selectedProdajnoMjestoId" INTEGER,
5+
6+
CONSTRAINT "AppSettings_pkey" PRIMARY KEY ("id"),
7+
CONSTRAINT "ensure_single_row" CHECK (id = 1)
8+
);
9+
10+
-- AddForeignKey
11+
ALTER TABLE "AppSettings" ADD CONSTRAINT "AppSettings_selectedProdajnoMjestoId_fkey" FOREIGN KEY ("selectedProdajnoMjestoId") REFERENCES "ProdajnoMjesto"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Receipt" ADD COLUMN "prodajnoMjestoNaziv" TEXT;

backend/prisma/schema.prisma

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ model Receipt {
9898
internalNote String?
9999
discountValue Decimal @db.Decimal(10, 2) @default(0)
100100
shippingCost Decimal @db.Decimal(10, 2) @default(0)
101+
prodajnoMjestoNaziv String?
101102
status ReceiptStatus @default(RACUN)
102103
items ReceiptItem[]
103104
transaction Transaction?
@@ -184,4 +185,11 @@ model ProdajnoMjesto {
184185
paymentDevice String
185186
firaApiKey String
186187
active Boolean @default(true)
188+
appSettings AppSettings[]
189+
}
190+
191+
model AppSettings {
192+
id Int @id @default(1)
193+
selectedProdajnoMjestoId Int?
194+
prodajnoMjesto ProdajnoMjesto? @relation(fields: [selectedProdajnoMjestoId], references: [id])
187195
}

frontend/src/components/Sidebar.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function Sidebar() {
3535
<div className="sidebar">
3636
<div className="sidebar-header">
3737
<h1>Fiskalna</h1>
38-
{user && <p className="user-name">{user.name} v1.2</p>}
38+
{user && <p className="user-name">{user.name}</p>}
3939
</div>
4040

4141
<nav className="sidebar-nav">

frontend/src/pages/Prodaja.jsx

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export default function Prodaja() {
1414
const [categories, setCategories] = useState([]);
1515
const [selectedItems, setSelectedItems] = useState([]);
1616
const [paymentMethod, setPaymentMethod] = useState("Gotovina");
17-
const [locations, setLocations] = useState([]);
1817
const [selectedLocationId, setSelectedLocationId] = useState("");
18+
const [activeLocation, setActiveLocation] = useState(null);
1919
const [loading, setLoading] = useState(true);
2020
const [searchParams] = useSearchParams();
2121
const [offlineCount, setOfflineCount] = useState(0);
@@ -38,22 +38,27 @@ export default function Prodaja() {
3838
fetchCategories();
3939

4040
const fetchLocations = async () => {
41-
try {
42-
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/prodajna-mjesta`, { credentials: "include" });
43-
const data = await response.json();
44-
if (response.ok && Array.isArray(data)) {
45-
setLocations(data.filter(loc => loc.active));
46-
if (data.length > 0 && !selectedLocationId) {
41+
try {
42+
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/prodajna-mjesta`, { credentials: "include" });
43+
const data = await response.json();
44+
if (response.ok && Array.isArray(data) && data.length > 0 && !selectedLocationId) {
4745
setSelectedLocationId(String(data[0].id));
4846
}
49-
} else {
50-
setLocations([]);
47+
} catch { /* ignore */ }
48+
};
49+
50+
const fetchActiveLocation = async () => {
51+
try {
52+
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/settings/active-location`, { credentials: "include" });
53+
const data = await response.json();
54+
setActiveLocation(data.prodajnoMjesto ?? null);
55+
} catch {
56+
setActiveLocation(null);
5157
}
52-
} catch {
53-
setLocations([]);
54-
}
5558
};
59+
5660
fetchLocations();
61+
fetchActiveLocation();
5762
}, [selectedLocationId]);
5863

5964
const fetchArticles = async () => {
@@ -197,6 +202,14 @@ export default function Prodaja() {
197202
return `https://porezna.gov.hr/rn?jir=${jir}&datv=${datv}&izn=${iznFormatted}`;
198203
};
199204

205+
if (receipt.prodajnoMjestoNaziv && activeLocation?.name !== receipt.prodajnoMjestoNaziv) {
206+
try {
207+
const locRes = await fetch(`${import.meta.env.VITE_API_URL}/api/settings/active-location`, { credentials: "include" });
208+
const locData = await locRes.json();
209+
setActiveLocation(locData.prodajnoMjesto ?? null);
210+
} catch { /* ignore */ }
211+
}
212+
200213
printFunction({
201214
num: receipt.invoiceNumber || receipt.receiptNumber,
202215
payment: paymentMethod,
@@ -207,7 +220,7 @@ export default function Prodaja() {
207220
tax: receipt.taxValue ?? totalTax,
208221
jir: receipt.jir ?? "",
209222
zki: receipt.zki ?? "",
210-
location: locations.find((loc) => String(loc.id) === String(selectedLocationId))?.name || "",
223+
location: receipt.prodajnoMjestoNaziv || activeLocation?.name || "",
211224
link: buildPoreznaLink(receipt.jir, receipt.invoiceDate || receipt.createdAt || new Date(), receipt.brutto ?? totalBrutto),
212225
phone: "0916043415",
213226
email: "info@kset.org",
@@ -340,22 +353,11 @@ export default function Prodaja() {
340353

341354
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px', flexWrap: 'wrap', gap: '10px' }}>
342355
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
343-
<h1 style={{ margin: 0 }}>
344-
Prodaja
345-
</h1>
346-
<label style={{ fontSize: '14px', color: '#333' }}>
347-
Prodajno mjesto:
348-
<select
349-
value={selectedLocationId}
350-
onChange={(e) => setSelectedLocationId(e.target.value)}
351-
style={{ marginLeft: '8px', padding: '5px 8px', borderRadius: '4px', border: '1px solid #ccc' }}
352-
>
353-
<option value="">(nije odabrano)</option>
354-
{locations.map(loc => (
355-
<option key={loc.id} value={loc.id}>{loc.name} ({loc.businessSpace})</option>
356-
))}
357-
</select>
358-
</label>
356+
<h1 style={{ margin: 0 }}>Prodaja</h1>
357+
{activeLocation
358+
? <span style={{ fontSize: '0.9rem', color: '#fff', background: '#e67e22', padding: '4px 10px', borderRadius: '20px' }}>Prodajno mjesto: {activeLocation.name}</span>
359+
: <span style={{ fontSize: '0.85rem', color: '#c0392b', background: '#fdecea', padding: '4px 10px', borderRadius: '20px' }}>Nije odabrano prodajno mjesto</span>
360+
}
359361
</div>
360362
{offlineCount > 0 && (
361363
<span style={{color: 'red', fontSize: '14px', marginLeft: '10px'}}>

frontend/src/pages/Racuni.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export default function Racuni() {
142142
<td>
143143
{new Date(receipt.createdAt).toLocaleTimeString("hr-HR", {hour: '2-digit', minute: '2-digit', second: '2-digit'})}
144144
</td>
145-
<td>{receipt.prodajnoMjesto?.name || "N/A"}</td>
145+
<td>{receipt.prodajnoMjestoNaziv || "N/A"}</td>
146146
<td>{receipt.paymentType}</td>
147147
<td><span className="currency">{parseFloat(receipt.brutto).toFixed(2)}</span></td>
148148
<td>

0 commit comments

Comments
 (0)