diff --git a/svc/main.go b/svc/main.go index fff5352..472693d 100644 --- a/svc/main.go +++ b/svc/main.go @@ -1170,6 +1170,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "costing", "view", wrapV3(http.HandlerFunc(routes.GetProductionHasCostHistoryHandler)), ) + bindV3(r, pgDB, + "/api/pricing/production-product-costing/firm-cost-history", "GET", + "costing", "view", + wrapV3(http.HandlerFunc(routes.GetProductionFirmCostHistoryHandler)), + ) bindV3(r, pgDB, "/api/pricing/production-product-costing/has-cost-detail-groups", "GET", "costing", "view", diff --git a/svc/models/production_product_costing.go b/svc/models/production_product_costing.go index fe752df..bf02851 100644 --- a/svc/models/production_product_costing.go +++ b/svc/models/production_product_costing.go @@ -62,6 +62,16 @@ type ProductionHasCostHistoryRow struct { SAciklama string `json:"sAciklama"` } +type ProductionFirmCostHistoryRow struct { + NOnMLNo int `json:"nOnMLNo"` + UrunKodu string `json:"UrunKodu"` + UrunAdi string `json:"UrunAdi"` + Tarihi string `json:"Tarihi"` + TPCount int `json:"tpCount"` + DTCount int `json:"dtCount"` + TotalRows int `json:"totalRows"` +} + type ProductionType struct { ID string `json:"id"` Aciklama string `json:"aciklama"` diff --git a/svc/queries/production_product_costing.go b/svc/queries/production_product_costing.go index 7395d0a..20ada8f 100644 --- a/svc/queries/production_product_costing.go +++ b/svc/queries/production_product_costing.go @@ -1049,6 +1049,62 @@ ORDER BY return uretimDB.QueryContext(ctx, sqlText, productCode) } +func GetProductionFirmCostHistory( + ctx context.Context, + uretimDB *sql.DB, + nFirmaID int, + excludeOnMLNo int, + limit int, +) ([]models.ProductionFirmCostHistoryRow, error) { + if nFirmaID <= 0 { + return []models.ProductionFirmCostHistoryRow{}, nil + } + if limit <= 0 { + limit = 100 + } + if limit > 300 { + limit = 300 + } + + rows, err := uretimDB.QueryContext(ctx, ` +SELECT TOP (@p3) + M.nOnMLNo, + LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))) AS UrunKodu, + ISNULL(M.UrunAdi, '') AS UrunAdi, + CONVERT(VARCHAR(10), COALESCE(M.Tarihi, M.dteKayitTarihi), 23) AS Tarihi, + SUM(CASE WHEN G.Grup = 'TP' THEN 1 ELSE 0 END) AS TPCount, + SUM(CASE WHEN G.Grup = 'DT' THEN 1 ELSE 0 END) AS DTCount, + COUNT(*) AS TotalRows +FROM dbo.spUrtOnMLMas M +INNER JOIN dbo.spUrtOnMLMasDet D + ON D.nOnMLNo = M.nOnMLNo +LEFT JOIN dbo.spUrtOnMLHammaddeTuru H + ON H.nHammaddeTuruNo = D.nHammaddeTuruNo +CROSS APPLY ( + SELECT UPPER(ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), LTRIM(RTRIM(ISNULL(H.sAciklama3, ''))))) AS Grup +) G +WHERE ISNULL(M.nFirmaID, 0) = @p1 + AND (ISNULL(@p2, 0) <= 0 OR M.nOnMLNo <> @p2) + AND G.Grup IN ('TP', 'DT') +GROUP BY M.nOnMLNo, M.UrunKodu, M.UrunAdi, M.Tarihi, M.dteKayitTarihi +ORDER BY COALESCE(M.Tarihi, M.dteKayitTarihi) DESC, M.nOnMLNo DESC +`, nFirmaID, excludeOnMLNo, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + out := make([]models.ProductionFirmCostHistoryRow, 0, limit) + for rows.Next() { + var item models.ProductionFirmCostHistoryRow + if err := rows.Scan(&item.NOnMLNo, &item.UrunKodu, &item.UrunAdi, &item.Tarihi, &item.TPCount, &item.DTCount, &item.TotalRows); err != nil { + return nil, err + } + out = append(out, item) + } + return out, rows.Err() +} + func GetProductionHasCostDetailRowsByOnMLNo( ctx context.Context, uretimDB *sql.DB, diff --git a/svc/routes/production_product_costing.go b/svc/routes/production_product_costing.go index 0ad19f6..55e596a 100644 --- a/svc/routes/production_product_costing.go +++ b/svc/routes/production_product_costing.go @@ -330,6 +330,32 @@ func GetProductionHasCostHistoryHandler(w http.ResponseWriter, r *http.Request) _ = json.NewEncoder(w).Encode(list) } +// GET /api/pricing/production-product-costing/firm-cost-history +func GetProductionFirmCostHistoryHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + uretimDB := db.GetUretimDB() + if uretimDB == nil { + http.Error(w, "URETIM veritabani baglantisi aktif degil", http.StatusServiceUnavailable) + return + } + + nFirmaID, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("n_firma_id"))) + excludeOnMLNo, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("exclude_onml_no"))) + limit := parsePositiveIntOrDefault(r.URL.Query().Get("limit"), 100) + if nFirmaID <= 0 { + http.Error(w, "n_firma_id zorunlu ve pozitif sayi olmali", http.StatusBadRequest) + return + } + + rows, err := queries.GetProductionFirmCostHistory(r.Context(), uretimDB, nFirmaID, excludeOnMLNo, limit) + if err != nil { + log.Printf("[ProductionFirmCostHistory] query error: %v", err) + http.Error(w, "Veritabani hatasi", http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(rows) +} + // GET /api/pricing/production-product-costing/has-cost-detail-groups func GetProductionHasCostDetailGroupsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") diff --git a/ui/src/pages/ProductionProductCostingHasCostDetail.vue b/ui/src/pages/ProductionProductCostingHasCostDetail.vue index d32db54..808d7c5 100644 --- a/ui/src/pages/ProductionProductCostingHasCostDetail.vue +++ b/ui/src/pages/ProductionProductCostingHasCostDetail.vue @@ -134,6 +134,17 @@ > Once maliyet olusturulunca aktif olur + @@ -690,6 +701,68 @@ + + + +
+
Malzemeleri Onceki Maliyetlerden Kopyala
+
+ HasCost listesinden bir maliyet secin. Yalnizca bos olan TP/DT gruplari aktarilir. +
+
+ +
+ + + + Hata: {{ firmCostHistoryError }} + + + + + + +
+ Mevcut TP veya DT satirlari korunur. Sadece hic satiri olmayan TP/DT grubu, secilen maliyetten tum satir ozellikleriyle doldurulur. +
+
+ + + + + +
+
+ formatDateTR(value) }, + { name: 'lTutarTL', label: 'Tutar TL', field: 'lTutarTL', align: 'right', sortable: true, format: value => formatMoney(value) }, + { name: 'lTutarUSD', label: 'Tutar USD', field: 'lTutarUSD', align: 'right', sortable: true, format: value => formatMoney(value) }, + { name: 'IscilikTutari', label: 'Iscilik Tutari', field: 'IscilikTutari', align: 'right', sortable: true, format: value => formatMoney(value) }, + { name: 'IscilikPrBr', label: 'Iscilik Pr.Br', field: 'IscilikPrBr', align: 'center', sortable: true }, + { name: 'IscilikUSDTutari', label: 'Iscilik USD', field: 'IscilikUSDTutari', align: 'right', sortable: true, format: value => formatMoney(value) }, + { name: 'nUrtReceteID', label: 'Recete No', field: 'nUrtReceteID', align: 'right', sortable: true }, + { name: 'MaliyetDurumu', label: 'Maliyet Durumu', field: 'MaliyetDurumu', align: 'left', sortable: true } +] const flatDetailRows = computed(() => detailGroups.value.flatMap(grp => Array.isArray(grp?.items) ? grp.items : [])) const priceDeviationAllSelected = computed(() => { const list = Array.isArray(priceDeviationRows.value) ? priceDeviationRows.value : [] @@ -4935,6 +5030,116 @@ async function confirmBrPriceDeviationIfNeeded () { return ok } +function isTPDTGroupName (value) { + const group = normalizeGroupName(value) + return group === 'TP' || group === 'DT' +} + +async function openFirmCostHistoryDialog () { + firmCostHistoryDialogOpen.value = true + firmCostHistoryLoading.value = true + firmCostHistoryError.value = '' + firmCostHistoryRows.value = [] + firmCostHistorySelected.value = [] + firmCostHistoryFilter.value = '' + try { + const data = await get('/pricing/production-product-costing/has-cost-products', { + search: '', + offset: 0, + limit: 1000, + trace_id: traceId.value + }) + const currentNo = parseInt(String(onMLNo.value || detailHeader.value?.nOnMLNo || '0'), 10) || 0 + firmCostHistoryRows.value = (Array.isArray(data) ? data : []) + .filter(row => (parseInt(String(row?.nOnMLNo || '0'), 10) || 0) !== currentNo) + } catch (err) { + firmCostHistoryError.value = await extractApiErrorDetail(err) + } finally { + firmCostHistoryLoading.value = false + } +} + +function confirmFirmCostCopy (source) { + return new Promise(resolve => { + $q.dialog({ + title: 'TP/DT Malzemelerini Kopyala', + message: `${source?.UrunKodu || '-'} / Maliyet No ${source?.nOnMLNo || '-'} kaydindan yalnizca bos olan TP/DT gruplari tum ozellikleriyle doldurulacak. Devam edilsin mi?`, + cancel: { label: 'Vazgec', flat: true }, + ok: { label: 'Kopyala', color: 'deep-purple-7' }, + persistent: true + }).onOk(() => resolve(true)).onCancel(() => resolve(false)).onDismiss(() => resolve(false)) + }) +} + +async function copyTPDTFromSelectedCost () { + const source = firmCostHistorySelected.value[0] + const sourceOnMLNo = parseInt(String(source?.nOnMLNo || '0'), 10) || 0 + if (!sourceOnMLNo) return + if (!(await confirmFirmCostCopy(source))) return + + firmCostCopyLoading.value = true + firmCostHistoryError.value = '' + try { + const sourceData = await get('/pricing/production-product-costing/has-cost-detail-groups', { + n_onml_no: sourceOnMLNo, + trace_id: traceId.value + }) + const sourceGroups = normalizeDetailGroups(sourceData).filter(group => isTPDTGroupName(group?.sAciklama3)) + const populatedGroups = new Set( + flatDetailRows.value + .filter(row => isTPDTGroupName(row?.sAciklama3)) + .map(row => normalizeGroupName(row?.sAciklama3)) + ) + const groupsToCopy = sourceGroups.filter(group => !populatedGroups.has(normalizeGroupName(group?.sAciklama3))) + const sourceRows = groupsToCopy.flatMap(group => ( + (Array.isArray(group?.items) ? group.items : []).map(row => ({ group, row })) + )) + if (sourceRows.length === 0) { + firmCostHistoryError.value = populatedGroups.has('TP') && populatedGroups.has('DT') + ? 'Mevcut maliyette TP ve DT gruplari zaten dolu. Hicbir satir degistirilmedi.' + : 'Secilen maliyette bos gruplari dolduracak TP/DT satiri bulunamadi.' + return + } + + sourceRows.forEach(({ group, row }) => { + newRowSequence.value += 1 + const copiedRow = recalculateDetailRow({ + ...row, + __rowKey: `firm-history-copy-${sourceOnMLNo}-${newRowSequence.value}`, + isNew: true, + nOnMLNo: detailHeader.value?.nOnMLNo || onMLNo.value || '', + nOnMLDetNo: getNextDetailNo(), + sAciklama3: normalizeGroupName(group?.sAciklama3 || row?.sAciklama3), + requiredPlaceholder: false, + draftChanged: true, + __copiedFromOnMLNo: sourceOnMLNo + }, { + preserveInputs: true, + priceType: 'COPY', + updateState: 'firm-history-copy', + markChanged: true + }) + applyEditorRowToGroups(copiedRow) + }) + + recalculateAllDetailRows() + syncAllGroupsOpen() + schedulePersistLocalDraft() + triggerUIUpdate() + firmCostHistoryDialogOpen.value = false + $q.notify({ + type: 'positive', + message: `${groupsToCopy.map(group => normalizeGroupName(group?.sAciklama3)).join(' ve ')} gruplarindaki ${sourceRows.length} satir ${source?.UrunKodu || ''} maliyetinden tum ozellikleriyle eklendi. Kaydet butonu ile onaylayabilirsiniz.`, + position: 'top-right', + timeout: 5000 + }) + } catch (err) { + firmCostHistoryError.value = await extractApiErrorDetail(err) + } finally { + firmCostCopyLoading.value = false + } +} + async function deleteCosting () { if (!detailHeader.value) return const n = parseInt(String(detailHeader.value?.nOnMLNo || detailHeader.value?.NOnMLNo || onMLNo.value || '0'), 10) || 0