Ispravka QR koda za 2FA — generisanje na serveru kao base64 PNG
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ntech/internal/db/sqlite"
|
||||
"ntech/internal/model"
|
||||
)
|
||||
|
||||
var srpskaImenaMeseci = []string{
|
||||
"", "Januar", "Februar", "Mart", "April", "Maj", "Jun",
|
||||
"Jul", "Avgust", "Septembar", "Oktobar", "Novembar", "Decembar",
|
||||
}
|
||||
|
||||
func formatujMesec(yyyymm string) string {
|
||||
var god, mes int
|
||||
if _, err := fmt.Sscanf(yyyymm, "%d-%d", &god, &mes); err != nil || mes < 1 || mes > 12 {
|
||||
return yyyymm
|
||||
}
|
||||
return fmt.Sprintf("%s %d", srpskaImenaMeseci[mes], god)
|
||||
}
|
||||
|
||||
// PodaciIzvestaja su podaci za stranicu izveštaja
|
||||
type PodaciIzvestaja struct {
|
||||
model.PodaciStranice
|
||||
MesecniPrihodi []MesecniPrihod
|
||||
GrafikonJSON template.JS
|
||||
StariNalozi []StariNalog
|
||||
TopArtikli []TopArtikal
|
||||
TopKlijenti []TopKlijent
|
||||
}
|
||||
|
||||
// MesecniPrihod drži prihod od prodaje i servisa za jedan mesec
|
||||
type MesecniPrihod struct {
|
||||
MesecPrikaz string
|
||||
Prodaja float64
|
||||
Servis float64
|
||||
Ukupno float64
|
||||
}
|
||||
|
||||
// StariNalog je servisni nalog bez datuma završetka stariji od 14 dana
|
||||
type StariNalog struct {
|
||||
ID int64
|
||||
BrojNaloga string
|
||||
Uredjaj string
|
||||
KlijentNaziv string
|
||||
Status string
|
||||
DatumPrijema string
|
||||
DanaProslo int
|
||||
}
|
||||
|
||||
// TopArtikal je artikal rangiran po prodatoj količini
|
||||
type TopArtikal struct {
|
||||
Rang int
|
||||
Naziv string
|
||||
Kategorija string
|
||||
UkupnoKolicina int
|
||||
UkupnoPrihod float64
|
||||
}
|
||||
|
||||
// TopKlijent je klijent rangiran po ukupnoj vrednosti naloga
|
||||
type TopKlijent struct {
|
||||
Rang int
|
||||
Naziv string
|
||||
BrojNaloga int
|
||||
UkupnoVrednost float64
|
||||
}
|
||||
|
||||
// Izvestaji renderuje stranicu sa izveštajima
|
||||
func (h *Handler) Izvestaji(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
podesavanja, err := sqlite.DohvatiSvaPodesavanja(ctx, h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju podešavanja", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// --- mesečni prihod: prodaja ---
|
||||
prodajaPoMesecu := map[string]float64{}
|
||||
prodajaRed, err := h.DB.QueryContext(ctx, `
|
||||
SELECT substr(datum, 1, 7), SUM(ukupno)
|
||||
FROM prodajni_nalozi
|
||||
WHERE substr(datum, 1, 10) >= date('now', '-11 months', 'start of month')
|
||||
GROUP BY substr(datum, 1, 7)`)
|
||||
if err != nil {
|
||||
log.Printf("izvestaji: prihod prodaja: %v", err)
|
||||
} else {
|
||||
defer prodajaRed.Close()
|
||||
for prodajaRed.Next() {
|
||||
var mesec string
|
||||
var iznos float64
|
||||
if err := prodajaRed.Scan(&mesec, &iznos); err == nil {
|
||||
prodajaPoMesecu[mesec] = iznos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- mesečni prihod: servis ---
|
||||
servisPoMesecu := map[string]float64{}
|
||||
servisRed, err := h.DB.QueryContext(ctx, `
|
||||
SELECT substr(datum_zavrsetka, 1, 7), SUM(cena_konacna)
|
||||
FROM servisni_nalozi
|
||||
WHERE datum_zavrsetka IS NOT NULL
|
||||
AND substr(datum_zavrsetka, 1, 10) >= date('now', '-11 months', 'start of month')
|
||||
GROUP BY substr(datum_zavrsetka, 1, 7)`)
|
||||
if err != nil {
|
||||
log.Printf("izvestaji: prihod servis: %v", err)
|
||||
} else {
|
||||
defer servisRed.Close()
|
||||
for servisRed.Next() {
|
||||
var mesec string
|
||||
var iznos float64
|
||||
if err := servisRed.Scan(&mesec, &iznos); err == nil {
|
||||
servisPoMesecu[mesec] = iznos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gradimo niz za poslednjih 12 meseci (hronološki)
|
||||
sada := time.Now()
|
||||
var mesecniPrihodi []MesecniPrihod
|
||||
var grafikonLabele []string
|
||||
var grafikonProdaja []float64
|
||||
var grafikonServis []float64
|
||||
|
||||
for i := 11; i >= 0; i-- {
|
||||
t := sada.AddDate(0, -i, 0)
|
||||
kljuc := t.Format("2006-01")
|
||||
prod := prodajaPoMesecu[kljuc]
|
||||
serv := servisPoMesecu[kljuc]
|
||||
mesecniPrihodi = append(mesecniPrihodi, MesecniPrihod{
|
||||
MesecPrikaz: formatujMesec(kljuc),
|
||||
Prodaja: prod,
|
||||
Servis: serv,
|
||||
Ukupno: prod + serv,
|
||||
})
|
||||
grafikonLabele = append(grafikonLabele, formatujMesec(kljuc))
|
||||
grafikonProdaja = append(grafikonProdaja, prod)
|
||||
grafikonServis = append(grafikonServis, serv)
|
||||
}
|
||||
|
||||
type grafikonPodaci struct {
|
||||
Labele []string `json:"labele"`
|
||||
Prodaja []float64 `json:"prodaja"`
|
||||
Servis []float64 `json:"servis"`
|
||||
}
|
||||
jsonBytes, _ := json.Marshal(grafikonPodaci{
|
||||
Labele: grafikonLabele,
|
||||
Prodaja: grafikonProdaja,
|
||||
Servis: grafikonServis,
|
||||
})
|
||||
|
||||
// --- stari otvoreni nalozi (>14 dana bez završetka) ---
|
||||
stariRed, err := h.DB.QueryContext(ctx, `
|
||||
SELECT sn.id, sn.broj_naloga, sn.uredjaj, sn.status, sn.datum_prijema,
|
||||
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, '')), '—') AS klijent_naziv
|
||||
FROM servisni_nalozi sn
|
||||
LEFT JOIN klijenti k ON k.id = sn.klijent_id
|
||||
WHERE sn.datum_zavrsetka IS NULL
|
||||
AND substr(sn.datum_prijema, 1, 10) <= date('now', '-14 days')
|
||||
ORDER BY sn.datum_prijema ASC`)
|
||||
|
||||
var stariNalozi []StariNalog
|
||||
if err != nil {
|
||||
log.Printf("izvestaji: stari nalozi: %v", err)
|
||||
} else {
|
||||
defer stariRed.Close()
|
||||
for stariRed.Next() {
|
||||
var sn StariNalog
|
||||
var datumVreme time.Time
|
||||
if err := stariRed.Scan(&sn.ID, &sn.BrojNaloga, &sn.Uredjaj, &sn.Status, &datumVreme, &sn.KlijentNaziv); err == nil {
|
||||
sn.DatumPrijema = datumVreme.Format("02.01.2006.")
|
||||
sn.DanaProslo = int(time.Since(datumVreme).Hours() / 24)
|
||||
stariNalozi = append(stariNalozi, sn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- top 10 najprodavanijih artikala ---
|
||||
artRed, err := h.DB.QueryContext(ctx, `
|
||||
SELECT a.naziv, COALESCE(k.naziv, '—'), SUM(sp.kolicina), SUM(sp.ukupno)
|
||||
FROM stavke_prodaje sp
|
||||
JOIN artikli a ON a.id = sp.artikal_id
|
||||
LEFT JOIN kategorije k ON k.id = a.kategorija_id
|
||||
GROUP BY sp.artikal_id
|
||||
ORDER BY SUM(sp.kolicina) DESC
|
||||
LIMIT 10`)
|
||||
|
||||
var topArtikli []TopArtikal
|
||||
if err != nil {
|
||||
log.Printf("izvestaji: top artikli: %v", err)
|
||||
} else {
|
||||
defer artRed.Close()
|
||||
for artRed.Next() {
|
||||
var a TopArtikal
|
||||
if err := artRed.Scan(&a.Naziv, &a.Kategorija, &a.UkupnoKolicina, &a.UkupnoPrihod); err == nil {
|
||||
a.Rang = len(topArtikli) + 1
|
||||
topArtikli = append(topArtikli, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- top 10 klijenata po ukupnoj vrednosti ---
|
||||
klijRed, err := h.DB.QueryContext(ctx, `
|
||||
SELECT
|
||||
COALESCE(NULLIF(k.naziv_firme, ''), TRIM(COALESCE(k.ime, '') || ' ' || COALESCE(k.prezime, ''))) AS naziv,
|
||||
COALESCE(p.ukupno_prodaja, 0) + COALESCE(s.ukupno_servis, 0) AS ukupno_vrednost,
|
||||
COALESCE(p.broj_prodaja, 0) + COALESCE(s.broj_servisa, 0) AS broj_naloga
|
||||
FROM klijenti k
|
||||
LEFT JOIN (
|
||||
SELECT klijent_id, SUM(ukupno) AS ukupno_prodaja, COUNT(*) AS broj_prodaja
|
||||
FROM prodajni_nalozi GROUP BY klijent_id
|
||||
) p ON p.klijent_id = k.id
|
||||
LEFT JOIN (
|
||||
SELECT klijent_id, SUM(cena_konacna) AS ukupno_servis, COUNT(*) AS broj_servisa
|
||||
FROM servisni_nalozi WHERE cena_konacna IS NOT NULL GROUP BY klijent_id
|
||||
) s ON s.klijent_id = k.id
|
||||
WHERE COALESCE(p.ukupno_prodaja, 0) + COALESCE(s.ukupno_servis, 0) > 0
|
||||
ORDER BY ukupno_vrednost DESC
|
||||
LIMIT 10`)
|
||||
|
||||
var topKlijenti []TopKlijent
|
||||
if err != nil {
|
||||
log.Printf("izvestaji: top klijenti: %v", err)
|
||||
} else {
|
||||
defer klijRed.Close()
|
||||
for klijRed.Next() {
|
||||
var k TopKlijent
|
||||
if err := klijRed.Scan(&k.Naziv, &k.UkupnoVrednost, &k.BrojNaloga); err == nil {
|
||||
k.Rang = len(topKlijenti) + 1
|
||||
topKlijenti = append(topKlijenti, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
podaci := PodaciIzvestaja{
|
||||
PodaciStranice: model.PodaciStranice{
|
||||
Stranica: "izvestaji",
|
||||
NaslovStranice: "Izveštaji",
|
||||
Tema: podesavanja["tema"],
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
LogoTip: podesavanja["logo_tip"],
|
||||
LogoPutanja: podesavanja["logo_putanja"],
|
||||
Korisnik: "Admin",
|
||||
},
|
||||
MesecniPrihodi: mesecniPrihodi,
|
||||
GrafikonJSON: template.JS(jsonBytes),
|
||||
StariNalozi: stariNalozi,
|
||||
TopArtikli: topArtikli,
|
||||
TopKlijenti: topKlijenti,
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(
|
||||
"web/templates/teme/podrazumevana/base.html",
|
||||
"web/templates/komponente/sidebar.html",
|
||||
"web/templates/komponente/topbar.html",
|
||||
"web/templates/stranice/izvestaji.html",
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "Greška pri učitavanju stranice", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "base", podaci); err != nil {
|
||||
http.Error(w, "Greška pri prikazu stranice", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user