add prefilled calendar provider links

This commit is contained in:
2026-09-06 19:57:35 +03:00
parent ecbcc2f646
commit 528aba16ae
4 changed files with 90 additions and 5 deletions
+35 -4
View File
@@ -1,5 +1,5 @@
import { TenderSourceHistory } from './TenderSourceHistory';
import React, { useState, useEffect, useId } from 'react';
import React, { useState, useEffect, useId, useMemo } from 'react';
import {useModalFocus} from '../hooks/useModalFocus';
import { useApp } from '../context/AppContext';
import { BidPackageBuilder } from './BidPackageBuilder';
@@ -12,7 +12,7 @@ import {
Send, Stamp, ArrowLeft, ExternalLink, Printer, PenLine } from 'lucide-react';
import { acknowledgeNoteDraft, editNoteDraft } from '../services/noteDrafts';
import { tenderStateKey } from '../services/tenderState';
import { createTenderICS } from '../services/tenderCalendar';
import { createTenderICS, googleCalendarUrl, outlookCalendarUrl } from '../services/tenderCalendar';
import { validateExchangeAmount, MAX_EXCHANGE_AMOUNT } from '../services/exchangeAmount';
export const TenderDetailModal = () => {
@@ -52,9 +52,20 @@ export const TenderDetailModal = () => {
const fxIsStale = Boolean(fx?.refreshError);
const bidAmountValid = validateExchangeAmount(bidAmount);
// Keep provider links deterministic during render; invalid source dates are
// reported only when the user tries to open a link.
const tender = (activeTenderDetail && tenders.find((t) => t.id === activeTenderDetail.id)) || activeTenderDetail;
const calendarUrls = useMemo(() => {
if (!tender) return { google: null, outlook: null, error: null };
try {
return { google: googleCalendarUrl(tender), outlook: outlookCalendarUrl(tender), error: null };
} catch (e) {
return { google: null, outlook: null, error: e.message };
}
}, [tender]);
if (!activeTenderDetail) return null;
// Havuzdaki güncel kayıt: takip/görev değişince düğme etiketleri de değişsin.
const tender = (activeTenderDetail && tenders.find((t) => t.id === activeTenderDetail.id)) || activeTenderDetail;
const savedNotes = tender.notes || [];
const handleAddNote = async (e) => {
@@ -83,6 +94,14 @@ export const TenderDetailModal = () => {
document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(link.href), 1000);
};
const handleCalendarOpen = (provider, event) => {
if (!calendarUrls[provider]) {
event.preventDefault();
setActionError(calendarUrls.error || 'Takvim bağlantısı oluşturulamadı.');
return;
}
setActionError(null);
};
return (
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-xs flex justify-center p-2 sm:p-4 animate-fade-in">
@@ -420,7 +439,19 @@ export const TenderDetailModal = () => {
>
<Calendar className="w-4 h-4 text-blue-600" /> Takvim Dosyasını İndir (.ics)
</button>
<p className="text-xs text-slate-600 mt-2">Bu dosya takvime otomatik senkron göndermez. Tarih veya saat değişirse yeni dosyayı indirip Google/Outlook kaydını güncelleyin; aynı etkinlik kimliği ve sürümü destekleniyorsa mevcut kayıt güncellenir.</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<a href={calendarUrls.google || '#'} target="_blank" rel="noopener noreferrer"
onClick={(event) => handleCalendarOpen('google', event)}
className="text-center bg-white hover:bg-blue-50 text-blue-700 font-bold py-2 px-3 rounded-lg text-xs border border-blue-200">
Google Takvim taslağı
</a>
<a href={calendarUrls.outlook || '#'} target="_blank" rel="noopener noreferrer"
onClick={(event) => handleCalendarOpen('outlook', event)}
className="text-center bg-white hover:bg-blue-50 text-blue-700 font-bold py-2 px-3 rounded-lg text-xs border border-blue-200">
Outlook taslağı
</a>
</div>
<p className="text-xs text-slate-600 mt-2">Google ve Outlook bağlantıları doğrulanmış tarih/saatle ön-doldurulmuş taslak açar; kaydetme ve değişiklik sonrası yeniden senkronizasyon kullanıcıya aittir. Otomatik OAuth senkronu yapılmaz.</p>
</div>
</div>
+37
View File
@@ -40,6 +40,43 @@ export function createTenderICS(t, now = new Date()) {
'END:VEVENT','END:VCALENDAR',''].map(fold).join('\r\n');
}
const calendarInstant = date => date.toISOString().replace(/\.000Z$/, 'Z');
const calendarText = t => [
`İKN: ${t.ikn || t.id || ''}`,
t.kurum || '',
'Kaynak sürümünü ve ihale tarihini resmi kaynaktan doğrulayın.',
].filter(Boolean).join('\n');
/** Google Calendar'a kaynak tarihiyle ön-doldurulmuş etkinlik taslağı açar. */
export function googleCalendarUrl(t) {
const start = tenderDeadline(t);
const instant = calendarInstant(start).replace(/[-:]/g, '');
const params = new URLSearchParams({
action: 'TEMPLATE',
text: `İhale son teklif: ${t.baslik || t.ikn || 'İhale'}`,
dates: `${instant}/${instant}`,
details: calendarText(t),
location: t.ihaleYeri || t.teslimYeri || '',
});
return `https://calendar.google.com/calendar/render?${params}`;
}
/** Outlook web takviminde kaynak tarihiyle ön-doldurulmuş taslak açar. */
export function outlookCalendarUrl(t) {
const start = tenderDeadline(t);
const instant = calendarInstant(start);
const params = new URLSearchParams({
path: '/calendar/action/compose',
rru: 'addevent',
startdt: instant,
enddt: instant,
subject: `İhale son teklif: ${t.baslik || t.ikn || 'İhale'}`,
body: calendarText(t),
location: t.ihaleYeri || t.teslimYeri || '',
});
return `https://outlook.live.com/calendar/0/deeplink/compose?${params}`;
}
function parseICSDate(value, params = '') {
const raw = String(value || '').trim();
const match = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})Z?)?$/.exec(raw);