Spaces:
Sleeping
Sleeping
| import { demoVisits, type DemoVisit } from '@/lib/mock-data'; | |
| import type { TravelPlan } from '@/lib/travel-planner'; | |
| export const OPS_STORAGE_KEY = 'ajet360.ops.v1'; | |
| export const OPS_STORAGE_EVENT = 'ajet360:ops-storage'; | |
| export type VisitTarget = { | |
| owner: string; | |
| period: string; | |
| target: number; | |
| setBy: 'self' | 'manager'; | |
| setAt: string; | |
| }; | |
| export type ContractType = 'Komisyon' | 'Garanti' | 'Özel Anlaşma'; | |
| export type ContractStatus = | |
| | 'Taslak' | |
| | 'İç İmzada' | |
| | 'Acente İmzasında' | |
| | 'Yürürlükte' | |
| | 'Süresi Doldu' | |
| | 'Arşiv'; | |
| export type SignerRole = 'Satış Müdürü' | 'Genel Müdür' | 'Muhasebe' | 'Acente'; | |
| export type InternalSignerRole = Exclude<SignerRole, 'Acente'>; | |
| export type ContractSignature = { | |
| role: SignerRole; | |
| signerName: string; | |
| signedAt: string; | |
| ip?: string; | |
| }; | |
| export type ContractEventAction = | |
| | 'created' | |
| | 'sent_internal' | |
| | 'signed_internal' | |
| | 'sent_external' | |
| | 'signed_external' | |
| | 'activated' | |
| | 'archived'; | |
| export type ContractEvent = { | |
| at: string; | |
| actor: string; | |
| action: ContractEventAction; | |
| note?: string; | |
| }; | |
| export type Contract = { | |
| id: string; | |
| agencyCode: string; | |
| agencyName: string; | |
| type: ContractType; | |
| startDate: string; | |
| endDate: string; | |
| commissionPct: number; | |
| status: ContractStatus; | |
| internalOrder: InternalSignerRole[]; | |
| signatures: ContractSignature[]; | |
| events: ContractEvent[]; | |
| createdAt: string; | |
| createdBy: string; | |
| }; | |
| export const INTERNAL_SIGNER_ORDER: InternalSignerRole[] = ['Satış Müdürü', 'Genel Müdür', 'Muhasebe']; | |
| export const INTERNAL_SIGNER_NAMES: Record<InternalSignerRole, string> = { | |
| 'Satış Müdürü': 'Mehmet Yılmaz', | |
| 'Genel Müdür': 'Ayşe Kaya', | |
| 'Muhasebe': 'Burak Demir', | |
| }; | |
| export type OpsStorageState = { | |
| visits: DemoVisit[]; | |
| targets?: VisitTarget[]; | |
| lastTravelPlan?: TravelPlan; | |
| contracts?: Contract[]; | |
| }; | |
| const EMPTY_STATE: OpsStorageState = { | |
| visits: [], | |
| targets: [], | |
| contracts: [], | |
| }; | |
| function canUseStorage() { | |
| return typeof window !== 'undefined' && Boolean(window.localStorage); | |
| } | |
| function readState(): OpsStorageState { | |
| if (!canUseStorage()) { | |
| return EMPTY_STATE; | |
| } | |
| const raw = window.localStorage.getItem(OPS_STORAGE_KEY); | |
| if (!raw) { | |
| return EMPTY_STATE; | |
| } | |
| try { | |
| const parsed = JSON.parse(raw) as Partial<OpsStorageState>; | |
| return { | |
| ...parsed, | |
| visits: Array.isArray(parsed.visits) ? parsed.visits : [], | |
| targets: Array.isArray(parsed.targets) ? parsed.targets : [], | |
| contracts: Array.isArray(parsed.contracts) ? parsed.contracts : [], | |
| }; | |
| } catch { | |
| return EMPTY_STATE; | |
| } | |
| } | |
| function writeState(state: OpsStorageState) { | |
| if (!canUseStorage()) { | |
| return; | |
| } | |
| window.localStorage.setItem(OPS_STORAGE_KEY, JSON.stringify(state)); | |
| window.dispatchEvent(new Event(OPS_STORAGE_EVENT)); | |
| } | |
| function getNextTripBaseDate() { | |
| const date = new Date(); | |
| date.setDate(date.getDate() + 1); | |
| date.setHours(9, 30, 0, 0); | |
| return date; | |
| } | |
| function getTravelPlanBaseDate(plan: TravelPlan) { | |
| if (!plan.input.startDate) { | |
| return getNextTripBaseDate(); | |
| } | |
| const date = new Date(`${plan.input.startDate}T09:30:00`); | |
| return Number.isNaN(date.getTime()) ? getNextTripBaseDate() : date; | |
| } | |
| function planTimestamp(plan: TravelPlan) { | |
| return plan.generatedAt.replace(/\D/g, '').slice(0, 14); | |
| } | |
| export function getStoredVisits() { | |
| return readState().visits; | |
| } | |
| export function getAllVisits() { | |
| return [...demoVisits, ...getStoredVisits()].sort( | |
| (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), | |
| ); | |
| } | |
| export function saveTravelPlanVisits(plan: TravelPlan) { | |
| const state = readState(); | |
| const baseDate = getTravelPlanBaseDate(plan); | |
| const timestamp = planTimestamp(plan); | |
| const participants = plan.participants.length > 0 ? plan.participants : plan.input.participants; | |
| const owner = participants[0] ?? `${plan.input.city} Saha Ekibi`; | |
| const participantsNote = participants.length > 0 ? ` Katilimcilar: ${participants.join(', ')}.` : ''; | |
| const createdVisits: DemoVisit[] = plan.stops.map((stop) => { | |
| const date = new Date(baseDate); | |
| const dayOffset = stop.day - 1; | |
| const hourOffset = (stop.order - 1) * 2; | |
| date.setDate(baseDate.getDate() + dayOffset); | |
| date.setHours(9 + hourOffset, 30, 0, 0); | |
| return { | |
| id: `travel-${timestamp}-${stop.day}-${stop.order}-${stop.agencyCode}`, | |
| agencyCode: stop.agencyCode, | |
| agencyName: stop.agencyName, | |
| city: plan.input.city, | |
| owner, | |
| date: date.toISOString(), | |
| type: 'Yuz yuze', | |
| status: 'Planli', | |
| note: `Seyahat plani: Gun ${stop.day}, sira ${stop.order}. ${stop.reason}.${participantsNote}`, | |
| }; | |
| }); | |
| const createdIds = new Set(createdVisits.map((visit) => visit.id)); | |
| const visits = [...state.visits.filter((visit) => !createdIds.has(visit.id)), ...createdVisits]; | |
| writeState({ | |
| ...state, | |
| visits, | |
| lastTravelPlan: plan, | |
| }); | |
| return createdVisits; | |
| } | |
| export function addVisit(visit: Omit<DemoVisit, 'id'>): DemoVisit { | |
| const id = `local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; | |
| const created: DemoVisit = { ...visit, id }; | |
| const state = readState(); | |
| writeState({ ...state, visits: [...state.visits, created] }); | |
| return created; | |
| } | |
| export function updateVisit(id: string, patch: Partial<DemoVisit>): void { | |
| const state = readState(); | |
| const idx = state.visits.findIndex((v) => v.id === id); | |
| if (idx < 0) { | |
| const fromDemo = demoVisits.find((v) => v.id === id); | |
| if (!fromDemo) return; | |
| writeState({ ...state, visits: [...state.visits, { ...fromDemo, ...patch, id }] }); | |
| return; | |
| } | |
| const current = state.visits[idx]!; | |
| const next = [...state.visits]; | |
| next[idx] = { ...current, ...patch }; | |
| writeState({ ...state, visits: next }); | |
| } | |
| export function getVisitById(id: string): DemoVisit | undefined { | |
| const stored = readState().visits.find((v) => v.id === id); | |
| if (stored) return stored; | |
| return demoVisits.find((v) => v.id === id); | |
| } | |
| export function listVisitTargets(): VisitTarget[] { | |
| return readState().targets ?? []; | |
| } | |
| export function getVisitTarget(owner: string, period: string): VisitTarget | undefined { | |
| return listVisitTargets().find((t) => t.owner === owner && t.period === period); | |
| } | |
| export function setVisitTarget(input: Omit<VisitTarget, 'setAt'>): VisitTarget { | |
| const state = readState(); | |
| const targets = state.targets ?? []; | |
| const next: VisitTarget = { ...input, setAt: new Date().toISOString() }; | |
| const idx = targets.findIndex((t) => t.owner === input.owner && t.period === input.period); | |
| const merged = idx >= 0 ? targets.map((t, i) => (i === idx ? next : t)) : [...targets, next]; | |
| writeState({ ...state, targets: merged }); | |
| return next; | |
| } | |
| export function periodKey(date: Date = new Date()): string { | |
| return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; | |
| } | |
| export function buildVisitSummaryMailto(visit: DemoVisit): string { | |
| const recipient = `${visit.agencyCode.toLowerCase()}@acente.ajet.com.tr`; | |
| const subject = `AJet · Toplantı Özeti — ${visit.agencyName}`; | |
| const date = new Date(visit.date).toLocaleString('tr-TR', { | |
| dateStyle: 'long', | |
| timeStyle: 'short', | |
| }); | |
| const sentimentLine = visit.sentiment | |
| ? `\nGörüşme tonu: ${visit.sentiment === 'POZITIF' ? 'Pozitif' : visit.sentiment === 'NEGATIF' ? 'Negatif' : 'Nötr'}\n` | |
| : ''; | |
| const actions = (visit.nextActions ?? []).filter(Boolean); | |
| const actionsBlock = actions.length | |
| ? `\nSonraki Adımlar:\n${actions.map((a) => `• ${a}`).join('\n')}\n` | |
| : ''; | |
| const body = [ | |
| `Sayın ${visit.agencyName} ekibi,`, | |
| '', | |
| `${date} tarihinde gerçekleştirdiğimiz görüşmenin özetini bilgilerinize sunuyoruz.${sentimentLine}`, | |
| visit.postNote || visit.note || 'Görüşme özeti hazırlanıyor.', | |
| actionsBlock, | |
| 'Her türlü soru ve geri dönüşünüz için ulaşabilirsiniz.', | |
| '', | |
| 'Saygılarımızla,', | |
| 'AJet Acente İlişkileri', | |
| ].join('\n'); | |
| return `mailto:${recipient}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; | |
| } | |
| const DEMO_CONTRACTS: Contract[] = [ | |
| { | |
| id: 'demo-contract-001', | |
| agencyCode: 'AJ-IST-001', | |
| agencyName: 'Istanbul Jet Travel', | |
| type: 'Komisyon', | |
| startDate: '2026-01-01T00:00:00.000Z', | |
| endDate: '2026-12-31T00:00:00.000Z', | |
| commissionPct: 14, | |
| status: 'Acente İmzasında', | |
| internalOrder: ['Satış Müdürü', 'Genel Müdür', 'Muhasebe'], | |
| signatures: [ | |
| { role: 'Satış Müdürü', signerName: 'Mehmet Yılmaz', signedAt: '2026-01-02T09:15:00.000Z', ip: 'demo-127.0.0.1' }, | |
| { role: 'Genel Müdür', signerName: 'Ayşe Kaya', signedAt: '2026-01-02T11:30:00.000Z', ip: 'demo-127.0.0.1' }, | |
| { role: 'Muhasebe', signerName: 'Burak Demir', signedAt: '2026-01-03T08:45:00.000Z', ip: 'demo-127.0.0.1' }, | |
| ], | |
| events: [ | |
| { at: '2026-01-02T09:00:00.000Z', actor: 'Sistem Kullanıcısı', action: 'created', note: 'Sözleşme oluşturuldu (Komisyon, %14 komisyon)' }, | |
| { at: '2026-01-02T09:15:00.000Z', actor: 'Mehmet Yılmaz', action: 'signed_internal', note: 'Satış Müdürü (Mehmet Yılmaz) e-imza ile imzaladı' }, | |
| { at: '2026-01-02T11:30:00.000Z', actor: 'Ayşe Kaya', action: 'signed_internal', note: 'Genel Müdür (Ayşe Kaya) e-imza ile imzaladı' }, | |
| { at: '2026-01-03T08:45:00.000Z', actor: 'Burak Demir', action: 'signed_internal', note: 'Muhasebe (Burak Demir) e-imza ile imzaladı' }, | |
| { at: '2026-01-03T09:00:00.000Z', actor: 'Sistem Kullanıcısı', action: 'sent_external', note: 'Acenteye e-imza linki gönderildi (Istanbul Jet Travel)' }, | |
| ], | |
| createdAt: '2026-01-02T09:00:00.000Z', | |
| createdBy: 'Sistem Kullanıcısı', | |
| }, | |
| { | |
| id: 'demo-contract-002', | |
| agencyCode: 'AJ-ANK-001', | |
| agencyName: 'Ankara Tur Merkezi', | |
| type: 'Garanti', | |
| startDate: '2025-07-01T00:00:00.000Z', | |
| endDate: '2026-06-30T00:00:00.000Z', | |
| commissionPct: 10, | |
| status: 'Yürürlükte', | |
| internalOrder: ['Satış Müdürü', 'Genel Müdür', 'Muhasebe'], | |
| signatures: [ | |
| { role: 'Satış Müdürü', signerName: 'Mehmet Yılmaz', signedAt: '2025-06-28T10:00:00.000Z', ip: 'demo-127.0.0.1' }, | |
| { role: 'Genel Müdür', signerName: 'Ayşe Kaya', signedAt: '2025-06-28T14:00:00.000Z', ip: 'demo-127.0.0.1' }, | |
| { role: 'Muhasebe', signerName: 'Burak Demir', signedAt: '2025-06-29T09:30:00.000Z', ip: 'demo-127.0.0.1' }, | |
| { role: 'Acente', signerName: 'Ankara Tur Merkezi Yetkilisi', signedAt: '2025-07-01T08:00:00.000Z', ip: 'demo-127.0.0.1' }, | |
| ], | |
| events: [ | |
| { at: '2025-06-27T09:00:00.000Z', actor: 'Sistem Kullanıcısı', action: 'created', note: 'Sözleşme oluşturuldu (Garanti, %10 komisyon)' }, | |
| { at: '2025-06-28T10:00:00.000Z', actor: 'Mehmet Yılmaz', action: 'signed_internal', note: 'Satış Müdürü (Mehmet Yılmaz) e-imza ile imzaladı' }, | |
| { at: '2025-06-28T14:00:00.000Z', actor: 'Ayşe Kaya', action: 'signed_internal', note: 'Genel Müdür (Ayşe Kaya) e-imza ile imzaladı' }, | |
| { at: '2025-06-29T09:30:00.000Z', actor: 'Burak Demir', action: 'signed_internal', note: 'Muhasebe (Burak Demir) e-imza ile imzaladı' }, | |
| { at: '2025-06-30T10:00:00.000Z', actor: 'Sistem Kullanıcısı', action: 'sent_external', note: 'Acenteye e-imza linki gönderildi (Ankara Tur Merkezi)' }, | |
| { at: '2025-07-01T08:00:00.000Z', actor: 'Ankara Tur Merkezi Yetkilisi', action: 'signed_external', note: 'Ankara Tur Merkezi (Ankara Tur Merkezi Yetkilisi) e-imza ile imzaladı' }, | |
| { at: '2025-07-01T08:01:00.000Z', actor: 'Sistem', action: 'activated', note: 'Sözleşme yürürlüğe girdi' }, | |
| ], | |
| createdAt: '2025-06-27T09:00:00.000Z', | |
| createdBy: 'Sistem Kullanıcısı', | |
| }, | |
| ]; | |
| export function listContracts(): Contract[] { | |
| return readState().contracts ?? []; | |
| } | |
| export function getAllContracts(): Contract[] { | |
| const stored = listContracts(); | |
| const storedIds = new Set(stored.map((c) => c.id)); | |
| return [...DEMO_CONTRACTS.filter((c) => !storedIds.has(c.id)), ...stored].sort( | |
| (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), | |
| ); | |
| } | |
| export function getContractById(id: string): Contract | undefined { | |
| const stored = listContracts().find((c) => c.id === id); | |
| if (stored) return stored; | |
| return DEMO_CONTRACTS.find((c) => c.id === id); | |
| } | |
| export type CreateContractInput = { | |
| agencyCode: string; | |
| agencyName: string; | |
| type: ContractType; | |
| startDate: string; | |
| endDate: string; | |
| commissionPct: number; | |
| createdBy?: string; | |
| }; | |
| export function createContract(input: CreateContractInput): Contract { | |
| const state = readState(); | |
| const contracts = state.contracts ?? []; | |
| const now = new Date().toISOString(); | |
| const ts = now.replace(/\D/g, '').slice(0, 14); | |
| const createdBy = input.createdBy ?? 'Sistem Kullanıcısı'; | |
| const contract: Contract = { | |
| id: `contract-${ts}-${input.agencyCode}`, | |
| agencyCode: input.agencyCode, | |
| agencyName: input.agencyName, | |
| type: input.type, | |
| startDate: input.startDate, | |
| endDate: input.endDate, | |
| commissionPct: input.commissionPct, | |
| status: 'Taslak', | |
| internalOrder: [...INTERNAL_SIGNER_ORDER], | |
| signatures: [], | |
| events: [ | |
| { | |
| at: now, | |
| actor: createdBy, | |
| action: 'created', | |
| note: `Sözleşme oluşturuldu (${input.type}, ${input.commissionPct}% komisyon)`, | |
| }, | |
| ], | |
| createdAt: now, | |
| createdBy, | |
| }; | |
| writeState({ ...state, contracts: [contract, ...contracts] }); | |
| return contract; | |
| } | |
| function nextStatusAfterSign(contract: Contract): ContractStatus { | |
| const internalCount = contract.signatures.filter((s) => s.role !== 'Acente').length; | |
| const externalSigned = contract.signatures.some((s) => s.role === 'Acente'); | |
| if (externalSigned) return 'Yürürlükte'; | |
| if (internalCount >= contract.internalOrder.length) return 'Acente İmzasında'; | |
| if (internalCount > 0) return 'İç İmzada'; | |
| return 'Taslak'; | |
| } | |
| export function getNextSigner(contract: Contract): SignerRole | null { | |
| const signedRoles = new Set(contract.signatures.map((s) => s.role)); | |
| for (const role of contract.internalOrder) { | |
| if (!signedRoles.has(role)) return role; | |
| } | |
| if (!signedRoles.has('Acente')) return 'Acente'; | |
| return null; | |
| } | |
| export function signContract( | |
| id: string, | |
| role: SignerRole, | |
| signerName: string, | |
| ): Contract | undefined { | |
| const state = readState(); | |
| const contracts = state.contracts ?? []; | |
| const idx = contracts.findIndex((c) => c.id === id); | |
| if (idx < 0) return undefined; | |
| const contract = contracts[idx]!; | |
| if (contract.signatures.some((s) => s.role === role)) return contract; | |
| const expected = getNextSigner(contract); | |
| if (expected !== role) return contract; | |
| const now = new Date().toISOString(); | |
| const signature: ContractSignature = { | |
| role, | |
| signerName, | |
| signedAt: now, | |
| ip: 'demo-127.0.0.1', | |
| }; | |
| const events: ContractEvent[] = [ | |
| ...contract.events, | |
| { | |
| at: now, | |
| actor: signerName, | |
| action: role === 'Acente' ? 'signed_external' : 'signed_internal', | |
| note: | |
| role === 'Acente' | |
| ? `${contract.agencyName} (${signerName}) e-imza ile imzaladı` | |
| : `${role} (${signerName}) e-imza ile imzaladı`, | |
| }, | |
| ]; | |
| const next: Contract = { | |
| ...contract, | |
| signatures: [...contract.signatures, signature], | |
| events, | |
| }; | |
| next.status = nextStatusAfterSign(next); | |
| if (next.status === 'Yürürlükte' && !next.events.some((e) => e.action === 'activated')) { | |
| next.events = [ | |
| ...next.events, | |
| { | |
| at: now, | |
| actor: 'Sistem', | |
| action: 'activated', | |
| note: 'Sözleşme yürürlüğe girdi', | |
| }, | |
| ]; | |
| } | |
| const arr = [...contracts]; | |
| arr[idx] = next; | |
| writeState({ ...state, contracts: arr }); | |
| return next; | |
| } | |
| export function markContractSentExternal(id: string, actor: string): Contract | undefined { | |
| const state = readState(); | |
| const contracts = state.contracts ?? []; | |
| const idx = contracts.findIndex((c) => c.id === id); | |
| if (idx < 0) return undefined; | |
| const contract = contracts[idx]!; | |
| const now = new Date().toISOString(); | |
| const next: Contract = { | |
| ...contract, | |
| events: [ | |
| ...contract.events, | |
| { | |
| at: now, | |
| actor, | |
| action: 'sent_external', | |
| note: `Acenteye e-imza linki gönderildi (${contract.agencyName})`, | |
| }, | |
| ], | |
| }; | |
| const arr = [...contracts]; | |
| arr[idx] = next; | |
| writeState({ ...state, contracts: arr }); | |
| return next; | |
| } | |
| export function archiveContract(id: string, actor: string): Contract | undefined { | |
| const state = readState(); | |
| const contracts = state.contracts ?? []; | |
| const idx = contracts.findIndex((c) => c.id === id); | |
| if (idx < 0) return undefined; | |
| const contract = contracts[idx]!; | |
| const now = new Date().toISOString(); | |
| const next: Contract = { | |
| ...contract, | |
| status: 'Arşiv', | |
| events: [ | |
| ...contract.events, | |
| { at: now, actor, action: 'archived', note: 'Sözleşme arşive alındı' }, | |
| ], | |
| }; | |
| const arr = [...contracts]; | |
| arr[idx] = next; | |
| writeState({ ...state, contracts: arr }); | |
| return next; | |
| } | |
| export function buildContractMailto(contract: Contract): string { | |
| const recipient = `${contract.agencyCode.toLowerCase()}@acente.ajet.com.tr`; | |
| const subject = `AJet · Sözleşme E-İmza Talebi — ${contract.agencyName}`; | |
| const startTr = new Date(contract.startDate).toLocaleDateString('tr-TR'); | |
| const endTr = new Date(contract.endDate).toLocaleDateString('tr-TR'); | |
| const signLink = `https://demo.ajet360.local/sozlesmeler/${contract.id}?as=acente`; | |
| const body = [ | |
| `Sayın ${contract.agencyName} ekibi,`, | |
| '', | |
| 'AJet ile aranızda imzalanmak üzere hazırlanan sözleşme, tarafımızdaki tüm yetkililer tarafından e-imza ile imzalanmıştır. Sürecin tamamlanması için sizin de e-imza onayınızı bekliyoruz.', | |
| '', | |
| `Sözleşme tipi: ${contract.type}`, | |
| `Geçerlilik: ${startTr} → ${endTr}`, | |
| `Komisyon oranı: %${contract.commissionPct}`, | |
| '', | |
| 'Aşağıdaki bağlantı üzerinden tek tıkla e-imza atabilirsiniz:', | |
| signLink, | |
| '', | |
| 'Saygılarımızla,', | |
| 'AJet Acente İlişkileri', | |
| ].join('\n'); | |
| return `mailto:${recipient}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; | |
| } | |
| export function resetOpsStorage() { | |
| if (!canUseStorage()) { | |
| return; | |
| } | |
| window.localStorage.removeItem(OPS_STORAGE_KEY); | |
| window.dispatchEvent(new Event(OPS_STORAGE_EVENT)); | |
| } | |