Servis predlog delovi: lager integracija, brisanje predloženih delova, odluka klijenta (migracija 079)
This commit is contained in:
@@ -178,6 +178,7 @@ type KlijentRepository interface {
|
||||
ListaFilter(ctx context.Context, filter KlijentFilter) ([]model.Klijent, error)
|
||||
PrebrojiPoFilteru(ctx context.Context, filter KlijentFilter) (int, error)
|
||||
DohvatiID(ctx context.Context, id int64) (*model.Klijent, error)
|
||||
Pronadji(ctx context.Context, tip, ime, prezime, nazivFirme, jmbg, telefon, email, mesto string) (*model.Klijent, error)
|
||||
Kreiraj(ctx context.Context, k *model.Klijent) (int64, error)
|
||||
Izmeni(ctx context.Context, k *model.Klijent) error
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
@@ -201,6 +202,8 @@ type ServisRepository interface {
|
||||
AzurirajCenaDijagnostike(ctx context.Context, id int64, cena float64) error
|
||||
OdbijPopravku(ctx context.Context, id int64, cena float64) error
|
||||
AzurirajKomentarKlijenta(ctx context.Context, id int64, tekst string) error
|
||||
SacuvajOdlukuKlijenta(ctx context.Context, id int64, odluka string, odgovor string) error
|
||||
ObrisiOdlukuKlijenta(ctx context.Context, id int64) error
|
||||
Obrisi(ctx context.Context, id int64, korisnikID *int64) error
|
||||
SledeciBroj(ctx context.Context) (string, error)
|
||||
}
|
||||
@@ -236,6 +239,7 @@ type ServisniPotrazivaniDeloviRepository interface {
|
||||
DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniPotrazivaniDeo, error)
|
||||
Obrisi(ctx context.Context, id int64) error
|
||||
ObrisiZaArtikal(ctx context.Context, nalogID, artikalID int64) error
|
||||
ObrisiPredlozeneZaArtikal(ctx context.Context, nalogID, artikalID int64) error
|
||||
// ProveriIPocistiZaArtikal proverava potraživane redove za dati artikal nakon
|
||||
// povećanja stanja i briše/smanjuje redove koji se mogu pokriti (FIFO).
|
||||
// Vraća ID-eve naloga čiji je poslednji potraživani red obrisan.
|
||||
|
||||
@@ -3,6 +3,7 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"ntech/internal/db"
|
||||
@@ -96,6 +97,115 @@ func (r *KlijentRepo) DohvatiID(ctx context.Context, id int64) (*model.Klijent,
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
// Pronadji traži postojećeg klijenta po imenu i prezimenu (fizičko) ili nazivu firme (pravno).
|
||||
// Vraća nil, nil ako nije pronađen.
|
||||
// Pretraga ide od najočiglednijeg ka opštijem: JMBG/PIB → ime+prezime+mesto → telefon → email → samo ime+prezime.
|
||||
func (r *KlijentRepo) Pronadji(ctx context.Context, tip, ime, prezime, nazivFirme, jmbg, telefon, email, mesto string) (*model.Klijent, error) {
|
||||
var k model.Klijent
|
||||
var imeN, prezimeN, jmbgN, tipIdent, nazivFirmeN, pibN, telefonN, emailN, mestoN, napomena sql.NullString
|
||||
|
||||
skeniraj := func(row *sql.Row) error {
|
||||
return row.Scan(
|
||||
&k.ID, &k.Tip, &imeN, &prezimeN, &jmbgN, &tipIdent, &nazivFirmeN, &pibN, &telefonN, &emailN, &mestoN, &napomena, &k.DatumUnosa,
|
||||
)
|
||||
}
|
||||
|
||||
var row *sql.Row
|
||||
|
||||
if tip == "pravno" {
|
||||
// 1. PIB (najpreciznije za firmu)
|
||||
if jmbg != "" {
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip = 'pravno' AND pib = ? LIMIT 1`, jmbg)
|
||||
if err := skeniraj(row); err == nil {
|
||||
goto popuni
|
||||
}
|
||||
}
|
||||
// 2. Naziv firme + mesto
|
||||
if nazivFirme != "" && mesto != "" {
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip = 'pravno' AND naziv_firme = ? AND mesto = ? LIMIT 1`, nazivFirme, mesto)
|
||||
if err := skeniraj(row); err == nil {
|
||||
goto popuni
|
||||
}
|
||||
}
|
||||
// 3. Samo naziv firme
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip = 'pravno' AND naziv_firme = ? LIMIT 1`, nazivFirme)
|
||||
if err := skeniraj(row); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Fizičko lice — više nivoa
|
||||
// 1. JMBG ili broj lične karte
|
||||
if jmbg != "" {
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip != 'pravno' AND jmbg = ? LIMIT 1`, jmbg)
|
||||
if err := skeniraj(row); err == nil {
|
||||
goto popuni
|
||||
}
|
||||
}
|
||||
// 2. Ime + prezime + mesto
|
||||
if ime != "" && prezime != "" && mesto != "" {
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND mesto = ? LIMIT 1`, ime, prezime, mesto)
|
||||
if err := skeniraj(row); err == nil {
|
||||
goto popuni
|
||||
}
|
||||
}
|
||||
// 3. Ime + prezime + telefon
|
||||
if ime != "" && prezime != "" && telefon != "" {
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND telefon = ? LIMIT 1`, ime, prezime, telefon)
|
||||
if err := skeniraj(row); err == nil {
|
||||
goto popuni
|
||||
}
|
||||
}
|
||||
// 4. Ime + prezime + email
|
||||
if ime != "" && prezime != "" && email != "" {
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? AND email = ? LIMIT 1`, ime, prezime, email)
|
||||
if err := skeniraj(row); err == nil {
|
||||
goto popuni
|
||||
}
|
||||
}
|
||||
// 5. Samo ime + prezime (poslednji fallback)
|
||||
row = r.db.QueryRowContext(ctx, `
|
||||
SELECT id, tip, ime, prezime, jmbg, tip_identifikacije, naziv_firme, pib, telefon, email, mesto, napomena, datum_unosa
|
||||
FROM klijenti WHERE tip != 'pravno' AND ime = ? AND prezime = ? LIMIT 1`, ime, prezime)
|
||||
if err := skeniraj(row); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("ntech: KlijentRepo.Pronadji: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
popuni:
|
||||
k.Ime = imeN.String
|
||||
k.Prezime = prezimeN.String
|
||||
k.JMBG = jmbgN.String
|
||||
k.TipIdentifikacije = tipIdent.String
|
||||
k.NazivFirme = nazivFirmeN.String
|
||||
k.PIB = pibN.String
|
||||
k.Telefon = telefonN.String
|
||||
k.Email = emailN.String
|
||||
k.Mesto = mestoN.String
|
||||
k.Napomena = napomena.String
|
||||
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
// Kreiraj dodaje novog klijenta u bazu
|
||||
func (r *KlijentRepo) Kreiraj(ctx context.Context, k *model.Klijent) (int64, error) {
|
||||
if k.Tip == "" {
|
||||
|
||||
@@ -58,10 +58,11 @@ func (r *ServisRepo) Lista(ctx context.Context, pretraga, status string) ([]mode
|
||||
sn.id, sn.klijent_id, sn.tehnicar_id, sn.broj_naloga, sn.uredjaj, sn.serijski_broj,
|
||||
sn.opis_kvara, sn.trazene_nadogradnje, sn.status, sn.cena_od, sn.cena_do, sn.cena_konacna,
|
||||
sn.avans, sn.napomena, sn.garancija_do, sn.garancija_dana, sn.datum_prijema, sn.datum_zavrsetka, sn.predvidjen_datum,
|
||||
sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.napomena_klijentu, sn.nalaz_dijagnostike, sn.uradjeno, sn.cena_dijagnostike, sn.popravka_odbijena, sn.javni_token, sn.komentar_klijenta,
|
||||
sn.ostecenja, sn.pin_uredjaja, sn.pribor, sn.napomena_klijentu, sn.nalaz_dijagnostike, sn.uradjeno, sn.cena_dijagnostike, sn.popravka_odbijena, sn.javni_token, sn.komentar_klijenta, sn.odluka_klijenta, sn.datum_odluke,
|
||||
COALESCE(kp.naziv, '') AS klijent_naziv,
|
||||
(EXISTS(SELECT 1 FROM servis_radovi sr WHERE sr.nalog_id = sn.id AND sr.predlozeno = 1)
|
||||
OR EXISTS(SELECT 1 FROM servisni_delovi sd WHERE sd.nalog_id = sn.id AND sd.predlozeno = 1)) AS ima_predlog
|
||||
OR EXISTS(SELECT 1 FROM servisni_delovi sd WHERE sd.nalog_id = sn.id AND sd.predlozeno = 1)
|
||||
OR EXISTS(SELECT 1 FROM servisni_potrazivani_delovi spd WHERE spd.nalog_id = sn.id AND spd.predlozeno = 1)) AS ima_predlog
|
||||
FROM servisni_nalozi sn
|
||||
LEFT JOIN klijent_prikaz kp ON kp.id = sn.klijent_id
|
||||
WHERE 1=1`
|
||||
@@ -107,7 +108,7 @@ func (r *ServisRepo) DohvatiID(ctx context.Context, id int64) (*model.ServisniNa
|
||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta, odluka_klijenta, datum_odluke
|
||||
FROM servisni_nalozi WHERE id = ?`, id)
|
||||
|
||||
var n model.ServisniNalog
|
||||
@@ -159,7 +160,7 @@ func (r *ServisRepo) DohvatiJavniToken(ctx context.Context, token string) (*mode
|
||||
id, klijent_id, tehnicar_id, broj_naloga, uredjaj, serijski_broj,
|
||||
opis_kvara, trazene_nadogradnje, status, cena_od, cena_do, cena_konacna,
|
||||
avans, napomena, garancija_do, garancija_dana, datum_prijema, datum_zavrsetka, predvidjen_datum,
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta
|
||||
ostecenja, pin_uredjaja, pribor, napomena_klijentu, nalaz_dijagnostike, uradjeno, cena_dijagnostike, popravka_odbijena, javni_token, komentar_klijenta, odluka_klijenta, datum_odluke
|
||||
FROM servisni_nalozi WHERE javni_token = ?`, token)
|
||||
|
||||
var n model.ServisniNalog
|
||||
@@ -349,6 +350,30 @@ func (r *ServisRepo) AzurirajKomentarKlijenta(ctx context.Context, id int64, tek
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObrisiOdlukuKlijenta poništava prethodnu odluku klijenta (npr. kad se doda novi predlog).
|
||||
func (r *ServisRepo) ObrisiOdlukuKlijenta(ctx context.Context, id int64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"UPDATE servisni_nalozi SET odluka_klijenta = NULL, komentar_klijenta = NULL, datum_odluke = NULL WHERE id = ?", id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.ObrisiOdlukuKlijenta: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SacuvajOdlukuKlijenta beleži odluku klijenta (prihvaceno/odbijeno) i poruku.
|
||||
// Ako je prihvaćeno, poziva PrihvatiPredlozene za delove i radove.
|
||||
func (r *ServisRepo) SacuvajOdlukuKlijenta(ctx context.Context, id int64, odluka string, odgovor string) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"UPDATE servisni_nalozi SET odluka_klijenta = ?, komentar_klijenta = ?, datum_odluke = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
odluka, odgovor, id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisRepo.SacuvajOdlukuKlijenta: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Obrisi briše servisni nalog po ID-u
|
||||
// Obrisi briše servisni nalog i vraća ugrađene delove na stanje u magacinu (u transakciji)
|
||||
func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) error {
|
||||
@@ -430,9 +455,9 @@ func (r *ServisRepo) Obrisi(ctx context.Context, id int64, korisnikID *int64) er
|
||||
// klijentNaziv je opcioni pokazivač, nil kada se čita bez JOIN-a
|
||||
func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *string, imaPredlog *bool) error {
|
||||
var klijentID, tehnicarID sql.NullInt64
|
||||
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, napomenaKlijentu, nalazDijagnostike, uradjeno, javniToken, komentarKlijenta sql.NullString
|
||||
var serijskiBroj, napomena, ostecenja, pinUredjaja, pribor, napomenaKlijentu, nalazDijagnostike, uradjeno, javniToken, komentarKlijenta, odlukaKlijenta sql.NullString
|
||||
var cenaOd, cenaDo, cenaKonacna, avans sql.NullFloat64
|
||||
var garancijaDo, datumZavrsetka, predvidjenDatum sql.NullTime
|
||||
var garancijaDo, datumZavrsetka, predvidjenDatum, datumOdluke sql.NullTime
|
||||
var garancijaDana sql.NullInt64
|
||||
var popravkaOdbijena sql.NullInt64
|
||||
|
||||
@@ -440,7 +465,7 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
&n.ID, &klijentID, &tehnicarID, &n.BrojNaloga, &n.Uredjaj, &serijskiBroj,
|
||||
&n.OpisKvara, &n.TrazeneNadogradnje, &n.Status, &cenaOd, &cenaDo, &cenaKonacna,
|
||||
&avans, &napomena, &garancijaDo, &garancijaDana, &n.DatumPrijema, &datumZavrsetka, &predvidjenDatum,
|
||||
&ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken, &komentarKlijenta,
|
||||
&ostecenja, &pinUredjaja, &pribor, &napomenaKlijentu, &nalazDijagnostike, &uradjeno, &n.CenaDijagnostike, &popravkaOdbijena, &javniToken, &komentarKlijenta, &odlukaKlijenta, &datumOdluke,
|
||||
}
|
||||
|
||||
if klijentNaziv != nil {
|
||||
@@ -505,6 +530,11 @@ func scanNalog(scan func(...any) error, n *model.ServisniNalog, klijentNaziv *st
|
||||
n.PopravkaOdbijena = popravkaOdbijena.Int64 != 0
|
||||
n.JavniToken = javniToken.String
|
||||
n.KomentarKlijenta = komentarKlijenta.String
|
||||
n.OdlukaKlijenta = odlukaKlijenta.String
|
||||
if datumOdluke.Valid {
|
||||
v := datumOdluke.Time
|
||||
n.DatumOdluke = &v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"ntech/internal/model"
|
||||
)
|
||||
@@ -23,7 +24,7 @@ func NoviServisniDeloviRepo(db *sql.DB) *ServisniDeloviRepo {
|
||||
func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniDeoSaArtiklom, error) {
|
||||
redovi, err := r.db.QueryContext(ctx, `
|
||||
SELECT sd.id, sd.nalog_id, sd.artikal_id, sd.kolicina, sd.cena_komada, sd.datum,
|
||||
sd.predlozeno, a.naziv
|
||||
sd.predlozeno, a.naziv, a.sifra
|
||||
FROM servisni_delovi sd
|
||||
JOIN artikli a ON a.id = sd.artikal_id
|
||||
WHERE sd.nalog_id = ?
|
||||
@@ -38,7 +39,7 @@ func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
||||
var d model.ServisniDeoSaArtiklom
|
||||
err := redovi.Scan(
|
||||
&d.ID, &d.NalogID, &d.ArtikalID, &d.Kolicina, &d.CenaKomada, &d.Datum,
|
||||
&d.Predlozeno, &d.ArtikalNaziv,
|
||||
&d.Predlozeno, &d.ArtikalNaziv, &d.ArtikalSifra,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniDeloviRepo.DohvatiZaNalog: scan: %w", err)
|
||||
@@ -53,25 +54,52 @@ func (r *ServisniDeloviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
||||
// stanju, a višak beleži u potraživane delove. Stanje se čita i koristi unutar iste
|
||||
// transakcije pa lager NIKAD ne ide u minus (nema TOCTOU između čitanja i upisa).
|
||||
// Vraća koliko je ugrađeno i koliko nedostaje.
|
||||
//
|
||||
// Predloženi delovi (predlozeno=true) ne skidaju sa lagera — sve ide u potraživane
|
||||
// do odluke klijenta. Tek PrihvatiPredlozene skida sa lagera.
|
||||
func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64, predlozeno bool) (ugradjeno, nedostaje int, err error) {
|
||||
predInt := 0
|
||||
if predlozeno {
|
||||
predInt = 1
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
ugradjeno, nedostaje, err = ugradiIliPotrazujTx(ctx, tx, nalogID, artikalID, kolicina, cenaKomada, korisnikID, predlozeno)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: commit: %w", err)
|
||||
}
|
||||
return ugradjeno, nedostaje, nil
|
||||
}
|
||||
|
||||
// ugradiIliPotrazujTx je interna verzija koja radi unutar postojeće transakcije.
|
||||
func ugradiIliPotrazujTx(ctx context.Context, tx *sql.Tx, nalogID, artikalID int64, kolicina int, cenaKomada float64, korisnikID *int64, predlozeno bool) (ugradjeno, nedostaje int, err error) {
|
||||
// Predloženi delovi: ne skidaju sa lagera, svaki predlog je poseban red (ne merge)
|
||||
if predlozeno {
|
||||
slog.Info("PREDLOG_INSERT", "nalogID", nalogID, "artikalID", artikalID, "kolicina", kolicina)
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (?, ?, ?, ?, 1)",
|
||||
nalogID, artikalID, kolicina, cenaKomada,
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("PREDLOG_INSERT_ERR", "err", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: predlozeni: %w", err)
|
||||
}
|
||||
slog.Info("PREDLOG_INSERT_OK")
|
||||
return 0, kolicina, nil
|
||||
}
|
||||
|
||||
// Ugrađeni delovi: standardna logika — skidamo sa lagera
|
||||
var stanjePre int
|
||||
if err = tx.QueryRowContext(ctx,
|
||||
"SELECT kolicina FROM artikli WHERE id = ?", artikalID,
|
||||
).Scan(&stanjePre); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: dohvati stanje: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: dohvati stanje: %w", err)
|
||||
}
|
||||
|
||||
// ugrađujemo najviše onoliko koliko fizički imamo; ostatak ide u potraživane
|
||||
ugradjeno = kolicina
|
||||
if ugradjeno > stanjePre {
|
||||
ugradjeno = stanjePre
|
||||
@@ -81,75 +109,69 @@ func (r *ServisniDeloviRepo) UgradiIliPotrazuj(ctx context.Context, nalogID, art
|
||||
}
|
||||
nedostaje = kolicina - ugradjeno
|
||||
|
||||
// ugradi ono što imamo: merge u servisni_delovi, skini sa magacina, zabeleži promenu
|
||||
if ugradjeno > 0 {
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
errRed := tx.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = ?",
|
||||
nalogID, artikalID, predInt,
|
||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = 0",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
if errRed == nil {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"UPDATE servisni_delovi SET kolicina = ?, cena_komada = ? WHERE id = ?",
|
||||
postojeciKol+ugradjeno, cenaKomada, postojeciID,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: merge: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: merge: %w", err)
|
||||
}
|
||||
} else if errors.Is(errRed, sql.ErrNoRows) {
|
||||
if _, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
nalogID, artikalID, ugradjeno, cenaKomada, predInt,
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO servisni_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (?, ?, ?, ?, 0)",
|
||||
nalogID, artikalID, ugradjeno, cenaKomada,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: insert: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: insert: %w", err)
|
||||
}
|
||||
} else {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: proveri: %w", errRed)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: proveri: %w", errRed)
|
||||
}
|
||||
|
||||
stanjePosle := stanjePre - ugradjeno
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"UPDATE artikli SET kolicina = ? WHERE id = ?", stanjePosle, artikalID,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: update stanje: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: update stanje: %w", err)
|
||||
}
|
||||
if err = zabeleziMagacinPromenu(ctx, tx, artikalID, model.PromenaIzlazServis,
|
||||
-ugradjeno, stanjePre, stanjePosle, nalogID, korisnikID, ""); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: magacin: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: magacin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// višak → potraživani delovi (ne skidamo sa lagera dok ne stigne)
|
||||
if nedostaje > 0 {
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
errRed := tx.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = ?",
|
||||
nalogID, artikalID, predInt,
|
||||
"SELECT id, kolicina FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = 0",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
if errRed == nil {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"UPDATE servisni_potrazivani_delovi SET kolicina = ?, cena_komada = ? WHERE id = ?",
|
||||
postojeciKol+nedostaje, cenaKomada, postojeciID,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani update: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: potraživani update: %w", err)
|
||||
}
|
||||
} else if errors.Is(errRed, sql.ErrNoRows) {
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (?, ?, ?, ?, ?)",
|
||||
nalogID, artikalID, nedostaje, cenaKomada, predInt,
|
||||
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (?, ?, ?, ?, 0)",
|
||||
nalogID, artikalID, nedostaje, cenaKomada,
|
||||
); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani insert: %w", err)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: potraživani insert: %w", err)
|
||||
}
|
||||
} else {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: potraživani proveri: %w", errRed)
|
||||
return 0, 0, fmt.Errorf("ntech: ugradiIliPotrazujTx: potraživani proveri: %w", errRed)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return 0, 0, fmt.Errorf("ntech: ServisniDeloviRepo.UgradiIliPotrazuj: commit: %w", err)
|
||||
}
|
||||
return ugradjeno, nedostaje, nil
|
||||
}
|
||||
|
||||
@@ -207,6 +229,7 @@ func (r *ServisniDeloviRepo) Obrisi(ctx context.Context, id int64, korisnikID *i
|
||||
|
||||
// PrihvatiPredlozene prebacuje sve predložene delove (i potraživane) naloga u
|
||||
// ugrađene (predlozeno → 0) — poziva se kad klijent prihvati predlog popravke.
|
||||
// Sada zaista skida sa lagera: predloženi delovi prethodno nisu dirali lager.
|
||||
func (r *ServisniDeloviRepo) PrihvatiPredlozene(ctx context.Context, nalogID int64) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
@@ -214,15 +237,51 @@ func (r *ServisniDeloviRepo) PrihvatiPredlozene(ctx context.Context, nalogID int
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"UPDATE servisni_delovi SET predlozeno = 0 WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||
); err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: delovi: %w", err)
|
||||
// Dohvati sve predložene potraživane delove
|
||||
redovi, err := tx.QueryContext(ctx,
|
||||
"SELECT id, artikal_id, kolicina, cena_komada FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND predlozeno = 1",
|
||||
nalogID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: dohvati predlozene: %w", err)
|
||||
}
|
||||
defer redovi.Close()
|
||||
|
||||
type predlozeniStavka struct {
|
||||
id int64
|
||||
artikalID int64
|
||||
kolicina int
|
||||
cenaKomada float64
|
||||
}
|
||||
var stavke []predlozeniStavka
|
||||
for redovi.Next() {
|
||||
var s predlozeniStavka
|
||||
if err := redovi.Scan(&s.id, &s.artikalID, &s.kolicina, &s.cenaKomada); err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: scan: %w", err)
|
||||
}
|
||||
stavke = append(stavke, s)
|
||||
}
|
||||
|
||||
// Za svaki predloženi deo: probaj da ugradiš (skine sa lagera koliko može)
|
||||
for _, s := range stavke {
|
||||
_, _, err := ugradiIliPotrazujTx(ctx, tx, nalogID, s.artikalID, s.kolicina, s.cenaKomada, nil, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: ugradi: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Obriši predložene potraživane (prebačeni su u ugrađene/potraživane bez predlozeno)
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"UPDATE servisni_potrazivani_delovi SET predlozeno = 0 WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||
"DELETE FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||
); err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: potraživani: %w", err)
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: obrisi potraživane: %w", err)
|
||||
}
|
||||
|
||||
// Očisti i servisni_delovi sa predlozeno=1 (zaostali iz starije verzije)
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
"DELETE FROM servisni_delovi WHERE nalog_id = ? AND predlozeno = 1", nalogID,
|
||||
); err != nil {
|
||||
return fmt.Errorf("ntech: ServisniDeloviRepo.PrihvatiPredlozene: obrisi delove: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
|
||||
@@ -54,6 +54,18 @@ func (r *ServisniPotrazivaniDeloviRepo) Obrisi(ctx context.Context, id int64) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObrisiPredlozeneZaArtikal briše samo predlozene redove (predlozeno=1) za dati artikal na nalogu
|
||||
func (r *ServisniPotrazivaniDeloviRepo) ObrisiPredlozeneZaArtikal(ctx context.Context, nalogID, artikalID int64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
"DELETE FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = 1",
|
||||
nalogID, artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ObrisiPredlozeneZaArtikal: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObrisiZaArtikal briše sve potraživane delove za dati artikal na datom nalogu
|
||||
func (r *ServisniPotrazivaniDeloviRepo) ObrisiZaArtikal(ctx context.Context, nalogID, artikalID int64) error {
|
||||
_, err := r.db.ExecContext(ctx,
|
||||
@@ -96,7 +108,7 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con
|
||||
}
|
||||
|
||||
redovi, err := tx.QueryContext(ctx,
|
||||
"SELECT id, nalog_id, kolicina, cena_komada FROM servisni_potrazivani_delovi WHERE artikal_id = ? ORDER BY datum",
|
||||
"SELECT id, nalog_id, kolicina, cena_komada FROM servisni_potrazivani_delovi WHERE artikal_id = ? AND predlozeno = 0 ORDER BY datum",
|
||||
artikalID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -165,7 +177,7 @@ func (r *ServisniPotrazivaniDeloviRepo) ProveriIPocistiZaArtikal(ctx context.Con
|
||||
for nalogID := range obrisaniNalozi {
|
||||
var preostalo int
|
||||
if err := tx.QueryRowContext(ctx,
|
||||
"SELECT COUNT(*) FROM servisni_potrazivani_delovi WHERE nalog_id = ?", nalogID,
|
||||
"SELECT COUNT(*) FROM servisni_potrazivani_delovi WHERE nalog_id = ? AND predlozeno = 0", nalogID,
|
||||
).Scan(&preostalo); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniPotrazivaniDeloviRepo.ProveriIPocistiZaArtikal: prebroji: %w", err)
|
||||
}
|
||||
@@ -187,7 +199,7 @@ func (r *ServisniPotrazivaniDeloviRepo) ugradiUNalog(ctx context.Context, tx *sq
|
||||
var postojeciID int64
|
||||
var postojeciKol int
|
||||
err := tx.QueryRowContext(ctx,
|
||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ?",
|
||||
"SELECT id, kolicina FROM servisni_delovi WHERE nalog_id = ? AND artikal_id = ? AND predlozeno = 0",
|
||||
nalogID, artikalID,
|
||||
).Scan(&postojeciID, &postojeciKol)
|
||||
|
||||
|
||||
@@ -124,6 +124,50 @@ func TestPotrazivaniDelimicnoPokrivanje(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPotrazivaniPredlozeniSeNePokrivaju: predloženi delovi (predlozeno=1) ne smeju
|
||||
// biti auto-konvertovani u ugrađene niti smeti otključavanje naloga.
|
||||
func TestPotrazivaniPredlozeniSeNePokrivaju(t *testing.T) {
|
||||
db := testDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if _, err := db.ExecContext(ctx, "INSERT INTO artikli (id, naziv, kolicina) VALUES (1, 'GPU', 5)"); err != nil {
|
||||
t.Fatalf("insert artikal: %v", err)
|
||||
}
|
||||
if _, err := db.ExecContext(ctx,
|
||||
"INSERT INTO servisni_nalozi (id, broj_naloga, uredjaj, opis_kvara, status) VALUES (1, 'SN-1', 'PC', 'kvar', 'U dijagnostici')",
|
||||
); err != nil {
|
||||
t.Fatalf("insert nalog: %v", err)
|
||||
}
|
||||
// predloženi deo serviser predlaže klijentu (predlozeno=1) — ne sme biti auto-ugradjen
|
||||
if _, err := db.ExecContext(ctx,
|
||||
"INSERT INTO servisni_potrazivani_delovi (nalog_id, artikal_id, kolicina, cena_komada, predlozeno) VALUES (1, 1, 3, 100, 1)",
|
||||
); err != nil {
|
||||
t.Fatalf("insert predlozeni: %v", err)
|
||||
}
|
||||
|
||||
potrRepo := NoviServisniPotrazivaniDeloviRepo(db)
|
||||
otkljucani, err := potrRepo.ProveriIPocistiZaArtikal(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("ProveriIPocistiZaArtikal: %v", err)
|
||||
}
|
||||
// nalog se NE otključava — predlozeni redovi se ignorišu
|
||||
if len(otkljucani) != 0 {
|
||||
t.Fatalf("nalog ne sme da se otključa zbog predlozenih redova, dobijeno %v", otkljucani)
|
||||
}
|
||||
// predlozeni red mora ostati nepromenjen
|
||||
if k := skalarInt(t, db, "SELECT kolicina FROM servisni_potrazivani_delovi WHERE nalog_id=1 AND predlozeno=1"); k != 3 {
|
||||
t.Fatalf("predlozeni deo mora ostati 3, dobijeno %d", k)
|
||||
}
|
||||
// magacin se ne sme dirljati
|
||||
if stanje := skalarInt(t, db, "SELECT kolicina FROM artikli WHERE id=1"); stanje != 5 {
|
||||
t.Fatalf("stanje magacina mora ostati 5, dobijeno %d", stanje)
|
||||
}
|
||||
// u servisni_delovi ne sme biti ništa ubačeno
|
||||
if k := skalarInt(t, db, "SELECT COUNT(*) FROM servisni_delovi WHERE nalog_id=1"); k != 0 {
|
||||
t.Fatalf("servisni_delovi mora biti prazan, dobijeno %d", k)
|
||||
}
|
||||
}
|
||||
|
||||
// skalarInt vraća jednu celobrojnu vrednost iz upita (za proveru stanja u testu)
|
||||
func skalarInt(t *testing.T, db *sql.DB, upit string) int {
|
||||
t.Helper()
|
||||
|
||||
@@ -21,8 +21,11 @@ func NoviServisniRadoviRepo(baza *sql.DB) *ServisniRadoviRepo {
|
||||
// DohvatiZaNalog vraća sve radove (usluge) jednog naloga, redom unosa
|
||||
func (r *ServisniRadoviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64) ([]model.ServisniRad, error) {
|
||||
redovi, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, nalog_id, COALESCE(usluga_id, 0), naziv, kolicina, cena_komada, datum, predlozeno
|
||||
FROM servis_radovi WHERE nalog_id = ? ORDER BY id`, nalogID)
|
||||
SELECT sr.id, sr.nalog_id, COALESCE(sr.usluga_id, 0), sr.naziv, sr.kolicina, sr.cena_komada, sr.datum, sr.predlozeno,
|
||||
COALESCE(u.sifra, '')
|
||||
FROM servis_radovi sr
|
||||
LEFT JOIN usluge u ON u.id = sr.usluga_id
|
||||
WHERE sr.nalog_id = ? ORDER BY sr.id`, nalogID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniRadoviRepo.DohvatiZaNalog: %w", err)
|
||||
}
|
||||
@@ -31,7 +34,7 @@ func (r *ServisniRadoviRepo) DohvatiZaNalog(ctx context.Context, nalogID int64)
|
||||
var radovi []model.ServisniRad
|
||||
for redovi.Next() {
|
||||
var rad model.ServisniRad
|
||||
if err := redovi.Scan(&rad.ID, &rad.NalogID, &rad.UslugaID, &rad.Naziv, &rad.Kolicina, &rad.CenaKomada, &rad.Datum, &rad.Predlozeno); err != nil {
|
||||
if err := redovi.Scan(&rad.ID, &rad.NalogID, &rad.UslugaID, &rad.Naziv, &rad.Kolicina, &rad.CenaKomada, &rad.Datum, &rad.Predlozeno, &rad.Sifra); err != nil {
|
||||
return nil, fmt.Errorf("ntech: ServisniRadoviRepo.DohvatiZaNalog: %w", err)
|
||||
}
|
||||
radovi = append(radovi, rad)
|
||||
|
||||
+148
-61
@@ -535,8 +535,11 @@ func (h *Handler) DetaljiNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
for _, nalogID := range otkljucani {
|
||||
if err := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusPrimljeno); err != nil {
|
||||
slog.Error("self-heal reset statusa naloga nije uspeo", "nalog_id", nalogID, "error", err)
|
||||
// reset samo ako je nalog čekao delove — ne sme da menja U dijagnostici, U popravci itd.
|
||||
if tekuci, e := h.ServisRepo.DohvatiID(r.Context(), nalogID); e == nil && tekuci != nil && tekuci.Status == model.StatusCekaDelove {
|
||||
if err := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusPrimljeno); err != nil {
|
||||
slog.Error("self-heal reset statusa naloga nije uspeo", "nalog_id", nalogID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,11 +750,13 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
imeArtikla = art.Naziv
|
||||
}
|
||||
|
||||
// forma „Predloženi delovi" šalje predlozeno=1; forma „Ugrađeni delovi" ne šalje
|
||||
predlozeno := r.FormValue("predlozeno") == "1"
|
||||
|
||||
// predlog ako status nije „Primljeno" (forma više ne određuje — server odlučuje)
|
||||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), nalogID)
|
||||
predlozeno := nalog != nil && nalog.Status != model.StatusPrimljeno
|
||||
slog.Info("DODAJ_DEO_IN", "status", nalog.Status, "predlozeno", predlozeno, "kol", kolicina)
|
||||
// atomično: ugradi ono što imamo (skida sa lagera, ne ide u minus), višak u potraživane
|
||||
ugradjeno, nedostaje, err := h.ServisniDeloviRepo.UgradiIliPotrazuj(r.Context(), nalogID, artikalID, kolicina, cena, &k.ID, predlozeno)
|
||||
slog.Info("DODAJ_DEO_OUT", "ugradjeno", ugradjeno, "nedostaje", nedostaje, "err", err)
|
||||
if err != nil {
|
||||
slog.Error("greška pri dodavanju dela", "error", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri dodavanju dela.")
|
||||
@@ -759,8 +764,9 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// ako nešto nedostaje, prebaci nalog u „Čeka delove" i obavesti korisnika
|
||||
if nedostaje > 0 {
|
||||
// ako nešto nedostaje kod ugrađenog dela, prebaci nalog u „Čeka delove".
|
||||
// Za predložene delove ne menjamo status — to su samo predlozi, ne blokiraju rad.
|
||||
if nedostaje > 0 && !predlozeno {
|
||||
if e := h.ServisRepo.AzurirajStatus(r.Context(), nalogID, model.StatusCekaDelove); e != nil {
|
||||
slog.Error("greška pri prebacivanju naloga u Čeka delove", "error", e)
|
||||
}
|
||||
@@ -775,6 +781,16 @@ func (h *Handler) DodajDeloNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if nedostaje > 0 && predlozeno {
|
||||
middleware.SetFlash(w, r, h.DB, "uspeh",
|
||||
"Predlog sačuvan — "+strconv.Itoa(kolicina)+" kom. „"+imeArtikla+
|
||||
"\" čeka odobrenje klijenta.")
|
||||
// novi predlog poništava prethodnu odluku klijenta
|
||||
if err := h.ServisRepo.ObrisiOdlukuKlijenta(r.Context(), nalogID); err != nil {
|
||||
slog.Error("greška pri brisanju odluke klijenta", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -795,26 +811,43 @@ func (h *Handler) deloviSaPotrazivanima(ctx context.Context, nalogID int64) ([]m
|
||||
artikalID int64
|
||||
predlozeno bool
|
||||
}
|
||||
potrMap := make(map[kljucDela]int)
|
||||
type potrVrednost struct {
|
||||
kolicina int
|
||||
cena float64
|
||||
}
|
||||
potrMap := make(map[kljucDela]potrVrednost)
|
||||
for _, p := range potrazivani {
|
||||
potrMap[kljucDela{p.ArtikalID, p.Predlozeno}] += p.Kolicina
|
||||
k := kljucDela{p.ArtikalID, p.Predlozeno}
|
||||
v := potrMap[k]
|
||||
v.kolicina += p.Kolicina
|
||||
if v.cena == 0 && p.CenaKomada > 0 {
|
||||
v.cena = p.CenaKomada
|
||||
}
|
||||
potrMap[k] = v
|
||||
}
|
||||
for i := range delovi {
|
||||
k := kljucDela{delovi[i].ArtikalID, delovi[i].Predlozeno}
|
||||
if kol, ok := potrMap[k]; ok {
|
||||
delovi[i].Potrazivano = kol
|
||||
if v, ok := potrMap[k]; ok {
|
||||
delovi[i].Potrazivano = v.kolicina
|
||||
delete(potrMap, k)
|
||||
}
|
||||
}
|
||||
for k, kol := range potrMap {
|
||||
for k, v := range potrMap {
|
||||
naziv := ""
|
||||
sifra := ""
|
||||
if art, e := h.Artikli.DohvatiID(ctx, k.artikalID); e == nil && art != nil {
|
||||
naziv = art.Naziv
|
||||
sifra = art.Sifra
|
||||
}
|
||||
delovi = append(delovi, model.ServisniDeoSaArtiklom{
|
||||
ArtikalNaziv: naziv,
|
||||
ServisniDeo: model.ServisniDeo{ArtikalID: k.artikalID, Predlozeno: k.predlozeno},
|
||||
Potrazivano: kol,
|
||||
ArtikalSifra: sifra,
|
||||
ServisniDeo: model.ServisniDeo{
|
||||
ArtikalID: k.artikalID,
|
||||
Kolicina: v.kolicina,
|
||||
CenaKomada: v.cena,
|
||||
Predlozeno: k.predlozeno,
|
||||
},
|
||||
})
|
||||
}
|
||||
return delovi, nil
|
||||
@@ -853,6 +886,28 @@ func (h *Handler) ObrisiDeloNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ObrisiPredlozeniDeoNaloga briše predlozene redove (predlozeno=1) za dati artikal na nalogu
|
||||
func (h *Handler) ObrisiPredlozeniDeoNaloga(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
|
||||
return
|
||||
}
|
||||
nalogID, err := parseID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID naloga", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
artikalID, err := parseID(chi.URLParam(r, "artikal_id"))
|
||||
if err != nil {
|
||||
http.Error(w, "Neispravan ID artikla", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.ServisniPotrazivaniDeloviRepo.ObrisiPredlozeneZaArtikal(r.Context(), nalogID, artikalID); err != nil {
|
||||
slog.Error("greška pri brisanju predloženog dela", "error", err)
|
||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška pri uklanjanju predloženog dela.")
|
||||
}
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// DodajRadNalogu dodaje stavku rada (uslugu) na nalog; naziv i cena se snapshot-uju
|
||||
func (h *Handler) DodajRadNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.zahtevajDozvolu(w, r, "servis.izmeni"); !ok {
|
||||
@@ -898,8 +953,9 @@ func (h *Handler) DodajRadNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// forma „Predložene usluge" šalje predlozeno=1; forma „Radovi" ne šalje
|
||||
predlozeno := r.FormValue("predlozeno") == "1"
|
||||
// predlog ako status nije „Primljeno" (server odlučuje, ne forma)
|
||||
nalog, _ := h.ServisRepo.DohvatiID(r.Context(), nalogID)
|
||||
predlozeno := nalog != nil && nalog.Status != model.StatusPrimljeno
|
||||
|
||||
if _, err := h.ServisniRadoviRepo.Dodaj(r.Context(), nalogID, uslugaID, usluga.Naziv, kolicina, cena, predlozeno); err != nil {
|
||||
slog.Error("greška pri dodavanju rada", "error", err)
|
||||
@@ -908,6 +964,13 @@ func (h *Handler) DodajRadNalogu(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// novi predlog poništava prethodnu odluku klijenta
|
||||
if predlozeno {
|
||||
if err := h.ServisRepo.ObrisiOdlukuKlijenta(r.Context(), nalogID); err != nil {
|
||||
slog.Error("greška pri brisanju odluke klijenta", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/servis/"+strconv.FormatInt(nalogID, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -1052,10 +1115,26 @@ func (h *Handler) mozdaKreirajKlijenta(ctx context.Context, r *http.Request, nal
|
||||
tipIdent = "jmbg" // prazan unos — podrazumevani tip radi konzistentnosti baze
|
||||
}
|
||||
|
||||
prezime := strings.TrimSpace(r.FormValue("prezime"))
|
||||
telefon := strings.TrimSpace(r.FormValue("telefon"))
|
||||
mestoLok := strings.TrimSpace(r.FormValue("mesto"))
|
||||
|
||||
// prvo proveri da li klijent već postoji u bazi
|
||||
postojeci, err := h.KlijentiRepo.Pronadji(ctx, tip, ime, prezime, nazivFirme, jmbg, telefon, email, mestoLok)
|
||||
if err != nil {
|
||||
slog.Error("greška pri pretrazi klijenta", "error", err)
|
||||
return "Došlo je do greške pri proveri klijenta. Pokušajte ponovo."
|
||||
}
|
||||
if postojeci != nil {
|
||||
nalog.KlijentID = &postojeci.ID
|
||||
return ""
|
||||
}
|
||||
|
||||
// klijent ne postoji — kreiraj novog
|
||||
klijent := model.Klijent{
|
||||
Tip: tip,
|
||||
Ime: ime,
|
||||
Prezime: strings.TrimSpace(r.FormValue("prezime")),
|
||||
Prezime: prezime,
|
||||
JMBG: jmbg,
|
||||
TipIdentifikacije: tipIdent,
|
||||
NazivFirme: nazivFirme,
|
||||
@@ -2008,24 +2087,24 @@ func (h *Handler) OdbijPopravku(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// PodaciJavnogStatusa su podaci za javnu status stranicu servisnog naloga
|
||||
type PodaciJavnogStatusa struct {
|
||||
Nalog model.ServisniNalog
|
||||
NazivFirme string
|
||||
LogoPutanja string
|
||||
Podnazlov string
|
||||
Adresa string
|
||||
Telefon string
|
||||
PIB string
|
||||
MaticniBroj string
|
||||
Radovi []model.ServisniRad // ugrađeni/odobreni
|
||||
ServisniDelovi []model.ServisniDeoSaArtiklom // ugrađeni/odobreni
|
||||
UkupnoSve float64 // zbir ugrađenih (radovi + delovi)
|
||||
PredlozeniRadovi []model.ServisniRad
|
||||
PredlozeniDelovi []model.ServisniDeoSaArtiklom
|
||||
UkupnoPredlog float64 // zbir predloženih (radovi + delovi)
|
||||
ImaPredlog bool
|
||||
UkupnoSaPredlogom float64 // ukupno ugrađeno + predloženo
|
||||
Odgovoreno bool // klijent već odgovorio (nema predloga, nije „Primljeno")
|
||||
SviStatusi []string
|
||||
Nalog model.ServisniNalog
|
||||
NazivFirme string
|
||||
LogoPutanja string
|
||||
Podnazlov string
|
||||
Adresa string
|
||||
Telefon string
|
||||
PIB string
|
||||
MaticniBroj string
|
||||
Radovi []model.ServisniRad // ugrađeni/odobreni
|
||||
ServisniDelovi []model.ServisniDeoSaArtiklom // ugrađeni/odobreni
|
||||
UkupnoSve float64 // zbir ugrađenih (radovi + delovi)
|
||||
PredlozeniRadovi []model.ServisniRad
|
||||
PredlozeniDelovi []model.ServisniDeoSaArtiklom
|
||||
UkupnoPredlog float64 // zbir predloženih (radovi + delovi)
|
||||
ImaPredlog bool
|
||||
UkupnoSaPredlogom float64 // ukupno ugrađeno + predloženo
|
||||
Odgovoreno bool // klijent već odgovorio (nema predloga, nije „Primljeno")
|
||||
SviStatusi []string
|
||||
}
|
||||
|
||||
// ServisJavniStatus prikazuje javnu status stranicu — dostupna bez prijave putem QR koda
|
||||
@@ -2077,24 +2156,24 @@ func (h *Handler) ServisJavniStatus(w http.ResponseWriter, r *http.Request) {
|
||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||
|
||||
h.renderujStandalone(w, "servis_status_javni", PodaciJavnogStatusa{
|
||||
Nalog: *nalog,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
LogoPutanja: logoZaDokument(podesavanja),
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
MaticniBroj: podesavanja["maticni_broj"],
|
||||
Radovi: ugrRadovi,
|
||||
ServisniDelovi: ugrDelovi,
|
||||
UkupnoSve: ukupnoSve,
|
||||
PredlozeniRadovi: predRadovi,
|
||||
PredlozeniDelovi: predDelovi,
|
||||
UkupnoPredlog: ukupnoPredlog,
|
||||
Nalog: *nalog,
|
||||
NazivFirme: podesavanja["naziv_firme"],
|
||||
LogoPutanja: logoZaDokument(podesavanja),
|
||||
Podnazlov: podesavanja["podnazlov"],
|
||||
Adresa: podesavanja["adresa"],
|
||||
Telefon: podesavanja["telefon"],
|
||||
PIB: podesavanja["pib"],
|
||||
MaticniBroj: podesavanja["maticni_broj"],
|
||||
Radovi: ugrRadovi,
|
||||
ServisniDelovi: ugrDelovi,
|
||||
UkupnoSve: ukupnoSve,
|
||||
PredlozeniRadovi: predRadovi,
|
||||
PredlozeniDelovi: predDelovi,
|
||||
UkupnoPredlog: ukupnoPredlog,
|
||||
ImaPredlog: len(predRadovi) > 0 || len(predDelovi) > 0,
|
||||
UkupnoSaPredlogom: ukupnoSve + ukupnoPredlog,
|
||||
Odgovoreno: !(len(predRadovi) > 0 || len(predDelovi) > 0) && nalog.Status != model.StatusPrimljeno,
|
||||
SviStatusi: model.SviStatusi,
|
||||
Odgovoreno: nalog.OdlukaKlijenta != "" || (!(len(predRadovi) > 0 || len(predDelovi) > 0) && nalog.Status != model.StatusPrimljeno),
|
||||
SviStatusi: model.SviStatusi,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2136,6 +2215,12 @@ func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// odluka je već doneta — ne dozvoljavamo ponovno glasanje
|
||||
if nalog.OdlukaKlijenta != "" {
|
||||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.ServisniRadoviRepo.PrihvatiPredlozene(r.Context(), nalog.ID); err != nil {
|
||||
slog.Error("greška pri prihvatanju predloženih radova", "error", err, "nalog_id", nalog.ID)
|
||||
http.Error(w, "Greška pri prihvatanju predloga.", http.StatusInternalServerError)
|
||||
@@ -2147,11 +2232,9 @@ func (h *Handler) ServisJavniPrihvati(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// sačuvaj komentar klijenta
|
||||
if komentar := strings.TrimSpace(r.FormValue("komentar")); komentar != "" {
|
||||
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), nalog.ID, komentar); err != nil {
|
||||
slog.Error("greška pri čuvanju komentara klijenta", "error", err, "nalog_id", nalog.ID)
|
||||
}
|
||||
komentar := strings.TrimSpace(r.FormValue("komentar"))
|
||||
if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, "prihvaceno", komentar); err != nil {
|
||||
slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||
@@ -2199,6 +2282,12 @@ func (h *Handler) ServisJavniOdbij(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// odluka je već doneta — ne dozvoljavamo ponovno glasanje
|
||||
if nalog.OdlukaKlijenta != "" {
|
||||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// brišemo predložene stavke — klijent ih je odbio
|
||||
if err := h.ServisniRadoviRepo.ObrisiPredlozene(r.Context(), nalog.ID); err != nil {
|
||||
slog.Error("greška pri brisanju predloženih radova", "error", err, "nalog_id", nalog.ID)
|
||||
@@ -2207,11 +2296,9 @@ func (h *Handler) ServisJavniOdbij(w http.ResponseWriter, r *http.Request) {
|
||||
slog.Error("greška pri brisanju predloženih delova", "error", err, "nalog_id", nalog.ID)
|
||||
}
|
||||
|
||||
// sačuvaj komentar klijenta
|
||||
if komentar := strings.TrimSpace(r.FormValue("komentar")); komentar != "" {
|
||||
if err := h.ServisRepo.AzurirajKomentarKlijenta(r.Context(), nalog.ID, komentar); err != nil {
|
||||
slog.Error("greška pri čuvanju komentara klijenta", "error", err, "nalog_id", nalog.ID)
|
||||
}
|
||||
komentar := strings.TrimSpace(r.FormValue("komentar"))
|
||||
if err := h.ServisRepo.SacuvajOdlukuKlijenta(r.Context(), nalog.ID, "odbijeno", komentar); err != nil {
|
||||
slog.Error("greška pri čuvanju odluke klijenta", "error", err, "nalog_id", nalog.ID)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/status/"+token, http.StatusSeeOther)
|
||||
|
||||
@@ -50,11 +50,13 @@ type ServisniNalog struct {
|
||||
PinUredjaja string
|
||||
Pribor string
|
||||
NapomenaKlijentu string
|
||||
NalazDijagnostike string // šta je serviser utvrdio pregledom (dijagnoza + predlog popravke)
|
||||
Uradjeno string // šta je serviser stvarno uradio tokom popravke
|
||||
CenaDijagnostike float64 // taksa kad klijent ne prihvati popravku; 0 = ne naplaćuje se
|
||||
PopravkaOdbijena bool // klijent odbio popravku posle dijagnostike → naplaćuje se samo dijagnostika
|
||||
KomentarKlijenta string // poruka klijenta uz prihvatanje/odbijanje predloga
|
||||
NalazDijagnostike string // šta je serviser utvrdio pregledom (dijagnoza + predlog popravke)
|
||||
Uradjeno string // šta je serviser stvarno uradio tokom popravke
|
||||
CenaDijagnostike float64 // taksa kad klijent ne prihvati popravku; 0 = ne naplaćuje se
|
||||
PopravkaOdbijena bool // klijent odbio popravku posle dijagnostike → naplaćuje se samo dijagnostika
|
||||
KomentarKlijenta string // poruka klijenta uz prihvatanje/odbijanje predloga
|
||||
OdlukaKlijenta string // '' / 'prihvaceno' / 'odbijeno'
|
||||
DatumOdluke *time.Time // kada je klijent doneo odluku
|
||||
JavniToken string
|
||||
}
|
||||
|
||||
@@ -78,6 +80,7 @@ func (d ServisniDeo) Ukupno() float64 {
|
||||
type ServisniDeoSaArtiklom struct {
|
||||
ServisniDeo
|
||||
ArtikalNaziv string
|
||||
ArtikalSifra string
|
||||
Potrazivano int // komada koji nedostaju (iz servisni_potrazivani_delovi), 0 ako nema
|
||||
}
|
||||
|
||||
@@ -102,6 +105,7 @@ type ServisniRad struct {
|
||||
NalogID int64
|
||||
UslugaID int64 // 0 ako je usluga u međuvremenu obrisana iz cenovnika
|
||||
Naziv string
|
||||
Sifra string
|
||||
Kolicina float64
|
||||
CenaKomada float64
|
||||
Datum string
|
||||
|
||||
Reference in New Issue
Block a user