standardize displayed timestamps to Istanbul

This commit is contained in:
2026-09-06 19:21:21 +03:00
parent b4cd5a073c
commit 97592a05c0
15 changed files with 21 additions and 15 deletions
+6
View File
@@ -2242,3 +2242,9 @@ Doğrulama: 294 otomatik test, üretim derlemesi ve browser QA başarılı.
Kur panelinde TCMB’den alınma zamanı artık tarayıcı yerel saatinden bağımsız olarak İstanbul saatinde gösteriliyor. USD/EUR oranları dört ondalıkla tutarlı sunuluyor ve ekran okuyucular için birim oranı açık adla veriliyor; kaynak bülten tarihi ve stale uyarısı korunuyor. Kur panelinde TCMB’den alınma zamanı artık tarayıcı yerel saatinden bağımsız olarak İstanbul saatinde gösteriliyor. USD/EUR oranları dört ondalıkla tutarlı sunuluyor ve ekran okuyucular için birim oranı açık adla veriliyor; kaynak bülten tarihi ve stale uyarısı korunuyor.
Doğrulama: 294 otomatik test, üretim derlemesi ve browser QA başarılı. Doğrulama: 294 otomatik test, üretim derlemesi ve browser QA başarılı.
### 2026-09-06 · Kullanıcıya gösterilen zamanların İstanbul standardı
İhale kaynak geçmişi, notlar, AI iş geçmişi, belge deposu, API anahtarları, bildirim abonelikleri ve kur/sağlayıcı kayıtlarındaki ISO zaman gösterimleri `Europe/Istanbul` ile sabitlendi. Kullanıcının cihaz saat dilimi artık aynı kaydın farklı gün/saat görünmesine neden olmuyor.
Doğrulama: 294 otomatik test, üretim derlemesi ve `git diff --check` başarılı. Farklı cihaz saat dilimlerinde görsel kabul ayrıca yapılmalı.
+1 -1
View File
@@ -27,7 +27,7 @@ export function AiJobHistory({ikn,onOpen,revision,locked=false,operationFilter='
{error&&<p role="alert">{error}</p>} {error&&<p role="alert">{error}</p>}
{!jobs.length&&!busy&&!error&&<p>Bu ihale için kayıtlı AI işi yok.</p>} {!jobs.length&&!busy&&!error&&<p>Bu ihale için kayıtlı AI işi yok.</p>}
<ul className="space-y-2 mt-2">{jobs.map(job=><li key={job.id} className="border rounded p-2"> <ul className="space-y-2 mt-2">{jobs.map(job=><li key={job.id} className="border rounded p-2">
<p>{operations[job.operation]} · {labels[job.state]} · {new Date(job.createdAt).toLocaleString('tr-TR')}</p> <p>{operations[job.operation]} · {labels[job.state]} · {new Date(job.createdAt).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })}</p>
{job.error&&<p>{job.error}</p>} {job.error&&<p>{job.error}</p>}
{job.state==='completed'&&<button type="button" disabled={busy || locked} className="underline py-2 mr-3" onClick={()=>act(async()=>{const data=await getAiJob(job.id);if(alive.current)await onOpen(data.job);})}>Kaynak ve sonucu aç</button>} {job.state==='completed'&&<button type="button" disabled={busy || locked} className="underline py-2 mr-3" onClick={()=>act(async()=>{const data=await getAiJob(job.id);if(alive.current)await onOpen(data.job);})}>Kaynak ve sonucu aç</button>}
{['failed','interrupted'].includes(job.state)&&<button type="button" disabled={busy} className="underline py-2 mr-3" onClick={()=>act(async()=>{await retryAiJob(job.id);await refresh();})}>İşi yeniden başlat</button>} {['failed','interrupted'].includes(job.state)&&<button type="button" disabled={busy} className="underline py-2 mr-3" onClick={()=>act(async()=>{await retryAiJob(job.id);await refresh();})}>İşi yeniden başlat</button>}
+1 -1
View File
@@ -50,7 +50,7 @@ export function DocumentTextReview({documentId,extractedText,onAnalyze}) {
<h5 className="font-bold text-sm">Metni karşılaştır ve düzelt</h5> <h5 className="font-bold text-sm">Metni karşılaştır ve düzelt</h5>
<p className="text-xs text-slate-600">Belgenin tüm sayfalarını açıp metin, miktar ve birimleri karşılaştırın. Bu kayıt kullanıcı incelemesidir; resmî onay veya e-imza değildir. Kaydetmek tek başına teklif dosyasını hazır yapmaz.</p> <p className="text-xs text-slate-600">Belgenin tüm sayfalarını açıp metin, miktar ve birimleri karşılaştırın. Bu kayıt kullanıcı incelemesidir; resmî onay veya e-imza değildir. Kaydetmek tek başına teklif dosyasını hazır yapmaz.</p>
{source?.stale && <p role="alert" className="text-xs text-amber-800">Kaynak dosya değişmiş. Önceki inceleme geçerli değil; tüm sayfaları yeniden karşılaştırın.</p>} {source?.stale && <p role="alert" className="text-xs text-amber-800">Kaynak dosya değişmiş. Önceki inceleme geçerli değil; tüm sayfaları yeniden karşılaştırın.</p>}
{source?.review && <p className="text-xs text-slate-500">Son kayıt: sürüm {source.review.revision} · {new Date(source.review.reviewed_at).toLocaleString('tr-TR')}</p>} {source?.review && <p className="text-xs text-slate-500">Son kayıt: sürüm {source.review.revision} · {new Date(source.review.reviewed_at).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })}</p>}
<label className="block text-xs font-semibold">Kontrol edilen metin <label className="block text-xs font-semibold">Kontrol edilen metin
<textarea value={text} onChange={e=>{setText(e.target.value);setCompared(false);setSaved(null);}} disabled={busy || analyzing} <textarea value={text} onChange={e=>{setText(e.target.value);setCompared(false);setSaved(null);}} disabled={busy || analyzing}
className="block w-full h-64 border rounded p-2 font-mono font-normal mt-1" /> className="block w-full h-64 border rounded p-2 font-mono font-normal mt-1" />
+1 -1
View File
@@ -51,7 +51,7 @@ export const SummaryCards = () => {
icon: BellRing, icon: BellRing,
color: 'purple', color: 'purple',
title: oncekiGiris title: oncekiGiris
? `Önceki ziyaretinizden (${new Date(oncekiGiris).toLocaleString('tr-TR')}) sonra yayımlanan ilanlar` ? `Önceki ziyaretinizden (${new Date(oncekiGiris).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })}) sonra yayımlanan ilanlar`
: 'İlk ziyaretiniz — bugün yayımlanan ilanlar gösteriliyor' : 'İlk ziyaretiniz — bugün yayımlanan ilanlar gösteriliyor'
}, },
{ {
+1 -1
View File
@@ -64,7 +64,7 @@ export const TenderDetailModal = () => {
id: Date.now(), id: Date.now(),
author: userProfile.name, author: userProfile.name,
text: noteText.trim(), text: noteText.trim(),
date: new Date().toLocaleString('tr-TR') date: new Date().toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })
}]}); }]});
if (saved) setNoteDrafts(state => acknowledgeNoteDraft(state, noteOwner, noteKey, noteDraft)); if (saved) setNoteDrafts(state => acknowledgeNoteDraft(state, noteOwner, noteKey, noteDraft));
}; };
+1 -1
View File
@@ -28,7 +28,7 @@ export function TenderSourceHistory({tender}) {
{state.error && <div role="alert"><p>{state.error}</p><button type="button" onClick={()=>load(state.loaded?state.nextCursor:null)} disabled={state.loading} className="underline py-2">Yeniden dene</button></div>} {state.error && <div role="alert"><p>{state.error}</p><button type="button" onClick={()=>load(state.loaded?state.nextCursor:null)} disabled={state.loading} className="underline py-2">Yeniden dene</button></div>}
{state.loaded && !state.records.length && <p className="mt-2">Henüz gözlem kaydı yok. Bu, ihale bilgilerinin hiç değişmediğini göstermez.</p>} {state.loaded && !state.records.length && <p className="mt-2">Henüz gözlem kaydı yok. Bu, ihale bilgilerinin hiç değişmediğini göstermez.</p>}
<ol className="space-y-3 mt-3">{state.records.map(record=><li key={record.id} className="border rounded p-2"> <ol className="space-y-3 mt-3">{state.records.map(record=><li key={record.id} className="border rounded p-2">
<p>{new Date(record.observedAt).toLocaleString('tr-TR')} · {record.sourceId}</p> <p>{new Date(record.observedAt).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })} · {record.sourceId}</p>
{!record.changes.length?<p>Bu kaynak için ilk gözlem.</p>:record.changes.map(change=><details key={change.field} className="mt-1"> {!record.changes.length?<p>Bu kaynak için ilk gözlem.</p>:record.changes.map(change=><details key={change.field} className="mt-1">
<summary className="cursor-pointer">{labels[change.field] || change.field} değişti</summary> <summary className="cursor-pointer">{labels[change.field] || change.field} değişti</summary>
<p className="whitespace-pre-wrap break-words">Önce: {change.before ?? 'Bilinmiyor'}</p> <p className="whitespace-pre-wrap break-words">Önce: {change.before ?? 'Bilinmiyor'}</p>
+1 -1
View File
@@ -164,7 +164,7 @@ const ApiKeySession = () => {
<p className="text-[10px] text-slate-400 mt-0.5"> <p className="text-[10px] text-slate-400 mt-0.5">
Oluşturma: {new Date(k.olusturma).toLocaleDateString('tr-TR')} Oluşturma: {new Date(k.olusturma).toLocaleDateString('tr-TR')}
{' · '} {' · '}
{k.son_kullanim ? `son kullanım: ${new Date(k.son_kullanim).toLocaleString('tr-TR')}` : 'hiç kullanılmadı'} {k.son_kullanim ? `son kullanım: ${new Date(k.son_kullanim).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })}` : 'hiç kullanılmadı'}
</p> </p>
</div> </div>
<button type="button" onClick={() => iptal(k.id)} disabled={busy || loading} <button type="button" onClick={() => iptal(k.id)} disabled={busy || loading}
+1 -1
View File
@@ -36,7 +36,7 @@ function Review({row,weekly,busy,resolve}){
<p className="font-bold">{row.subject}</p><p>{String(row.to)} · {labels[row.state]}</p><p className="break-all">Kayıt: {row.id}</p> <p className="font-bold">{row.subject}</p><p>{String(row.to)} · {labels[row.state]}</p><p className="break-all">Kayıt: {row.id}</p>
{row.error&&<p>{row.error}</p>} {row.error&&<p>{row.error}</p>}
{row.providerId&&<p className="break-all">Sağlayıcı kaydı: {row.providerId}</p>} {row.providerId&&<p className="break-all">Sağlayıcı kaydı: {row.providerId}</p>}
{row.review&&<p>{row.review.actor} · {new Date(row.review.at).toLocaleString('tr-TR')} · {row.review.note}</p>} {row.review&&<p>{row.review.actor} · {new Date(row.review.at).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })} · {row.review.note}</p>}
{row.state==='uncertain'&&<fieldset disabled={busy} className="space-y-2"> {row.state==='uncertain'&&<fieldset disabled={busy} className="space-y-2">
<label className="block">Kontrol açıklaması<textarea className="block border w-full p-2" value={note} onChange={e=>setNote(e.target.value)} minLength={20}/></label> <label className="block">Kontrol açıklaması<textarea className="block border w-full p-2" value={note} onChange={e=>setNote(e.target.value)} minLength={20}/></label>
<label className="block">Sağlayıcı gönderim kimliği<input className="block border w-full p-2" value={providerId} onChange={e=>setProviderId(e.target.value)}/></label> <label className="block">Sağlayıcı gönderim kimliği<input className="block border w-full p-2" value={providerId} onChange={e=>setProviderId(e.target.value)}/></label>
+1 -1
View File
@@ -313,7 +313,7 @@ const DocumentVaultSession = ({signedIn}) => {
{d.tur === 'ihale' ? 'İhale dokümanı' : 'Özel firma belgesi'} · {d.kategori || 'Diğer'} · <strong>{formatBayt(d.boyut)}</strong> {d.tur === 'ihale' ? 'İhale dokümanı' : 'Özel firma belgesi'} · {d.kategori || 'Diğer'} · <strong>{formatBayt(d.boyut)}</strong>
</p> </p>
<p className="text-[10px] text-slate-400 mt-0.5"> <p className="text-[10px] text-slate-400 mt-0.5">
{new Date(d.olusturma).toLocaleString('tr-TR')} {new Date(d.olusturma).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })}
<span className="block">{documentProcessingLabel(d.isleme)}</span> <span className="block">{documentProcessingLabel(d.isleme)}</span>
</p> </p>
<label className="flex items-center gap-1.5 mt-1.5 text-[10px] text-slate-500"> <label className="flex items-center gap-1.5 mt-1.5 text-[10px] text-slate-500">
+1 -1
View File
@@ -122,7 +122,7 @@ const SubscriptionForm = () => {
<div className="min-w-0"> <div className="min-w-0">
<p className="text-xs font-bold text-slate-800">{ozet(a)}</p> <p className="text-xs font-bold text-slate-800">{ozet(a)}</p>
<p className="text-[10px] text-slate-400 mt-0.5"> <p className="text-[10px] text-slate-400 mt-0.5">
{a.sonGonderim ? `son gönderim: ${new Date(a.sonGonderim).toLocaleString('tr-TR')}` : 'ilk tur bekleniyor'} {a.sonGonderim ? `son gönderim: ${new Date(a.sonGonderim).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })}` : 'ilk tur bekleniyor'}
{a.deliveryStatus && <span className="block text-amber-700">{a.deliveryStatus.state==='uncertain'?'Gönderim sonucu belirsiz. Uygulama yöneticisinin sağlayıcı kaydını kontrol etmesi gerekiyor.':'E-posta gönderimi bekliyor.'}</span>} {a.deliveryStatus && <span className="block text-amber-700">{a.deliveryStatus.state==='uncertain'?'Gönderim sonucu belirsiz. Uygulama yöneticisinin sağlayıcı kaydını kontrol etmesi gerekiyor.':'E-posta gönderimi bekliyor.'}</span>}
</p> </p>
</div> </div>
+1 -1
View File
@@ -25,5 +25,5 @@ export function analysisView(data,input) {
return {...data.result,tenderKey:input.tender?.ikn || input.tender?.id || null, return {...data.result,tenderKey:input.tender?.ikn || input.tender?.id || null,
mode:data.mode || (input.mode==='derin'?'derin':'standart'),fileName:input.fileName, mode:data.mode || (input.mode==='derin'?'derin':'standart'),fileName:input.fileName,
model:`${data.model.label} (${data.model.provider})`,modelId:data.model.model, model:`${data.model.label} (${data.model.provider})`,modelId:data.model.model,
attempts:data.attempts,analyzedAt:new Date(data.analyzedAt).toLocaleString('tr-TR')}; attempts:data.attempts,analyzedAt:new Date(data.analyzedAt).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' })};
} }
+1 -1
View File
@@ -5,7 +5,7 @@ export function companyScanProposal(data) {
if(!Array.isArray(result[key])||result[key].some(value=>typeof value!=='string'||!value.trim()))throw new Error('Tarama önerileri eksik veya geçersiz. Profil değiştirilmedi.'); if(!Array.isArray(result[key])||result[key].some(value=>typeof value!=='string'||!value.trim()))throw new Error('Tarama önerileri eksik veya geçersiz. Profil değiştirilmedi.');
return [...new Set(result[key].map(value=>value.trim()))]; return [...new Set(result[key].map(value=>value.trim()))];
}; };
return {scrapedAt:new Date().toLocaleString('tr-TR'),extractedSectors:array('sektorler'),extractedProducts:array('urunler'), return {scrapedAt:new Date().toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' }),extractedSectors:array('sektorler'),extractedProducts:array('urunler'),
extractedCertifications:array('sertifikalar'),ihaleKonulari:array('ihaleKonulari'),ozet:typeof result.ozet==='string'?result.ozet:null, extractedCertifications:array('sertifikalar'),ihaleKonulari:array('ihaleKonulari'),ozet:typeof result.ozet==='string'?result.ozet:null,
tarananSayfa:data.tarananSayfa,gercekTarama:true,matchScoreBoost:0}; tarananSayfa:data.tarananSayfa,gercekTarama:true,matchScoreBoost:0};
} }
+1 -1
View File
@@ -170,7 +170,7 @@ export const fetchLiveEKAPData = async () => {
resolve({ resolve({
success: true, success: true,
source: 'EKAP Open Data Live Stream API v2.6', source: 'EKAP Open Data Live Stream API v2.6',
fetchedAt: new Date().toLocaleString('tr-TR'), fetchedAt: new Date().toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' }),
tenders: REAL_EKAP_LIVE_TENDERS, tenders: REAL_EKAP_LIVE_TENDERS,
totalActiveLiveCount: 58920, totalActiveLiveCount: 58920,
todayAddedCount: 1445, todayAddedCount: 1445,
+1 -1
View File
@@ -8,7 +8,7 @@ export function addLocalNotification(current,title,text) {
export function mergeServerNotifications(current,rows) { export function mergeServerNotifications(current,rows) {
if(!Array.isArray(rows)||rows.some(row=>typeof row?.id!=='string'||typeof row.baslik!=='string'||typeof row.metin!=='string'))throw new Error('Bildirim listesi geçersiz.'); if(!Array.isArray(rows)||rows.some(row=>typeof row?.id!=='string'||typeof row.baslik!=='string'||typeof row.metin!=='string'))throw new Error('Bildirim listesi geçersiz.');
const remote=rows.map(row=>({id:'server:'+row.id,serverId:row.id,title:row.baslik,text:row.metin, const remote=rows.map(row=>({id:'server:'+row.id,serverId:row.id,title:row.baslik,text:row.metin,
time:new Date(row.zaman).toLocaleString('tr-TR'),read:!!row.okundu,ikn:row.ikn})); time:new Date(row.zaman).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' }),read:!!row.okundu,ikn:row.ikn}));
return keepRecentLocal([...remote,...current.filter(row=>!row.serverId)]); return keepRecentLocal([...remote,...current.filter(row=>!row.serverId)]);
} }
export async function acknowledgeNotifications(snapshot,request) { export async function acknowledgeNotifications(snapshot,request) {
+2 -2
View File
@@ -166,7 +166,7 @@ export const parseWebsiteCapabilitiesDynamically = (url) => {
const isMatched = matchRule.keywords.some(kw => cleanUrl.includes(kw)); const isMatched = matchRule.keywords.some(kw => cleanUrl.includes(kw));
if (isMatched) { if (isMatched) {
return { return {
scrapedAt: new Date().toLocaleString('tr-TR'), scrapedAt: new Date().toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' }),
extractedSectors: [matchRule.sector], extractedSectors: [matchRule.sector],
extractedProducts: [...matchRule.products], extractedProducts: [...matchRule.products],
extractedCertifications: [...matchRule.certifications], extractedCertifications: [...matchRule.certifications],
@@ -179,7 +179,7 @@ export const parseWebsiteCapabilitiesDynamically = (url) => {
// 2. Generic Pure Extraction (Guarantees ZERO wrong cross-sector hallucination) // 2. Generic Pure Extraction (Guarantees ZERO wrong cross-sector hallucination)
const formattedBrandName = coreDomainName.toUpperCase(); const formattedBrandName = coreDomainName.toUpperCase();
return { return {
scrapedAt: new Date().toLocaleString('tr-TR'), scrapedAt: new Date().toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' }),
extractedSectors: [`${formattedBrandName} Uzmanlık Alanı`], extractedSectors: [`${formattedBrandName} Uzmanlık Alanı`],
extractedProducts: [ extractedProducts: [
`${formattedBrandName} Kurumsal Ürün Serisi`, `${formattedBrandName} Kurumsal Ürün Serisi`,