fix: persist tender preparation and correct calendar export

This commit is contained in:
2026-09-06 08:30:32 +03:00
parent 909ebae7c2
commit c91631d99f
19 changed files with 569 additions and 182 deletions
+4
View File
@@ -102,3 +102,7 @@ export const telegramSil = () => iste('/api/me/telegram', { method: 'DELETE' });
// --- Takip bildirimleri (zeyilname / sonuç) ---
export const bildirimleriGetir = () => iste('/api/me/bildirimler').then((d) => d.bildirimler || []);
export const bildirimOkundu = (idler) => iste('/api/me/bildirimler', { method: 'POST', body: JSON.stringify({ idler }) });
export const getTenderStates = () => iste('/api/me/tender-state').then(d => d.records || {});
export const saveTenderState = (key, patch, snapshot, version) =>
iste('/api/me/tender-state', {method:'PUT',body:JSON.stringify({key,patch,snapshot,version})}).then(d => d.record);
+4 -3
View File
@@ -27,8 +27,9 @@ export function yaziylaTL(n) {
if (!isFinite(sayi)) return '';
const negatif = sayi < 0;
const mutlak = Math.abs(sayi);
const tam = Math.floor(mutlak);
const kurus = Math.round((mutlak - tam) * 100);
const toplamKurus = Math.round((mutlak + Number.EPSILON) * 100);
const tam = Math.floor(toplamKurus / 100);
const kurus = toplamKurus % 100;
const gruplar = [];
let kalan = tam;
@@ -126,7 +127,7 @@ sınırlama yapılmadan bütün koşullarıyla kabul edilmiştir.</p>
<p>İhale dokümanında yer alan bütün şartları kabul ederek, söz konusu ${bedelCumle}
kabul ve taahhüt ediyoruz.</p>
<p>Teklifimizin, ihale tarihinden itibaren <strong>${kacir(f.gecerlilikGun || '60')}</strong>
<p>Teklifimizin, ihale tarihinden itibaren <strong>${kacir(f.gecerlilikGun || '[DOĞRULAYIN]')}</strong>
takvim günü süreyle geçerli olduğunu beyan ediyoruz.</p>
${f.kisim ? `<p>Teklifimiz <strong>${kacir(f.kisim)}</strong> kısmına/kısımlarına ilişkindir.</p>` : ''}
+2 -2
View File
@@ -21,7 +21,7 @@ export async function fetchTenderDetail(tenderId) {
}
// Kullanıcının ihale üzerindeki durumunu (kanban, takip vb.) canlı veriyle birleştirir.
const USER_FIELDS = ['kanbanStatus', 'takipte', 'incelendi', 'atananPersonel'];
const USER_FIELDS = ['kanbanStatus', 'takipte', 'incelendi', 'atananPersonel', 'gorev', 'notes', 'checklist', 'bidDraft'];
export function mergeUserState(liveTenders, previousTenders) {
const prevById = new Map((previousTenders || []).map((t) => [t.id, t]));
@@ -30,7 +30,7 @@ export function mergeUserState(liveTenders, previousTenders) {
if (!prev) return tender;
const merged = { ...tender };
for (const f of USER_FIELDS) {
if (prev[f] !== undefined && prev[f] !== null && prev[f] !== false) merged[f] = prev[f];
if (prev[f] !== undefined) merged[f] = prev[f];
}
if (prev.aciklama && !merged.aciklama) merged.aciklama = prev.aciklama;
return merged;
+38
View File
@@ -0,0 +1,38 @@
// Calendar dates in Turkish procurement announcements are Europe/Istanbul (+03:00).
// Never manufacture a deadline when the source omitted its time.
export function tenderDeadline(t) {
const source = String(t.aciklama || '');
const date = /^(\d{2})\.(\d{2})\.(\d{4})/.exec(String(t.tarih || ''));
if (!date) throw new Error('İhale tarihi doğrulanamadı; takvime eklenemedi.');
const [,day,month,year] = date;
const time = /^(\d{2}):(\d{2})/.exec(String(t.saat || '').trim())
|| new RegExp(`${day}\\.${month}\\.${year}\\s*[-–]?\\s*(\\d{2}):(\\d{2})`).exec(source);
if (!time) throw new Error('İhale saati kaynakta bulunamadı; önce tarih ve saati doğrulayın.');
const hour = Number(time[1]), minute = Number(time[2]);
const calendarDate = new Date(Date.UTC(Number(year), Number(month)-1, Number(day)));
if (hour > 23 || minute > 59 || calendarDate.getUTCFullYear() !== Number(year)
|| calendarDate.getUTCMonth()+1 !== Number(month) || calendarDate.getUTCDate() !== Number(day))
throw new Error('Geçersiz ihale tarihi veya saati.');
return new Date(Date.UTC(Number(year),Number(month)-1,Number(day),hour-3,minute));
}
const escapeICS = v => String(v || '').replace(/\\/g,'\\\\').replace(/\r?\n/g,'\\n').replace(/;/g,'\\;').replace(/,/g,'\\,');
const stamp = date => date.toISOString().replace(/[-:]/g,'').replace(/\.\d{3}Z$/,'Z');
function fold(line) {
const result = []; let current = '';
for (const char of line) {
if (new TextEncoder().encode(current + char).length > 75) { result.push(current); current = ' '; }
current += char;
}
result.push(current); return result.join('\r\n');
}
export function createTenderICS(t, now = new Date()) {
const start = tenderDeadline(t);
const uid = encodeURIComponent(String(t.ikn || t.id || '').trim());
if (!uid) throw new Error('İhale kimliği gerekli.');
return ['BEGIN:VCALENDAR','VERSION:2.0','PRODID:-//Ihaleist//Tender Calendar//TR','CALSCALE:GREGORIAN',
'BEGIN:VEVENT',`UID:${uid}@ihaleist.com`,`DTSTAMP:${stamp(now)}`,
`DTSTART:${stamp(start)}`,`DTEND:${stamp(start)}`,
`SUMMARY:${escapeICS('İhale son teklif: '+t.baslik)}`,
`DESCRIPTION:${escapeICS('İKN: '+t.ikn+' — '+t.kurum)}`,
'END:VEVENT','END:VCALENDAR',''].map(fold).join('\r\n');
}
+18
View File
@@ -0,0 +1,18 @@
export const tenderStateKey = t => String(t?.ikn || t?.id || '').trim();
export const USER_STATE_FIELDS = ['takipte', 'incelendi', 'kanbanStatus', 'atananPersonel', 'gorev', 'notes', 'checklist', 'bidDraft'];
export function applyTenderStates(tenders, records = {}) {
const seen = new Set();
const result = tenders.map(t => {
const key = tenderStateKey(t); seen.add(key);
const record = records[key];
return record ? { ...t, ...record.data } : t;
});
for (const [key, record] of Object.entries(records)) {
if (!seen.has(key) && record.snapshot && (record.data.takipte || record.data.gorev))
result.push({ ...record.snapshot, ...record.data });
}
return result;
}
export function tenderSnapshot(t) {
return Object.fromEntries(['id','ikn','baslik','kurum','tarih','saat','sehir','alimTuru'].map(k => [k,t[k] ?? '']));
}