proje / apps /web /src /lib /ops-storage.ts
rodopsin
Fix contract signer role typing
6e86bbb
Raw
History Blame Contribute Delete
18.6 kB
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));
}