From d886fba6dee0966e787dd18dc201954f418b8cbb Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Thu, 21 May 2026 12:46:02 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/routes/production_product_costing_pdf.go | 124 ++++++++++---- ui/.quasar/prod-spa/app.js | 75 +++++++++ ui/.quasar/prod-spa/client-entry.js | 158 ++++++++++++++++++ ui/.quasar/prod-spa/client-prefetch.js | 116 +++++++++++++ ui/.quasar/prod-spa/quasar-user-options.js | 23 +++ .../ProductionProductCostingHasCostDetail.vue | 19 ++- 6 files changed, 482 insertions(+), 33 deletions(-) create mode 100644 ui/.quasar/prod-spa/app.js create mode 100644 ui/.quasar/prod-spa/client-entry.js create mode 100644 ui/.quasar/prod-spa/client-prefetch.js create mode 100644 ui/.quasar/prod-spa/quasar-user-options.js diff --git a/svc/routes/production_product_costing_pdf.go b/svc/routes/production_product_costing_pdf.go index efce93b..5721fd3 100644 --- a/svc/routes/production_product_costing_pdf.go +++ b/svc/routes/production_product_costing_pdf.go @@ -89,6 +89,18 @@ func GetProductionProductCostingOnMLPDFHandler(w http.ResponseWriter, r *http.Re return } + // Exchange rates (needed for EUR conversions in PDF summary tables) + usdRate := 0.0 + eurRate := 0.0 + if mssqlDB := db.GetDB(); mssqlDB != nil { + row, err := queries.GetProductionHasCostDetailExchangeRatesByDate(ctx, mssqlDB, header.MaliyetTarihi) + if err == nil { + var rateDate string + var gbpRate float64 + _ = row.Scan(&rateDate, &usdRate, &eurRate, &gbpRate) + } + } + pdf := gofpdf.New("L", "mm", "A4", "") pdf.SetMargins(8, 8, 8) pdf.SetAutoPageBreak(false, 10) @@ -98,9 +110,11 @@ func GetProductionProductCostingOnMLPDFHandler(w http.ResponseWriter, r *http.Re } export := &costingPDF{ - pdf: pdf, - header: header, - groups: groups, + pdf: pdf, + header: header, + groups: groups, + usdRate: usdRate, + eurRate: eurRate, } export.draw() @@ -224,6 +238,9 @@ type costingPDF struct { pdf *gofpdf.Fpdf header models.ProductionHasCostDetailHeader groups []models.ProductionHasCostDetailGroup + + usdRate float64 + eurRate float64 } func (c *costingPDF) draw() { @@ -312,13 +329,14 @@ type pdfGroupTotalRow struct { group string try float64 usd float64 + eur float64 } func (c *costingPDF) drawHeaderSummaryTables() { pdf := c.pdf partRows := c.computePartSummary() - groupRows, grandTRY, grandUSD := c.computeGroupTotals() + groupRows, grandTRY, grandUSD, grandEUR := c.computeGroupTotals() // Table styling (use same brand palette as statements PDF) // colorPrimary/colorSecondary/colorDetailFill are in statements_pdf.go (same package). @@ -332,28 +350,36 @@ func (c *costingPDF) drawHeaderSummaryTables() { pdf.CellFormat(0, 4.8, "Parca Bazli Maliyet Ozellikleri", "", 1, "L", false, 0, "") partCols := []string{"Parca", "TRY", "USD", "EUR"} partW := []float64{70, 22, 22, 22} + // Add TOTAL row + totalTry, totalUsd, totalEur := 0.0, 0.0, 0.0 + for _, r := range partRows { + totalTry += r.try + totalUsd += r.usd + totalEur += r.eur + } + partRowsWithTotal := append(partRows, pdfPartSummaryRow{name: "TOPLAM", try: totalTry, usd: totalUsd, eur: totalEur}) c.drawMiniTable(partCols, partW, func(i int) []string { - if i >= len(partRows) { + if i >= len(partRowsWithTotal) { return nil } - r := partRows[i] + r := partRowsWithTotal[i] return []string{r.name, pdfMoney(r.try), pdfMoney(r.usd), pdfMoney(r.eur)} - }, len(partRows), true) + }, len(partRowsWithTotal), true, true) pdf.Ln(2) // Group totals table pdf.SetFont("dejavu", "B", 8.2) pdf.CellFormat(0, 4.8, "Grup Toplamlari", "", 1, "L", false, 0, "") - gCols := []string{"Grup", "TRY", "USD"} - gW := []float64{30, 22, 22} - totalRows := append(groupRows, pdfGroupTotalRow{group: "TOPLAM", try: grandTRY, usd: grandUSD}) + gCols := []string{"Grup", "TRY", "USD", "EUR"} + gW := []float64{30, 22, 22, 22} + totalRows := append(groupRows, pdfGroupTotalRow{group: "TOPLAM", try: grandTRY, usd: grandUSD, eur: grandEUR}) c.drawMiniTable(gCols, gW, func(i int) []string { if i >= len(totalRows) { return nil } r := totalRows[i] - return []string{r.group, pdfMoney(r.try), pdfMoney(r.usd)} - }, len(totalRows), true) + return []string{r.group, pdfMoney(r.try), pdfMoney(r.usd), pdfMoney(r.eur)} + }, len(totalRows), true, true) pdf.Ln(2) } @@ -372,7 +398,10 @@ func (c *costingPDF) computePartSummary() []pdfPartSummaryRow { } row.try += it.LTutar row.usd += it.USDTutar - row.eur += it.EURTutar + // EUR isn't directly returned by the has-cost query; derive from USD using exchange rates when available. + if c.usdRate > 0 && c.eurRate > 0 { + row.eur += it.USDTutar * (c.usdRate / c.eurRate) + } } } out := make([]pdfPartSummaryRow, 0, len(byName)) @@ -384,7 +413,7 @@ func (c *costingPDF) computePartSummary() []pdfPartSummaryRow { return out } -func (c *costingPDF) computeGroupTotals() (rows []pdfGroupTotalRow, grandTRY float64, grandUSD float64) { +func (c *costingPDF) computeGroupTotals() (rows []pdfGroupTotalRow, grandTRY float64, grandUSD float64, grandEUR float64) { want := []string{"CM2", "FABRIC", "DT", "TP"} by := map[string]*pdfGroupTotalRow{} for _, w := range want { @@ -400,6 +429,9 @@ func (c *costingPDF) computeGroupTotals() (rows []pdfGroupTotalRow, grandTRY flo // g.TotalTutar is TRY total; g.TotalUSDTutar is USD total t.try += g.TotalTutar t.usd += g.TotalUSDTutar + if c.usdRate > 0 && c.eurRate > 0 { + t.eur += g.TotalUSDTutar * (c.usdRate / c.eurRate) + } } for _, w := range want { @@ -407,11 +439,12 @@ func (c *costingPDF) computeGroupTotals() (rows []pdfGroupTotalRow, grandTRY flo rows = append(rows, *r) grandTRY += r.try grandUSD += r.usd + grandEUR += r.eur } - return rows, grandTRY, grandUSD + return rows, grandTRY, grandUSD, grandEUR } -func (c *costingPDF) drawMiniTable(cols []string, widths []float64, rowFn func(i int) []string, rowCount int, zebra bool) { +func (c *costingPDF) drawMiniTable(cols []string, widths []float64, rowFn func(i int) []string, rowCount int, zebra bool, emphasizeLastRow bool) { pdf := c.pdf // Header row @@ -433,25 +466,41 @@ func (c *costingPDF) drawMiniTable(cols []string, widths []float64, rowFn func(i pdf.SetXY(x0, y0+hH) // Data rows - pdf.SetFont("dejavu", "", 7.4) pdf.SetDrawColor(220, 220, 220) for i := 0; i < rowCount; i++ { row := rowFn(i) if row == nil { break } + isLast := emphasizeLastRow && (i == rowCount-1) fill := zebra && (i%2 == 1) - if fill { - pdf.SetFillColor(colorDetailFill[0], colorDetailFill[1], colorDetailFill[2]) + + // Style rules: + // - Total row: primary fill + bigger bold text + // - Zebra rows: detail fill + // - Normal: white + if isLast { + pdf.SetFont("dejavu", "B", 8.6) + pdf.SetTextColor(255, 255, 255) + pdf.SetFillColor(colorPrimary[0], colorPrimary[1], colorPrimary[2]) } else { - pdf.SetFillColor(255, 255, 255) + pdf.SetFont("dejavu", "", 7.4) + pdf.SetTextColor(30, 30, 30) + if fill { + pdf.SetFillColor(colorDetailFill[0], colorDetailFill[1], colorDetailFill[2]) + } else { + pdf.SetFillColor(255, 255, 255) + } } x = x0 y := pdf.GetY() rh := 5.0 + if isLast { + rh = 5.6 + } for cidx, val := range row { style := "" - if fill { + if fill || isLast { style = "DF" } pdf.Rect(x, y, widths[cidx], rh, style) @@ -465,6 +514,7 @@ func (c *costingPDF) drawMiniTable(cols []string, widths []float64, rowFn func(i } pdf.SetXY(x0, y+rh) } + pdf.SetTextColor(0, 0, 0) } func formatDateTRDot(s string) string { @@ -503,12 +553,14 @@ func (c *costingPDF) drawGroup(g models.ProductionHasCostDetailGroup, firstGroup "Renk", "Miktar", "Br", + "Br\nFiyat", + "Pr\nBr", "USD\nFiyat", "USD\nTutar", "TRY\nFiyat", "TRY\nTutar", } - wn := []float64{8, 20, 22, 32, 70, 14, 14, 10, 16, 16, 16, 16} // sum = 250 + wn := []float64{8, 20, 22, 30, 56, 14, 14, 10, 16, 12, 16, 16, 16, 16} // sum = 252 c.drawTableHeader(cols, wn) // PDF-specific ordering: by hammadde turu no, then code. @@ -648,25 +700,39 @@ func (c *costingPDF) drawRowWithGroup(it models.ProductionHasCostDetailGroupItem c.drawCell(x, y0, wn[7], rowH, strings.TrimSpace(it.SBirim), "C", fill) x += wn[7] + // Prefer input price if present; otherwise lFiyat. + price := it.LFiyat + cur := strings.TrimSpace(it.SDovizCinsi) + if it.FiyatGirilen != nil && *it.FiyatGirilen > 0 { + price = *it.FiyatGirilen + if strings.TrimSpace(it.FiyatDoviz) != "" { + cur = strings.TrimSpace(it.FiyatDoviz) + } + } + c.drawCell(x, y0, wn[8], rowH, pdfMoney(price), "R", fill) + x += wn[8] + c.drawCell(x, y0, wn[9], rowH, cur, "C", fill) + x += wn[9] + // Always show USD/TRY unit+total. // In URETIM schema: lFiyat/lTutar are in TRY, lDovizFiyati/usdTutar are in USD. - c.drawCell(x, y0, wn[8], rowH, pdfMoney(it.LDovizFiyati), "R", fill) - x += wn[8] + c.drawCell(x, y0, wn[10], rowH, pdfMoney(it.LDovizFiyati), "R", fill) + x += wn[10] usdTotal := it.USDTutar if usdTotal == 0 && it.LMiktar != 0 && it.LDovizFiyati != 0 { usdTotal = it.LMiktar * it.LDovizFiyati } - c.drawCell(x, y0, wn[9], rowH, pdfMoney(usdTotal), "R", fill) - x += wn[9] + c.drawCell(x, y0, wn[11], rowH, pdfMoney(usdTotal), "R", fill) + x += wn[11] // Prefer input price if present; otherwise lFiyat. unitTRY := it.LFiyat if it.FiyatGirilen != nil && *it.FiyatGirilen > 0 && strings.EqualFold(strings.TrimSpace(it.FiyatDoviz), "TRY") { unitTRY = *it.FiyatGirilen } - c.drawCell(x, y0, wn[10], rowH, pdfMoney(unitTRY), "R", fill) - x += wn[10] - c.drawCell(x, y0, wn[11], rowH, pdfMoney(it.LTutar), "R", fill) + c.drawCell(x, y0, wn[12], rowH, pdfMoney(unitTRY), "R", fill) + x += wn[12] + c.drawCell(x, y0, wn[13], rowH, pdfMoney(it.LTutar), "R", fill) pdf.SetXY(x0, y0+rowH) } diff --git a/ui/.quasar/prod-spa/app.js b/ui/.quasar/prod-spa/app.js new file mode 100644 index 0000000..caeaac1 --- /dev/null +++ b/ui/.quasar/prod-spa/app.js @@ -0,0 +1,75 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + + + +import { Quasar } from 'quasar' +import { markRaw } from 'vue' +import RootComponent from 'app/src/App.vue' + +import createStore from 'app/src/stores/index' +import createRouter from 'app/src/router/index' + + + + + +export default async function (createAppFn, quasarUserOptions) { + + + // Create the app instance. + // Here we inject into it the Quasar UI, the router & possibly the store. + const app = createAppFn(RootComponent) + + + + app.use(Quasar, quasarUserOptions) + + + + + const store = typeof createStore === 'function' + ? await createStore({}) + : createStore + + + app.use(store) + + + + + + const router = markRaw( + typeof createRouter === 'function' + ? await createRouter({store}) + : createRouter + ) + + + // make router instance available in store + + store.use(({ store }) => { store.router = router }) + + + + // Expose the app, the router and the store. + // Note that we are not mounting the app here, since bootstrapping will be + // different depending on whether we are in a browser or on the server. + return { + app, + store, + router + } +} diff --git a/ui/.quasar/prod-spa/client-entry.js b/ui/.quasar/prod-spa/client-entry.js new file mode 100644 index 0000000..5223e2b --- /dev/null +++ b/ui/.quasar/prod-spa/client-entry.js @@ -0,0 +1,158 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + +import { createApp } from 'vue' + + + + + + + +import '@quasar/extras/roboto-font/roboto-font.css' + +import '@quasar/extras/material-icons/material-icons.css' + + + + +// We load Quasar stylesheet file +import 'quasar/dist/quasar.sass' + + + + +import 'src/css/app.css' + + +import createQuasarApp from './app.js' +import quasarUserOptions from './quasar-user-options.js' + + + + + + + + +const publicPath = `/` + + +async function start ({ + app, + router + , store +}, bootFiles) { + + let hasRedirected = false + const getRedirectUrl = url => { + try { return router.resolve(url).href } + catch (err) {} + + return Object(url) === url + ? null + : url + } + const redirect = url => { + hasRedirected = true + + if (typeof url === 'string' && /^https?:\/\//.test(url)) { + window.location.href = url + return + } + + const href = getRedirectUrl(url) + + // continue if we didn't fail to resolve the url + if (href !== null) { + window.location.href = href + window.location.reload() + } + } + + const urlPath = window.location.href.replace(window.location.origin, '') + + for (let i = 0; hasRedirected === false && i < bootFiles.length; i++) { + try { + await bootFiles[i]({ + app, + router, + store, + ssrContext: null, + redirect, + urlPath, + publicPath + }) + } + catch (err) { + if (err && err.url) { + redirect(err.url) + return + } + + console.error('[Quasar] boot error:', err) + return + } + } + + if (hasRedirected === true) return + + + app.use(router) + + + + + + + app.mount('#q-app') + + + +} + +createQuasarApp(createApp, quasarUserOptions) + + .then(app => { + // eventually remove this when Cordova/Capacitor/Electron support becomes old + const [ method, mapFn ] = Promise.allSettled !== void 0 + ? [ + 'allSettled', + bootFiles => bootFiles.map(result => { + if (result.status === 'rejected') { + console.error('[Quasar] boot error:', result.reason) + return + } + return result.value.default + }) + ] + : [ + 'all', + bootFiles => bootFiles.map(entry => entry.default) + ] + + return Promise[ method ]([ + + import(/* webpackMode: "eager" */ 'boot/dayjs'), + + import(/* webpackMode: "eager" */ 'boot/locale'), + + import(/* webpackMode: "eager" */ 'boot/resizeObserverGuard') + + ]).then(bootFiles => { + const boot = mapFn(bootFiles).filter(entry => typeof entry === 'function') + start(app, boot) + }) + }) + diff --git a/ui/.quasar/prod-spa/client-prefetch.js b/ui/.quasar/prod-spa/client-prefetch.js new file mode 100644 index 0000000..9bbe3c5 --- /dev/null +++ b/ui/.quasar/prod-spa/client-prefetch.js @@ -0,0 +1,116 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + + + +import App from 'app/src/App.vue' +let appPrefetch = typeof App.preFetch === 'function' + ? App.preFetch + : ( + // Class components return the component options (and the preFetch hook) inside __c property + App.__c !== void 0 && typeof App.__c.preFetch === 'function' + ? App.__c.preFetch + : false + ) + + +function getMatchedComponents (to, router) { + const route = to + ? (to.matched ? to : router.resolve(to).route) + : router.currentRoute.value + + if (!route) { return [] } + + const matched = route.matched.filter(m => m.components !== void 0) + + if (matched.length === 0) { return [] } + + return Array.prototype.concat.apply([], matched.map(m => { + return Object.keys(m.components).map(key => { + const comp = m.components[key] + return { + path: m.path, + c: comp + } + }) + })) +} + +export function addPreFetchHooks ({ router, store, publicPath }) { + // Add router hook for handling preFetch. + // Doing it after initial route is resolved so that we don't double-fetch + // the data that we already have. Using router.beforeResolve() so that all + // async components are resolved. + router.beforeResolve((to, from, next) => { + const + urlPath = window.location.href.replace(window.location.origin, ''), + matched = getMatchedComponents(to, router), + prevMatched = getMatchedComponents(from, router) + + let diffed = false + const preFetchList = matched + .filter((m, i) => { + return diffed || (diffed = ( + !prevMatched[i] || + prevMatched[i].c !== m.c || + m.path.indexOf('/:') > -1 // does it has params? + )) + }) + .filter(m => m.c !== void 0 && ( + typeof m.c.preFetch === 'function' + // Class components return the component options (and the preFetch hook) inside __c property + || (m.c.__c !== void 0 && typeof m.c.__c.preFetch === 'function') + )) + .map(m => m.c.__c !== void 0 ? m.c.__c.preFetch : m.c.preFetch) + + + if (appPrefetch !== false) { + preFetchList.unshift(appPrefetch) + appPrefetch = false + } + + + if (preFetchList.length === 0) { + return next() + } + + let hasRedirected = false + const redirect = url => { + hasRedirected = true + next(url) + } + const proceed = () => { + + if (hasRedirected === false) { next() } + } + + + + preFetchList.reduce( + (promise, preFetch) => promise.then(() => hasRedirected === false && preFetch({ + store, + currentRoute: to, + previousRoute: from, + redirect, + urlPath, + publicPath + })), + Promise.resolve() + ) + .then(proceed) + .catch(e => { + console.error(e) + proceed() + }) + }) +} diff --git a/ui/.quasar/prod-spa/quasar-user-options.js b/ui/.quasar/prod-spa/quasar-user-options.js new file mode 100644 index 0000000..ac1dae3 --- /dev/null +++ b/ui/.quasar/prod-spa/quasar-user-options.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * THIS FILE IS GENERATED AUTOMATICALLY. + * DO NOT EDIT. + * + * You are probably looking on adding startup/initialization code. + * Use "quasar new boot " and add it there. + * One boot file per concern. Then reference the file(s) in quasar.config file > boot: + * boot: ['file', ...] // do not add ".js" extension to it. + * + * Boot files are your "main.js" + **/ + +import lang from 'quasar/lang/tr.js' + + + +import {Loading,Dialog,Notify} from 'quasar' + + + +export default { config: {"notify":{"position":"top","timeout":2500}},lang,plugins: {Loading,Dialog,Notify} } + diff --git a/ui/src/pages/ProductionProductCostingHasCostDetail.vue b/ui/src/pages/ProductionProductCostingHasCostDetail.vue index 0e838d7..bbce129 100644 --- a/ui/src/pages/ProductionProductCostingHasCostDetail.vue +++ b/ui/src/pages/ProductionProductCostingHasCostDetail.vue @@ -251,12 +251,12 @@ >
- {{ grp.sAciklama3 || 'TANIMSIZ' }} + {{ grp.sAciklama3 || 'TANIMSIZ' }} + + Toplam Miktar: {{ formatBarQuantity(resolveGroupQuantity(grp)) }} MT +
- - Toplam Miktar: {{ formatBarQuantity(resolveGroupQuantity(grp)) }} MT | - Grup Toplami TRY: {{ formatBarMoney(resolveGroupTRYTutar(grp)) }} | USD: {{ formatBarMoney(resolveGroupUSDTutar(grp)) }}