Счёт — за 3 клика.
Деньги приходят сами.

Invoice in 3 clicks.
Get paid on time.

Rechnung in 3 Klicks.
Zahlung pünktlich.

Выставь счёт, отправь ссылку и получи оплату — картой, по СБП (QR) или по платёжной ссылке. Мы сами напомним, если клиент задержится.

Create an invoice, send the link, get paid by card or transfer. If your client runs late, we chase the payment — politely, in your name.

Rechnung erstellen, Link verschicken, bezahlt werden — per Karte oder Überweisung. Läuft es beim Kunden nicht rund, erinnern wir höflich — in Ihrem Namen.

3 кликадо первого счёта СБП / картаприём оплаты Разбор счетоввходящие в данные Без паролявход по почте
3 clicksto your first invoice Card / Apple Payget paid No passwordsign in by email Paste a PDFinvoice → clean data
3 Klicksbis zur ersten Rechnung Karte / SEPAGeld kommt an Ohne PasswortAnmeldung per E-Mail PDF reinRechnung → saubere Daten
Автоплатёж · настройки

Мы работаем со счетами. От выставления — до оплаты.

We handle invoices. From issuing to getting paid.

Wir arbeiten mit Rechnungen. Vom Ausstellen bis zur Zahlung.

DueBuddy — сервис для тех, кто работает на себя. Здесь только счета: выставить свой, получить деньги, разобрать чужой.

DueBuddy is built for people who work for themselves. Invoices only: issue yours, get paid, read someone else's.

DueBuddy ist für Menschen, die für sich selbst arbeiten. Nur Rechnungen: die eigene ausstellen, bezahlt werden, die fremde einlesen.

Late payment calculator

How much do late payments cost you?

Enter what you are owed right now — and see what overdue invoices cost you every month.

You are losing about
—
a month on overdue invoices and chasing
money: — · time: —
— a year
Formula: outstanding × share overdue × 24% a year (cost of money) + hours chasing × your rate. 24% is a typical credit-card / overdraft rate.

Универсальный — всё сразуAll-in-oneAlles in einem

Выставляю + Входящие в одном кабинете. Один список счетов — выставить свои и разобрать чужие.

Invoice, track and parse in one place. One list of invoices — the ones you send and the ones you receive.

Ausstellen, verfolgen und einlesen an einem Ort. Eine Liste — Ihre Rechnungen und die, die Sie bekommen.

Тарифы

Платите за то, чем пользуетесь

Основной

Выставляю и слежу

1 490 ₽/мес
—
Свои счета и напоминания по ним.

Слежу

390 ₽/мес
слежение за чужими
Слежение за счетами, выставленными не через нас: подтягиваем из почты и напоминаем. Вход через Google. Для рынка США.

Разбор входящих

690 ₽/мес
только входящие
Только разбор входящих: чужие счета в данные, дубли, выгрузка. Счета не выставляются.

Всё сразу

2 570 ₽/мес
всё вместе
Всё вместе: свои счета и напоминания, слежение за чужими счетами, разбор входящих. Выставление требует Lava.top.
СкороComing soonBald verfügbar

API распознавания счетовInvoice recognition APIAPI für die Rechnungserkennung

Присылайте счета программно: наша система распознаёт поля и возвращает данные. Оплата по страницам, объёмные пакеты для больших потоков. Send invoices programmatically: our engine recognises fields and returns structured data. Priced per page, with volume packages for large streams. Senden Sie Rechnungen programmatisch: unser System erkennt die Felder und liefert strukturierte Daten. Abrechnung pro Seite, Volumenpakete für große Mengen.

PricingPreise

Free while we are in betaIn der Beta kostenlos

Beta access is free. Pricing is announced before launch — you will see it here and in the app first.Die Beta ist kostenlos. Preise nennen wir vor dem Start — hier und in der App zuerst.

Партнёрская программаReferral programmeEmpfehlungsprogramm

Приведи друга — получай 20% каждый месяц

Refer a friend — earn 20% every month

Freunde werben — 20 % jeden Monat

20% от подписки каждого приведённого пользователя — пока он платит. Начисляем в конце месяца. Для выплат нужен аккаунт в Lava.top.

20% of every referred user's subscription, for as long as they keep paying. Paid out monthly. A Lava.top account is required for payouts.

20 % vom Abo jedes geworbenen Nutzers, solange er zahlt. Auszahlung monatlich. Für Auszahlungen ist ein Lava.top-Konto nötig.

20%с каждого приведённого пользователя, каждый месяцof every referred user, every monthpro geworbenem Nutzer, monatlich
Ссылка — только для пользователей сервиса
Your link — for registered users only
Ihr Link — nur für angemeldete Nutzer

Войдите в кабинет — там появится ваша персональная партнёрская ссылка: скопировать или отправить другу.

Sign in and your personal referral link appears in the app: copy it or send it to a friend.

Melden Sie sich an — Ihr persönlicher Empfehlungslink erscheint in der App: kopieren oder an Freunde senden.

1 Поделитесь ссылкойShare your linkLink teilen с другом или в чатеwith a friend or in a chatan Freunde oder im Chat
2 Друг подключитсяThey sign upSie melden sich an и начнёт выставлять счетаand start sending invoicesund verschicken Rechnungen
3 Получайте 20%You earn 20%Sie verdienen 20 % с его подписки, пока он платитof their subscription, monthlyvom Abo, monatlich
ВопросыFAQFragen

Частые вопросы

Common questions

Häufige Fragen

Что именно делает DueBuddy?

Работает со счетами: помогает выставить счёт и получить оплату и разбирает входящие счета. Два модуля — по отдельности или вместе.

Как клиент оплачивает?

Способ выбирается при создании счёта: СБП по QR или картой. Для клиентов из-за границы — карта, Apple Pay, SEPA или PayPal.

Можно ли платить криптой?

Нет. Приём крипты мы не делаем — это принципиально. Крипта (USDT) — только один из способов вывода ваших денег.

Как я получу свои деньги?

Деньги приходят в ваш аккаунт Lava.top, а не к нам. Вывод — на карту физлица или самозанятого, либо USDT (крипта). Вывод открывается после 3 принятых платежей, плюс до 7 дней проверки.

Какие форматы разбираете?

PDF с текстовым слоем, фото через OCR, XLSX и CSV.

Что если клиент не платит?

Тон напоминаний нарастает: от дружеского до формального. Всё вежливо, от вашего имени.

Нужна ли онлайн-касса?

Нет. Для самозанятых (НПД) касса не требуется — чек формируется в приложении «Мой налог».

Можно ли вернуть оплату?

Да, однократно в течение 2 часов с момента оплаты, если услуга не была использована.

What exactly does DueBuddy do?

It works with invoices: it helps you issue one and get paid, and it turns incoming invoices into data. Two modules — separately or together.

How does my client pay?

You pick the method when you create the invoice: card, Apple Pay, SEPA or PayPal for clients abroad.

Can buyers pay in crypto?

No. We do not accept crypto payments — by design. Crypto (USDT) is only one way to withdraw your money.

How do I actually get my money?

The money lands in your own Lava.top account, not with us. Payouts go to a personal or sole-trader card, or as USDT (crypto). Payouts open after 3 accepted payments, plus up to 7 days of checks.

Which formats can you parse?

PDF with a text layer, photos via OCR, XLSX and CSV.

What if the client does not pay?

Reminders escalate from friendly to formal — always polite, in your name.

Is there a cancellation window?

Yes — one refund within 2 hours of payment, if the service has not been used.

Who can use it?

People who invoice by themselves — freelancers, sole traders, small studios. Rules depend on your country.

Was macht DueBuddy genau?

Es arbeitet mit Rechnungen: Sie stellen eine aus und werden bezahlt — und wir lesen Eingangsrechnungen in Daten um. Zwei Module, einzeln oder zusammen.

Wie zahlt mein Kunde?

Sie wählen die Zahlungsart beim Erstellen der Rechnung: Karte, Apple Pay, SEPA oder PayPal bei Kunden im Ausland.

Kann man mit Krypto bezahlen?

Nein. Krypto nehmen wir nicht als Zahlung an — bewusst nicht. Krypto (USDT) ist nur ein Weg, Ihr Geld auszuzahlen.

Wie bekomme ich mein Geld?

Das Geld landet in Ihrem eigenen Lava.top-Konto, nicht bei uns. Auszahlung auf Karte (Privatperson oder Kleinunternehmer) oder USDT (Krypto). Auszahlung ab 3 angenommenen Zahlungen, plus bis zu 7 Tage Prüfung.

Welche Formate lesen Sie?

PDF mit Textebene, Fotos per OCR, XLSX und CSV. Für Deutschland zusätzlich XRechnung und ZUGFeRD.

Was, wenn der Kunde nicht zahlt?

Die Erinnerungen werden bestimmter: erst freundlich, dann förmlich. Immer höflich, in Ihrem Namen.

Brauche ich eine Kasse?

Nein. Sie stellen als Freiberufler, Kleinunternehmer oder Einzelunternehmer Rechnungen.

Gibt es eine Rückerstattung?

Ja, einmalig innerhalb von 2 Stunden nach Zahlung, wenn die Leistung nicht genutzt wurde.

Новый счёт
'; return dlBlob(base+'.doc','application/msword;charset=utf-8',html); } if(kind==='pdf'){ const w=window.open('','_blank'); if(!w){alert('Разрешите всплывающие окна для PDF');return} w.document.write(''+d.title+''+ ''+ '

'+d.title+'

'+d.cols.map(c=>'').join('')+''+ d.rows.map(r=>''+r.map(v=>'').join('')+'').join('')+ '
'+esc2(c)+'
'+esc2(v)+'

Сохраните как PDF: Печать → «Сохранить как PDF».

'); w.document.close(); setTimeout(function(){try{w.print()}catch(e){}},350); return; } } function exportCsv(kind){ const rows=demoRows(); let text,type,name; if(kind==='json'){text=JSON.stringify({vendor:rows[0].v,amount:rows[1].v,number:rows[2].v,duplicate:true},null,2);type='application/json';name='invoice-parsed.json'} else{const q=s=>'"'+String(s).replace(/"/g,'""')+'"';text='\uFEFF'+rows.map(r=>q(r.k)+';'+q(r.v)).join('\r\n');type='text/csv;charset=utf-8';name='invoice-parsed.csv'} try{const blob=new Blob([text],{type});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();setTimeout(()=>{URL.revokeObjectURL(a.href);a.remove()},500)}catch(e){} } /* ================= ДАННЫЕ КАБИНЕТА ================= */ const CARDS={ ru:[{id:1042,no:'Счёт № 1042',client:'Иван Петров',item:'Разработка логотипа',sum:'12 500 ₽',due:'срок завтра',stamp:'sent',sl:'Отправлен'}, {id:1037,no:'Счёт № 1037',client:'ООО «Студия»',item:'Печать буклетов',sum:'38 000 ₽',due:'просрочен на 5 дней',stamp:'overdue',sl:'Просрочен'}, {id:1031,no:'Счёт № 1031',client:'Мария С.',item:'Стрижка газона',sum:'7 200 ₽',due:'оплачен',stamp:'paid',sl:'Оплачен'}], en:[{id:1042,no:'Invoice #1042',client:'John Carter',item:'Logo design',sum:'€1 250',due:'due tomorrow',stamp:'sent',sl:'Sent'}, {id:1037,no:'Invoice #1037',client:'Studio LLC',item:'Booklet print',sum:'€3 800',due:'5 days overdue',stamp:'overdue',sl:'Overdue'}, {id:1031,no:'Invoice #1031',client:'Maria S.',item:'Garden work',sum:'€720',due:'paid',stamp:'paid',sl:'Paid'}], de:[{id:1042,no:'Rechnung Nr. 1042',client:'Max Müller GmbH',item:'Logo-Design',sum:'1 250 €',due:'fällig morgen',stamp:'sent',sl:'Versendet'}, {id:1037,no:'Rechnung Nr. 1037',client:'Studio GmbH',item:'Drucksachen',sum:'3 800 €',due:'5 Tage überfällig',stamp:'overdue',sl:'Überfällig'}, {id:1031,no:'Rechnung Nr. 1031',client:'Maria S.',item:'Gartenarbeit',sum:'720 €',due:'bezahlt',stamp:'paid',sl:'Bezahlt'}] }; const DRAFT={ru:{client:'',item:'',sum:'',cur:'₽',due:'1 день'},en:{client:'',item:'',sum:'',cur:'€',due:'1 день'},de:{client:'',item:'',sum:'',cur:'€',due:'1 день'}}; const NEWC=[]; function cards(){return NEWC.concat(CARDS[lang]||CARDS.ru)} /* тон вежливости (как в движке догона: loyal → neutral → firm) и план напоминаний. Демо-значения: в продукте это считает движок из due_date и истории писем. */ const TONEM={1042:{t:'loy',od:0,next:'15.09'},1037:{t:'neu',od:5,next:'18.09'},1031:{t:'firm',od:0,next:'—'}}; const TONEL={loy:{ru:'лояльный (дружеский)',en:'loyal (friendly)',de:'loyal (freundlich)'}, neu:{ru:'средний (деловой)',en:'neutral (businesslike)',de:'neutral (sachlich)'}, firm:{ru:'жёсткий (формальный)',en:'firm (formal)',de:'firm (förmlich)'}}; const LETTERS={'ru': {'loy': 'Здравствуйте, {name}! Напоминаю про счёт № {no} на {sum}, срок — {d}. Если уже оплатили — спасибо, письмо можно не читать.', 'neu': 'Здравствуйте, {name}. Счёт № {no} на {sum} ждёт оплаты, срок истёк {d}. Подскажите, всё ли в порядке с оплатой?', 'firm': 'Здравствуйте, {name}. По счёту № {no} на {sum} срок оплаты истёк {d}. Прошу оплатить в течение 3 рабочих дней.'}, 'en': {'loy': 'Hi {name}, a quick reminder about invoice #{no} for {sum}, due {d}. If it is already paid, please ignore this note.', 'neu': 'Hello {name}. Invoice #{no} for {sum} is still awaiting payment, it was due {d}. Is everything fine with the payment?', 'firm': 'Hello {name}. Invoice #{no} for {sum} is now overdue ({d}). Please arrange payment within 3 business days.'}, 'de': {'loy': 'Hallo {name}, kurze Erinnerung an Rechnung Nr. {no} über {sum}, fällig {d}. Falls schon bezahlt, einfach ignorieren.', 'neu': 'Guten Tag {name}. Rechnung Nr. {no} über {sum} ist noch offen, fällig war {d}. Ist mit der Zahlung alles in Ordnung?', 'firm': 'Guten Tag {name}. Rechnung Nr. {no} über {sum} ist seit {d} überfällig. Bitte zahlen Sie innerhalb von 3 Werktagen.'}}; const TONEASK={loy:{ru:'Вежливое напоминание',en:'Friendly reminder',de:'Freundliche Erinnerung'}, neu:{ru:'Деловое напоминание',en:'Businesslike reminder',de:'Sachliche Erinnerung'}, firm:{ru:'Формальное требование',en:'Formal notice',de:'Förmliche Aufforderung'}}; function toneOf(c){const m=TONEM[c.id]||{};return c.tone||m.t||'loy'} function odOf(c){const m=TONEM[c.id]||{};return (c.od!==undefined?c.od:m.od)||0} function nextOf(c){return c.next!==undefined?c.next:(TONEM[c.id]||{}).next||'—'} function toneDot(c){return ``} /* ================= ПРИЛОЖЕНИЕ ================= */ let appStep='auth', currentCabTab=0, userEmail='', userName='', afterAuth='invoice', detailIdx=-1; function startApp(){afterAuth='invoice';show('app');gotoAuth()} function startRef(){afterAuth='ref';show('app');gotoAuth()} function rerenderApp(){ if(appStep==='invoice'){ const g=id=>(document.getElementById(id)||{}).value; const v={client:g('inp-client'),item:g('inp-item'),sum:g('inp-sum'),cur:g('inp-cur'),due:g('inp-due')}; const cc=g('ccSel'),mt=method; gotoInvoice(v); const cs=document.getElementById('ccSel'); if(cs&&cc){cs.value=cc;renderMethods()} const ch=document.querySelector('#inp-meth .chip[data-v="'+mt+'"]'); if(ch&&!ch.disabled)pickMethodSilent(mt); } else if(appStep==='cabinet'){cabTab(currentCabTab<0?0:currentCabTab)} else if(appStep==='ref'){gotoRef(false)} } function gotoAuth(){ appStep='auth';$('#appTitle').textContent=t('authTitle');$('#bnav').style.display='none'; $('#appContent').innerHTML=`

${t('authH1')}

${t('authP')}

${t('emailErr')}

${t('noPass')}

`; setTimeout(()=>{const i=$('#inp-email');if(i)i.focus()},50); } function gmailNote(){const n=document.getElementById('gmailNote');if(!n)return;n.style.display='block';n.textContent=t('gmailNote')} function submitEmail(){ const v=$('#inp-email').value.trim(); if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)){$('#f-email').classList.add('has-error');return} $('#f-email').classList.remove('has-error');userEmail=v;loadState();ymGoal('signup');gotoCheck(); } function gotoCheck(){ appStep='check';$('#appTitle').textContent=t('checkTitle');$('#bnav').style.display='none'; $('#appContent').innerHTML=`

${t('checkH1')}

${t('checkP')} ${esc(userEmail)}. ${t('checkP2')}

DueBuddy${t('ecSubj')}
${t('ecBody')}
${t('ecNote')}
`; } /* имя/фамилия — необязательный шаг */ function gotoName(){ appStep='name';$('#appTitle').textContent=t('nameH');$('#bnav').style.display='none'; $('#appContent').innerHTML=`

${t('nameH')}

${t('nameP')}

`; } function saveName(){userName=(($('#inp-fn').value||'').trim()+' '+(($('#inp-ln').value||'').trim())).trim();afterName()} /* ---- счёт ---- */ function curOptions(cur){return ['₽','$','€'].map(x=>``).join('')} function dueOptions(due){return ['1 день','3 дня','5 дней','7 дней','14 дней','30 дней','по факту получения'].map(v=>``).join('')} /* ===== ФОРМА СЧЁТА: откуда платит клиент -> доступные способы ===== Правило: одна ось выбора (страна плательщика). Россия — отдельной плиткой. Показываем ТОЛЬКО подтверждённые для этой страны способы (факты Lava.top). */ const METH={ sbp:{ru:'СБП по QR',en:'SBP by QR',de:'SBP per QR'}, mir:{ru:'Карта «Мир»',en:'Mir card',de:'Mir-Karte'}, card:{ru:'Карта Visa / Mastercard',en:'Visa / Mastercard',de:'Visa / Mastercard'}, applepay:{ru:'Apple Pay',en:'Apple Pay',de:'Apple Pay'}, sepa:{ru:'Перевод SEPA',en:'SEPA transfer',de:'SEPA-Überweisung'}, paypal:{ru:'PayPal',en:'PayPal',de:'PayPal'}, pix:{ru:'Pix',en:'Pix',de:'Pix'}, ideal:{ru:'iDEAL',en:'iDEAL',de:'iDEAL'} }; /* Список стран плательщика. tier:'conf' — способ подтверждён по документации Lava.top, 'intl' — международный рельс (карта Visa/MC, Apple Pay, PayPal); точный набор для страны подтверждаем при создании счёта. Крипта нигде не предлагается. */ const PAY_RAW=[ ['ru','🇷🇺','₽','sbp,mir','conf','main','Россия','Russia','Russland'], ['de','🇩🇪','€','card,applepay,sepa,paypal','conf','main','Германия','Germany','Deutschland'], ['at','🇦🇹','€','card,applepay,sepa,paypal','conf','main','Австрия','Austria','Österreich'], ['ch','🇨🇭','€','card,applepay,sepa,paypal','intl','main','Швейцария','Switzerland','Schweiz'], ['nl','🇳🇱','€','ideal','conf','main','Нидерланды','Netherlands','Niederlande'], ['es','🇪🇸','€','bizum','conf','main','Испания','Spain','Spanien'], ['pt','🇵🇹','€','mbway','conf','main','Португалия','Portugal','Portugal'], ['be','🇧🇪','€','bancontact','conf','main','Бельгия','Belgium','Belgien'], ['gb','🇬🇧','$','card,applepay,paypal','intl','main','Великобритания','United Kingdom','Vereinigtes Königreich'], ['us','🇺🇸','$','card,applepay,paypal','conf','main','США','United States','USA'], ['ca','🇨🇦','$','card,applepay,paypal','intl','main','Канада','Canada','Kanada'], ['au','🇦🇺','$','card,applepay,paypal','intl','main','Австралия','Australia','Australien'], ['br','🇧🇷','$','pix','conf','main','Бразилия','Brazil','Brasilien'], ['it','🇮🇹','€','card,applepay,sepa,paypal','intl','eu','Италия','Italy','Italien'], ['fr','🇫🇷','€','card,applepay,sepa,paypal','intl','eu','Франция','France','Frankreich'], ['pl','🇵🇱','€','card,applepay,sepa,paypal','intl','eu','Польша','Poland','Polen'], ['cz','🇨🇿','€','card,applepay,sepa,paypal','intl','eu','Чехия','Czechia','Tschechien'], ['sk','🇸🇰','€','card,applepay,sepa,paypal','intl','eu','Словакия','Slovakia','Slowakei'], ['hu','🇭🇺','€','card,applepay,sepa,paypal','intl','eu','Венгрия','Hungary','Ungarn'], ['ro','🇷🇴','€','card,applepay,sepa,paypal','intl','eu','Румыния','Romania','Rumänien'], ['bg','🇧🇬','€','card,applepay,sepa,paypal','intl','eu','Болгария','Bulgaria','Bulgarien'], ['gr','🇬🇷','€','card,applepay,sepa,paypal','intl','eu','Греция','Greece','Griechenland'], ['ie','🇮🇪','€','card,applepay,sepa,paypal','intl','eu','Ирландия','Ireland','Irland'], ['dk','🇩🇰','€','card,applepay,sepa,paypal','intl','eu','Дания','Denmark','Dänemark'], ['se','🇸🇪','€','card,applepay,sepa,paypal','intl','eu','Швеция','Sweden','Schweden'], ['no','🇳🇴','€','card,applepay,sepa,paypal','intl','eu','Норвегия','Norway','Norwegen'], ['fi','🇫🇮','€','card,applepay,sepa,paypal','intl','eu','Финляндия','Finland','Finnland'], ['is','🇮🇸','€','card,applepay,sepa,paypal','intl','eu','Исландия','Iceland','Island'], ['ee','🇪🇪','€','card,applepay,sepa,paypal','intl','eu','Эстония','Estonia','Estland'], ['lv','🇱🇻','€','card,applepay,sepa,paypal','intl','eu','Латвия','Latvia','Lettland'], ['lt','🇱🇹','€','card,applepay,sepa,paypal','intl','eu','Литва','Lithuania','Litauen'], ['si','🇸🇮','€','card,applepay,sepa,paypal','intl','eu','Словения','Slovenia','Slowenien'], ['hr','🇭🇷','€','card,applepay,sepa,paypal','intl','eu','Хорватия','Croatia','Kroatien'], ['lu','🇱🇺','€','card,applepay,sepa,paypal','intl','eu','Люксембург','Luxembourg','Luxemburg'], ['mt','🇲🇹','€','card,applepay,sepa,paypal','intl','eu','Мальта','Malta','Malta'], ['cy','🇨🇾','€','card,applepay,sepa,paypal','intl','eu','Кипр','Cyprus','Zypern'], ['rs','🇷🇸','€','card,applepay,paypal','intl','eu','Сербия','Serbia','Serbien'], ['ua','🇺🇦','$','card,applepay,paypal','intl','eu','Украина','Ukraine','Ukraine'], ['md','🇲🇩','$','card,applepay,paypal','intl','eu','Молдова','Moldova','Republik Moldau'], ['ge','🇬🇪','$','card,applepay,paypal','intl','eu','Грузия','Georgia','Georgien'], ['am','🇦🇲','$','card,applepay,paypal','intl','eu','Армения','Armenia','Armenien'], ['az','🇦🇿','$','card,applepay,paypal','intl','eu','Азербайджан','Azerbaijan','Aserbaidschan'], ['kz','🇰🇿','$','card,applepay,paypal','intl','eu','Казахстан','Kazakhstan','Kasachstan'], ['uz','🇺🇿','$','card,applepay,paypal','intl','eu','Узбекистан','Uzbekistan','Usbekistan'], ['kg','🇰🇬','$','card,applepay,paypal','intl','eu','Киргизия','Kyrgyzstan','Kirgisistan'], ['by','🇧🇾','$','card,paypal','intl','eu','Беларусь','Belarus','Belarus'], ['tr','🇹🇷','$','card,applepay,paypal','intl','eu','Турция','Türkiye','Türkei'], ['mx','🇲🇽','$','card,applepay,paypal','intl','am','Мексика','Mexico','Mexiko'], ['ar','🇦🇷','$','card,applepay,paypal','intl','am','Аргентина','Argentina','Argentinien'], ['cl','🇨🇱','$','card,applepay,paypal','intl','am','Чили','Chile','Chile'], ['co','🇨🇴','$','card,applepay,paypal','intl','am','Колумбия','Colombia','Kolumbien'], ['pe','🇵🇪','$','card,applepay,paypal','intl','am','Перу','Peru','Peru'], ['uy','🇺🇾','$','card,applepay,paypal','intl','am','Уругвай','Uruguay','Uruguay'], ['jp','🇯🇵','$','card,applepay,paypal','intl','as','Япония','Japan','Japan'], ['kr','🇰🇷','$','card,applepay,paypal','intl','as','Южная Корея','South Korea','Südkorea'], ['cn','🇨🇳','$','card,paypal','intl','as','Китай','China','China'], ['hk','🇭🇰','$','card,paypal','intl','as','Гонконг','Hong Kong','Hongkong'], ['sg','🇸🇬','$','card,applepay,paypal','intl','as','Сингапур','Singapore','Singapur'], ['in','🇮🇳','$','card,paypal','intl','as','Индия','India','Indien'], ['id','🇮🇩','$','card,paypal','intl','as','Индонезия','Indonesia','Indonesien'], ['my','🇲🇾','$','card,paypal','intl','as','Малайзия','Malaysia','Malaysia'], ['th','🇹🇭','$','card,paypal','intl','as','Таиланд','Thailand','Thailand'], ['vn','🇻🇳','$','card,paypal','intl','as','Вьетнам','Vietnam','Vietnam'], ['ph','🇵🇭','$','card,paypal','intl','as','Филиппины','Philippines','Philippinen'], ['ae','🇦🇪','$','card,applepay,paypal','intl','me','ОАЭ','United Arab Emirates','Vereinigte Arabische Emirate'], ['il','🇮🇱','$','card,applepay,paypal','intl','me','Израиль','Israel','Israel'], ['sa','🇸🇦','$','card,applepay,paypal','intl','me','Саудовская Аравия','Saudi Arabia','Saudi-Arabien'], ['eg','🇪🇬','$','card,paypal','intl','me','Египет','Egypt','Ägypten'], ['za','🇿🇦','$','card,paypal','intl','me','ЮАР','South Africa','Südafrika'], ['ma','🇲🇦','$','card,paypal','intl','me','Марокко','Morocco','Marokko'], ['ng','🇳🇬','$','card,paypal','intl','me','Нигерия','Nigeria','Nigeria'], ['ke','🇰🇪','$','card,paypal','intl','me','Кения','Kenya','Kenia'], ['nz','🇳🇿','$','card,applepay,paypal','intl','oc','Новая Зеландия','New Zealand','Neuseeland'] ]; const PAYER=PAY_RAW.map(r=>({code:r[0],flag:r[1],cur:r[2],methods:r[3]?r[3].split(','):[],tier:r[4],grp:r[5],n:{ru:r[6],en:r[7],de:r[8]}})); PAYER.push({code:'xx',flag:'🌍',cur:'',methods:[],tier:'none',grp:'other',n:{ru:'Другая страна',en:'Other country',de:'Anderes Land'}}); const PAY_GROUPS=[ {k:'main',n:{ru:'Основные',en:'Main',de:'Haupt'}}, {k:'eu',n:{ru:'Европа и СНГ',en:'Europe & CIS',de:'Europa & GUS'}}, {k:'am',n:{ru:'Америка',en:'Americas',de:'Amerika'}}, {k:'as',n:{ru:'Азия',en:'Asia',de:'Asien'}}, {k:'me',n:{ru:'Ближний Восток и Африка',en:'Middle East & Africa',de:'Naher Osten & Afrika'}}, {k:'oc',n:{ru:'Океания',en:'Oceania',de:'Ozeanien'}}, {k:'other',n:{ru:'Другая страна',en:'Other country',de:'Anderes Land'}} ]; let payer='ru', method='sbp', methodPicked=false; /* «Оплата по реквизитам»: клиент платит напрямую, мы только передаём реквизиты. */ let reqMode=false, reqData={bank:'',bik:'',acc:'',card:'',file:''}; function reqPick(inp){reqData.file=(inp.files&&inp.files[0]&&inp.files[0].name)||'';setReqMode(true)} function setReqMode(on){ const g=id=>((document.getElementById(id)||{}).value||''); reqData={bank:g('rq-bank')||reqData.bank,bik:g('rq-bik')||reqData.bik,acc:g('rq-acc')||reqData.acc,card:g('rq-card')||reqData.card,file:reqData.file}; const v={client:g('inp-client'),item:g('inp-item'),sum:g('inp-sum'),cur:g('inp-cur'),due:g('inp-due')}; reqMode=on; gotoInvoice(v); } function reqReady(){const g=id=>((document.getElementById(id)||{}).value||'').trim();return !!(g('rq-acc')||g('rq-card')||reqData.acc||reqData.card||reqData.file)} function payerObj(){for(const p of PAYER){if(p.code===payer)return p}return PAYER[0]} let payerQ=''; function payerList(){ const q=(payerQ||'').trim().toLowerCase(); return PAY_GROUPS.map(g=>{ let rows=PAYER.filter(p=>p.grp===g.k); if(q)rows=rows.filter(p=>(lo(p.n)+' '+p.code).toLowerCase().indexOf(q)>=0); rows=rows.slice().sort((a,b)=>lo(a.n).localeCompare(lo(b.n),lang==='ru'?'ru':(lang==='de'?'de':'en'))); return {g:g,rows:rows}; }); } function renderPayers(){ const box=document.getElementById('payerSel'); if(!box)return; box.innerHTML=payerList().map(x=>{ if(!x.rows.length)return ''; return ``+x.rows.map(p=> ``).join('')+``; }).join(''); box.value=payer; } /* поиск страны: набрал 1–2 буквы — список сузился; единственное совпадение выбирается само */ function payerFilter(v){ payerQ=v||''; const box=document.getElementById('payerSel'); if(!box)return; renderPayers(); const rest=payerList().reduce((a,x)=>a.concat(x.rows),[]); if(payerQ.trim().length>=2&&rest.length===1&&rest[0].code!==payer)pickPayer(rest[0].code); } function gotoInvoice(pre){ if(!plan){show('app');gotoPlans();return} if(!planPaid&&!freePass){show('app');gotoPlanWait();return} if(!can('issue')){show('app');lockScreen('D');return} appStep='invoice';$('#appTitle').textContent=t('invTitle');$('#bnav').style.display='none'; const d=Object.assign({},DRAFT[lang],pre||{}); if(d.payer)payer=d.payer; if(pre&&pre.method){method=pre.method}else{const pp=payerObj();if(pp.methods.length>1){method='';methodPicked=false}} $('#appContent').innerHTML=`

${t('invH1')}

${t('invP')}

${t('loggedAs')} ${esc(userEmail)}

${t('clientErr')}
${t('itemErr')}
${t('sumErr')}
${reqMode?`
${esc(lavaT('reqT'))}
${esc(lavaT('reqP'))}
` :`
`}
${PAY_NOTE[lang]}
`; renderPayers(); renderMethods(); setTimeout(()=>{const i=$('#inp-client');if(i)i.focus()},50); } function pickPayer(c){payer=c;method='';methodPicked=false;renderPayers();renderMethods()} function renderMethods(){ const box=document.getElementById('methodBox'); if(!box){syncPay();return} const p=payerObj(); const curSel=document.getElementById('inp-cur'); if(curSel&&p.cur)curSel.value=p.cur; const ph=document.getElementById('payerHint'); if(ph){ if(p.code==='ru')ph.textContent=(lang==='de'?'Visa und Mastercard funktionieren in Russland nicht — verfügbar sind SBP und Mir.' :lang==='en'?'Visa and Mastercard do not work in Russia — SBP and Mir are available.' :'Карты Visa и Mastercard в России не работают — доступны СБП и «Мир».'); else if(p.tier==='intl')ph.textContent=lavaT('intlHint'); else ph.textContent=''; } if(!p.methods.length){ method='';methodPicked=false; box.innerHTML=`
${t('noMethods')}
`; syncPay(); return; } const auto=(p.methods.length===1); if(auto){method=p.methods[0];methodPicked=true} else if(p.methods.indexOf(method)<0){method='';methodPicked=false} box.innerHTML=`
${p.methods.map(m=> ``).join('')}
${auto?t('autoMethod'):''}
`; syncPay(); } function pickMethod(el){ if(el.disabled)return; document.querySelectorAll('#inp-meth .chip').forEach(c=>c.classList.remove('active')); el.classList.add('active'); method=el.dataset.v; methodPicked=true; syncPay(); } function pickMethodSilent(v){ document.querySelectorAll('#inp-meth .chip').forEach(c=>{if(c.dataset.v===v){c.classList.add('active');method=v}else{c.classList.remove('active')}}); } function waitlist(btn){btn.textContent=t('notifyDone');btn.disabled=true;btn.style.opacity='0.6'} function syncPay(){ const dueEl=document.getElementById('inp-due'); if(!dueEl)return; const one=(dueEl.value==='1 день'); const dHint=document.getElementById('dueHint'); if(dHint)dHint.textContent=one?'':(lang==='de'?'SBP-QR gilt nur kurz — für längere Fristen geben wir einen Karten-Link.' :lang==='en'?'SBP QR is short-lived — longer terms get a card link.' :'QR СБП живёт ограниченное время — для срока больше суток даём карточную ссылку.'); const sbp=document.querySelector('#inp-meth .chip[data-v="sbp"]'); if(sbp){ sbp.disabled=!one; sbp.style.opacity=one?'':'0.45'; sbp.style.cursor=one?'pointer':'not-allowed'; if(!one&&method==='sbp'){ const alt=document.querySelector('#inp-meth .chip[data-v="mir"]'); if(alt){method='mir';methodPicked=true;pickMethodSilent('mir')}else{method='';methodPicked=false} } } const cb=document.getElementById('createBtn'); if(cb){const ok=reqMode?reqReady():!!(payerObj().methods.length&&method);cb.disabled=!ok;cb.style.opacity=ok?'':'0.5';cb.style.cursor=ok?'':'not-allowed'} } function submitInvoice(){ const client=$('#inp-client').value.trim(),item=$('#inp-item').value.trim(),sum=$('#inp-sum').value.trim(); let err=false; if(!client){$('#f-client').classList.add('has-error');err=true} if(!item){$('#f-item').classList.add('has-error');err=true} if(!sum||+sum<=0){$('#f-sum').classList.add('has-error');err=true} if(err)return; const p=payerObj(); if(reqMode){ if(!reqReady()){$('#f-sum').classList.add('has-error');const e=document.getElementById('rq-bank');if(e)e.style.borderColor='var(--danger)';return} const loc0=lang==='ru'?'ru-RU':(lang==='de'?'de-DE':'en-US'); const cur0=$('#inp-cur').value, due0=$('#inp-due').value; const gv=id=>((document.getElementById(id)||{}).value||'').trim(); window._inv={client:client,item:item,sum:Number(sum).toLocaleString(loc0),cur:cur0,due:due0,req:true, bank:gv('rq-bank'),acc:gv('rq-acc'),card:gv('rq-card'),bik:gv('rq-bik'),file:reqData.file,id:1043+NEWC.length, payerName:(lang==='de'?'Bankdaten':lang==='en'?'Bank details':'Реквизиты')}; DRAFT[lang]={client:'',item:'',sum:'',cur:cur0,due:'1 день',payer:payer}; NEWC.unshift({id:1043+NEWC.length,no:(lang==='ru'?'Счёт № ':lang==='de'?'Rechnung Nr. ':'Invoice #')+(1043+NEWC.length), client:client,item:item,sum:Number(sum).toLocaleString(loc0)+' '+cur0,due:dueLabel(due0),stamp:'draft',sl:t('ready')}); gotoDone(); return; } if(!p.methods.length||!method){return} const cur=$('#inp-cur').value,due=$('#inp-due').value; const loc=lang==='ru'?'ru-RU':(lang==='de'?'de-DE':'en-US'); window._inv={client,item,sum:Number(sum).toLocaleString(loc),cur,due,method,payer:p.code, sbp:(method==='sbp'),payerName:(p.flag+' '+lo(p.n))}; DRAFT[lang]={client:'',item:'',sum:'',cur:cur,due:'1 день',payer:p.code}; const list=NEWC,nextId=1043+NEWC.length;window._inv.id=nextId; list.unshift({id:nextId,no:(lang==='ru'?'Счёт № ':lang==='de'?'Rechnung Nr. ':'Invoice #')+nextId,client,item,sum:Number(sum).toLocaleString(loc)+' '+cur,due:dueLabel(due),stamp:'draft',sl:t('ready')}); gotoDone(); } function gotoDone(){ymGoal('invoice_created'); appStep='done';$('#appTitle').textContent=t('doneTitle');$('#bnav').style.display='none'; const inv=window._inv||{client:'—',item:'—',sum:'0',cur:'₽',due:'1 день',sbp:true}; window._share={url:'https://pay.duebuddy.work/inv/9f3k2m',text:(lang==='de'?'Rechnung über ':lang==='en'?'Invoice for ':'Счёт на оплату ')+inv.sum+' '+inv.cur+' — '+inv.item+' ('+inv.client+')'}; const S=window._share,encU=encodeURIComponent(S.url),encT=encodeURIComponent(S.text); const blocked=payBlocked()&&!inv.req; const reqZone = inv.req?`
${esc(lavaT('reqDoneT'))}
${esc(lavaT('reqDoneP'))}
${inv.bank?`
${esc(lavaT('reqBank'))}${esc(inv.bank)}
`:''} ${inv.bik?`
${esc(lavaT('reqBik'))}${esc(inv.bik)}
`:''} ${inv.acc?`
${esc(lavaT('reqAcc'))}${esc(inv.acc)}
`:''} ${inv.card?`
${esc(lavaT('reqCard'))}${esc(inv.card)}
`:''} ${inv.file?`
${esc(lavaT('reqFile'))}${esc(inv.file)}
`:''}
`:''; const payZone = inv.req?'':(blocked ? `
${t('linkBlockedT')}
${t('linkBlockedP')}
` : `${lavaT('demoLink')} `); const qr=(!blocked&&!inv.req&&inv.sbp)?`
QR
${lang==='de'?'QR nur für SBP — geteilt wird der Link.':lang==='en'?'QR only for SBP — what you share is the link.':'QR только для СБП — делимся ссылкой.'}
`:''; $('#appContent').innerHTML=`
${t('invNo')}${esc(String(inv.id||1042))}${t('today')} · ${esc(dueLabel(inv.due))}
${t('ready')}
${esc(inv.sum)} ${esc(inv.cur)}
${esc(inv.client)} · ${esc(inv.item)}
${payZone}${reqZone}${qr}
`; } function askDelete(text,onYes){ const back=document.createElement('div');back.className='confirm-back'; back.innerHTML=''; const close=function(){try{document.body.removeChild(back)}catch(e){}}; back.querySelector('.cancel').onclick=close; back.querySelector('.danger').onclick=function(){close();onYes()}; back.onclick=function(ev){if(ev.target===back)close()}; document.addEventListener('keydown',function esc1(ev){if(ev.key==='Escape'){close();document.removeEventListener('keydown',esc1)}}); document.body.appendChild(back); const b=back.querySelector('.danger');if(b&&b.focus)b.focus(); } function markSent(){const s=document.getElementById('invStamp');if(s){s.className='stamp sent';s.textContent=t('sent')}} function copyText(txt,btn){if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt)}const o=btn.textContent;btn.textContent=t('copied');btn.style.color='var(--ok)';setTimeout(function(){btn.textContent=o;btn.style.color=''},1500)} function copyLink(btn){ const u=window._share&&window._share.url; if(!u){btn.textContent=lavaT('linkBlockedT');return} if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u)} const o=btn.textContent;btn.textContent=t('copied');btn.style.color='var(--ok)'; setTimeout(()=>{btn.textContent=o;btn.style.color=''},1500); } async function shareInvoice(btn){ const S=window._share; if(!S||!S.url){btn.textContent=lavaT('linkBlockedT');return} markSent(); if(navigator.share){try{await navigator.share({title:t('shareSubj'),text:S.text,url:S.url});return}catch(e){}} const p=document.getElementById('sharePanel');if(!p)return; p.style.display=(p.style.display==='none'||!p.style.display)?'grid':'none'; } function sendApiLead(){ const email=(document.getElementById('apiEmail')||{}).value||''; const msg=document.getElementById('apiMsg'); const lang=(document.documentElement.getAttribute('data-lang')||document.body.getAttribute('data-lang')||'en').slice(0,2); if(!email||email.indexOf('@')<0){ if(msg){msg.style.color='#ff9a8b';msg.textContent='Укажите e-mail / Enter e-mail';} return; } const pages=parseInt((document.getElementById('apiPages')||{}).value||'0',10)||0; const body={email:email,company:((document.getElementById('apiCompany')||{}).value||'').trim(), pages_per_month:pages,comment:((document.getElementById('apiComment')||{}).value||'').trim(), source:'prototype',lang:(['ru','en','de'].indexOf(lang)>=0?lang:'')}; fetch('https://app.duebuddy.work/api/v1/leads',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}) .then(r=>r.json()).then(d=>{ if(msg){msg.style.color='#7ee0b8';msg.textContent=(d&&d.message)||'OK';} }) .catch(()=>{ if(msg){msg.style.color='#ff9a8b';msg.textContent='Не удалось отправить. Попробуйте позже.';} }); } const BUILD='сборка 15 · 04.10 — автоплатёж в кабинете, наставник Lava'; const SUPPORT_EMAIL='andreiseximen@gmail.com'; const refUrl='https://duebuddy.work/r/demo123'; function copyRef(btn){ if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(refUrl)} const o=btn.textContent;btn.textContent=t('copied');btn.style.color='var(--ok)'; setTimeout(()=>{btn.textContent=o;btn.style.color=''},1500); } async function shareRef(btn){ const txt=lang==='de'?'Komm zu DueBuddy — hol dir 20 % deines Abos zurück':lang==='en'?'Join DueBuddy — earn 20% of your subscription back each month':'Приведи друга в DueBuddy — получай 20% от его подписки каждый месяц'; if(navigator.share){try{await navigator.share({title:'DueBuddy',text:txt,url:refUrl});return}catch(e){}} copyRef(btn); } /* ---- кабинет ---- */ /* ---- API распознавания: ползунок, цена, подписка ---- */ const API_STEPS=[1000,2000,5000,10000,25000,50000,100000,250000,500000]; const API_BASE='https://app.duebuddy.work'; function apiCountry(){ try{ const c=(localStorage.getItem('cc')||'').toUpperCase(); if(c) return c; }catch(e){} return (lang==='de'?'DE':lang==='ru'?'RU':'US'); } function apiSheets(){ const el=document.getElementById('apiSlider'); const i=el?parseInt(el.value,10):2; return API_STEPS[Math.max(0,Math.min(API_STEPS.length-1,i))]; } function fmtN(n){ return String(n).replace(/\B(?=(\d{3})+(?!\d))/g,' '); } async function apiQuote(){ const sh=apiSheets(); const elS=document.getElementById('apiSheets'), elP=document.getElementById('apiPrice'), elPer=document.getElementById('apiPer'); if(elS) elS.textContent=fmtN(sh)+(lang==='de'?' Seiten':lang==='en'?' pages':' листов'); if(elP) elP.textContent='…'; try{ const r=await fetch(API_BASE+'/api/v1/api/price?sheets='+sh+'&country='+apiCountry(),{credentials:'include'}); const d=await r.json(); if(elP) elP.textContent=(d.price_local!=null?d.price_local:d.price_eur)+' '+(d.currency||'EUR')+(lang==='de'?' / Monat':lang==='en'?' / month':' / мес'); if(elPer) elPer.textContent=(d.cents_per_sheet_local!=null?d.cents_per_sheet_local:d.cents_per_sheet)+' '+(lang==='de'?'Cent/Seite':lang==='en'?'cents per page':'центов за лист'); }catch(e){ if(elP) elP.textContent='—'; } } async function apiRefreshSub(){ const box=document.getElementById('apiSubBox'); if(!box) return; try{ const r=await fetch(API_BASE+'/api/v1/api/dashboard',{credentials:'include'}); if(!r.ok){ box.textContent=''; return; } const d=await r.json(); if(d.has_subscription){ box.innerHTML=(lang==='de'?`Abo aktiv: ${fmtN(d.sheets_per_month)} Seiten/Monat · verbraucht ${fmtN(d.sheets_used)} · übrig ${fmtN(d.sheets_left)} · bis ${(d.expires_at||'').slice(0,10)}` :lang==='en'?`Subscription active: ${fmtN(d.sheets_per_month)} pages/month · used ${fmtN(d.sheets_used)} · left ${fmtN(d.sheets_left)} · until ${(d.expires_at||'').slice(0,10)}` :`Подписка активна: ${fmtN(d.sheets_per_month)} листов/мес · израсходовано ${fmtN(d.sheets_used)} · осталось ${fmtN(d.sheets_left)} · до ${(d.expires_at||'').slice(0,10)}`); } else { box.textContent=(lang==='de'?'Kein Abo — wählen Sie das Volumen und abonnieren.':lang==='en'?'No subscription yet — choose a volume and subscribe.':'Подписки пока нет — выберите объём и оформите.'); } }catch(e){ box.textContent=''; } } async function apiSubscribe(){ const msg=document.getElementById('apiMsg'), btn=document.getElementById('apiPayBtn'); const sh=apiSheets(); if(!(userEmail||'').trim()){ if(msg){msg.style.color='#ff9a8b';msg.textContent=(lang==='de'?'Bitte zuerst anmelden.':lang==='en'?'Please sign in first.':'Сначала войдите в кабинет.')} return; } if(btn){btn.disabled=true} if(msg){msg.style.color='var(--muted)';msg.textContent=(lang==='de'?'Rechnung wird erstellt…':lang==='en'?'Creating invoice…':'Выставляю счёт…')} try{ const r=await fetch(API_BASE+'/api/v1/api/subscribe',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({sheets:sh})}); const d=await r.json(); if(r.ok&&d.pay_url){ if(msg){msg.style.color='#7ee0b8';msg.textContent=(lang==='de'?'Weiter zur Zahlung…':lang==='en'?'Redirecting to payment…':'Переход к оплате…')} window.location.href=d.pay_url; return; } if(msg){msg.style.color='#ff9a8b';msg.textContent=(lang==='de'?'Fehler: ':(lang==='en'?'Error: ':'Ошибка: '))+(d.detail||r.status)} }catch(e){ if(msg){msg.style.color='#ff9a8b';msg.textContent='network'} } if(btn){btn.disabled=false} } function cabKey(){const ts=tabs();return (ts[currentCabTab]||{}).k||''} function cabTab(idx,el){ currentCabTab=idx; const ts=tabs(); if(idx>=ts.length){idx=ts.length-1;currentCabTab=idx} const key=ts[idx].k; if(ts[idx].locked){lockScreen(ts[idx].cost);return} $('#appTitle').textContent=key==='more'?t('cabTitle'):t(ts[idx].t); renderNav(); const tabBar=key==='more'?'':`
${ts.filter(x=>x.k!=='more').map((x,i)=>``).join('')}
`; let body=''; if(key==='inv')body=renderInvTab(); if(key==='track'||key==='foreign')body=renderTrackTab(); if(key==='inbox')body=renderInboxTab(); if(key==='more'){ body=`
${lavaT('lavaTitle')}
${lavaT('lavaSubShort')}
${lang==='de'?'API zur Rechnungserkennung':lang==='en'?'Invoice recognition API':'API распознавания счетов'}
${lang==='de'?'Seiten pro Monat':lang==='en'?'Pages per month':'Листов в месяц'}
1 000500 000
5 000
—
${lang==='de'?'Abrechnung pro Monat. Nicht genutzte Seiten verfallen am Monatsende; wir erinnern 3 Tage und 1 Tag vorher.':lang==='en'?'Billed monthly. Unused pages expire at month end; we remind you 3 days and 1 day before.':'Оплата за месяц. Неиспользованные листы в конце месяца сгорают; напоминаем за 3 дня и за 1 день.'}
${t('refTitle')}
${t('refNote')}
${t('settings')}
${esc(BUILD)}
`; } const wide=(key==='inv'||key==='track'||key==='foreign'||key==='inbox'); setWide(wide); $('#appContent').innerHTML=tabBar+body; if(key==='inbox')inboxWire('cabDrop','cabInboxOut'); if(key==='more'){apiQuote();apiRefreshSub();} } function editDraft(){gotoInvoice(Object.assign({},DRAFT[lang]))} const DKEY=function(){try{return 'db_draft::'+(userEmail||'anon').toLowerCase()}catch(e){return 'db_draft::anon'}}; function draftTouch(){ const g=id=>{const e=document.getElementById(id);return e?e.value:undefined}; const c=g('inp-client'),i=g('inp-item'),su=g('inp-sum'),cu=g('inp-cur'),du=g('inp-due'); if(c===undefined&&i===undefined&&su===undefined)return; DRAFT[lang]={client:(c||'').trim(),item:(i||'').trim(),sum:(su||'').trim(),cur:cu||DRAFT[lang].cur,due:du||DRAFT[lang].due,payer:payer}; try{localStorage.setItem(DKEY(),JSON.stringify(DRAFT[lang]))}catch(e){} } function draftLoad(){try{const raw=localStorage.getItem(DKEY());if(raw){const d=JSON.parse(raw)||{};DRAFT[lang]=Object.assign({},DRAFT[lang],d)}}catch(e){}} function delDraft(ev){if(ev&&ev.stopPropagation)ev.stopPropagation();askDelete(lavaT('delAsk'),function(){DRAFT[lang]={client:'',item:'',sum:'',cur:DRAFT[lang].cur,due:'1 день',payer:payer};try{localStorage.removeItem(DKEY())}catch(e){}crmRerender()})} function gotoTariffs(){renderAll();show('landing');setTimeout(function(){scrollPricing()},150)} function openFeedback(){ const b=document.getElementById('fbBox'); if(!b)return; if(b.innerHTML){b.innerHTML='';return} const lbl={ru:'Ваше сообщение',en:'Your message',de:'Ihre Nachricht'}[lang]; const btn={ru:'Отправить',en:'Send',de:'Senden'}[lang]; b.innerHTML=`
`; setTimeout(function(){const t2=document.getElementById('fbText');if(t2)t2.focus()},40); } function sendFeedback(){ const el=document.getElementById('fbText'),v=((el&&el.value)||'').trim(); const b=document.getElementById('fbBox'); if(!b)return; if(v.length<3){if(el)el.style.outline='1px solid var(--accent)';return} const subj=encodeURIComponent((lang==='de'?'DueBuddy Feedback':lang==='en'?'DueBuddy feedback':'Обратная связь DueBuddy')+' — '+(userEmail||'')); const body=encodeURIComponent(v+'\n\n---\n'+(userEmail||'')); try{window.open('mailto:'+SUPPORT_EMAIL+'?subject='+subj+'&body='+body,'_blank')}catch(e){} const msg={ru:'Открылась ваша почтовая программа — отправьте письмо. Если она не открылась, напишите на ', en:'Your mail app should open — send the message. If it did not open, write to ', de:'Ihr Mailprogramm sollte sich öffnen — senden Sie die Nachricht. Falls nicht, schreiben Sie an '}[lang]; b.innerHTML=`
${msg}${SUPPORT_EMAIL}
`; } function remindCard(i){const c=cards()[i];if(!c||c.stamp==='paid')return;c.stamp='reminded';c.sl=(lang==='de'?'Erinnerung gesendet':lang==='en'?'Reminder sent':'Напоминание отправлено');c.due=c.sl} function invoiceDetail(i){ const c=cards()[i];if(!c)return; appStep='detail';$('#appTitle').textContent=t('detailTitle');$('#bnav').style.display='none'; const L=(LAVATXT[lang]||LAVATXT.ru); const paid=(c.stamp==='paid'); $('#appContent').innerHTML=`
${esc(c.no)}${esc(c.due)}
${esc(c.sl)}
${esc(c.sum)}
${esc(c.client)} · ${esc(c.item)}
${L.fClient}${esc(c.client)}
${L.fItem}${esc(c.item)}
${L.fSum}${esc(c.sum)}
${L.fTerms}${esc(c.due)}
${L.fStatus}${esc(c.sl)}
${t('payLink')}pay.duebuddy.work/inv/9f3k2m
${lavaT('demoLink')}
`; const hist=histOf(c); $('#appContent').innerHTML+=`
${esc(L.histT)}
${hist.map(h=>`
${esc(h.d)} · ${esc(h.s)}${esc(h.o)}
`).join('')}
${esc(L.histNote)}
${esc(L.paidAsk)}
${paid?'':``} ${paid?``:``}
`; } function showLetterStage(k){ const st=[['loy','−3'],['neu','0'],['loy','+5'],['neu','+10'],['firm','+18']][k]||['loy','−3']; const L=(LETTERS[lang]||LETTERS.ru)[st[0]]; const txt=L.replace('{name}','Иван Петров').replace('{no}','1042').replace('{sum}','12 500 ₽').replace('{d}',st[1]+' дн.'); appStep='letter';$('#appTitle').textContent=lavaT('letterT');$('#bnav').style.display='none'; $('#appContent').innerHTML=`

${esc(lavaT('letterT'))}

${esc((lavaT('ladderSteps')[k]||''))} · ${esc((TONEL[st[0]]||{})[lang]||'')}

${esc(txt)}
${esc(lavaT('letterNote'))}
`; show('app'); } function markPartial(i){const c=cards()[i];if(!c)return;c.stamp='partial';c.sl=(lang==='de'?'Teilzahlung':lang==='en'?'Part paid':'Оплачен частично');invoiceDetail(i)} function histOf(c){ const paidNow=(c.stamp==='paid'); const base=[ {d:'12.09',s:(lang==='de'?'Brief 1 · freundlich':lang==='en'?'Letter 1 · friendly':'Письмо 1 · дружеское'),o:(lang==='de'?'geöffnet':lang==='en'?'opened':'открыто'),ok:true}, {d:'15.09',s:(lang==='de'?'Brief 2 · sachlich':lang==='en'?'Letter 2 · businesslike':'Письмо 2 · деловое'),o:(lang==='de'?'nicht geöffnet':lang==='en'?'not opened':'не открыто'),ok:false}]; if(c.stamp==='overdue'||c.stamp==='reminded'||paidNow)base.push({d:'18.09',s:(lang==='de'?'Brief 3 · förmlich':lang==='en'?'Letter 3 · formal':'Письмо 3 · формальное'),o:paidNow?(lang==='de'?'bezahlt':lang==='en'?'paid':'оплачен'):(lang==='de'?'gesendet':lang==='en'?'sent':'отправлено'),ok:paidNow}); return base; } function unpaid(i){const c=cards()[i];if(!c)return;c.stamp='sent';c.sl=t('sent');invoiceDetail(i)} function markPaid(i){ const c=cards()[i];if(!c)return; c.stamp='paid';c.sl=(lang==='de'?'Bezahlt':lang==='en'?'Paid':'Оплачен');c.due=c.sl;invoiceDetail(i); } /* ================= КАЛЬКУЛЯТОР ПОТЕРЬ (EN) ================= */ let calcCur='$'; function setCalcCur(c){ calcCur=c; const u=document.getElementById('curUsd'),e=document.getElementById('curEur'); if(u)u.classList.toggle('active',c==='$'); if(e)e.classList.toggle('active',c==='€'); recalcLoss(); } function recalcLoss(){ const d=document.getElementById('calc-debt'); if(!d)return; const debt=parseFloat(d.value)||0; const share=(parseFloat($('#calc-share').value)||0)/100; const hours=parseFloat($('#calc-hours').value)||0; const rate=parseFloat($('#calc-rate').value)||0; $('#calc-share-val').textContent=Math.round(share*100)+'%'; $('#calc-hours-val').textContent=hours+' h'; $('#calc-rate-val').textContent=calcCur+Math.round(rate); const money=debt*share*(0.24/12),time=hours*rate,mo=money+time,yr=mo*12; const f=n=>calcCur+Math.round(n).toLocaleString('en-US'); $('#calc-mo').textContent=f(mo); $('#calc-brk').textContent='money: '+f(money)+' · time: '+f(time); $('#calc-yr').textContent=f(yr)+' a year'; } /* ================= ПРИОРИТЕТНЫЙ БЛОК СТРАНЫ ================= */ const WIP={ru:'в работе',en:'in progress',de:'in Arbeit'}; const PRIO={ ru:{k:{ru:'Приём оплаты',en:'Getting paid',de:'Zahlungseingang'}, h:{ru:'Счёт — ссылкой. Оплата — по СБП или картой «Мир».',en:'Send the invoice as a link. Get paid by SBP or card.',de:'Rechnung als Link. Zahlung per SBP oder Karte.'}, sub:{ru:'Клиент не регистрируется: открывает ссылку и платит. Способ выбирается до создания счёта — поэтому ссылка сразу рабочая.', en:'Your client does not sign up: they open the link and pay. The method is picked before the invoice is created — so the link works right away.', de:'Ihr Kunde meldet sich nicht an: Link öffnen und zahlen. Die Zahlungsart wird vor der Rechnung gewählt — der Link funktioniert sofort.'}, steps:[{n:'1',b:{ru:'Выбираете, как платит клиент',en:'Pick how the client pays',de:'Zahlungsart wählen'},s:{ru:'СБП по QR — если срок 1 день. Дольше — карточная ссылка.',en:'SBP by QR — for a 1-day term. Longer terms get a card link.',de:'SBP per QR — bei 1 Tag Frist. Länger: Kartenlink.'}}, {n:'2',b:{ru:'Создаёте счёт',en:'Create the invoice',de:'Rechnung erstellen'},s:{ru:'Клиент, что продано, сумма. Валюта ₽ / $ / €.',en:'Client, item, amount. Currency ₽ / $ / €.',de:'Kunde, Leistung, Betrag. Währung ₽ / $ / €.'}}, {n:'3',b:{ru:'Отправляете ссылку',en:'Send the link',de:'Link senden'},s:{ru:'Клиент платит картой «Мир» или по QR. Без регистрации.',en:'They pay by card or QR. No account needed.',de:'Zahlung per Karte oder QR. Ohne Anmeldung.'}}], chips:[{t:{ru:'СБП по QR',en:'SBP by QR',de:'SBP per QR'},s:{ru:'срок 1 день',en:'1-day term',de:'Frist 1 Tag'}}, {t:{ru:'Карта «Мир»',en:'Mir card',de:'Mir-Karte'},s:{ru:'любой срок',en:'any term',de:'jede Frist'}}, {t:{ru:'Из-за границы',en:'From abroad',de:'Aus dem Ausland'},s:{ru:'карта, SEPA, PayPal',en:'card, SEPA, PayPal',de:'Karte, SEPA, PayPal'}}]}, de:{k:{ru:'Разбор входящих',en:'Incoming invoices',de:'Eingangsrechnungen einlesen'}, h:{ru:'Чужие счета — в данные. PDF, фото, Excel.',en:'Someone else\'s invoice into data. PDF, photo, Excel.',de:'Fremde Rechnung in Daten. PDF, Foto, Excel.'}, sub:{ru:'Загружаете документ — получаете поля: поставщик, сумма, номер, дата. Дальше проверяете и выгружаете бухгалтеру.', en:'Upload the document — get the fields back: vendor, amount, number, date. Then review and export to your accountant.', de:'Beleg hochladen — Felder erhalten: Absender, Betrag, Nummer, Datum. Dann prüfen und an die Buchhaltung übergeben.'}, steps:[{n:'1',b:{ru:'Загрузка',en:'Upload',de:'Hochladen'},s:{ru:'PDF, фото, XLSX, CSV. Файл остаётся у вас до разбора.',en:'PDF, photo, XLSX, CSV. The file stays with you until you confirm.',de:'PDF, Foto, XLSX, CSV. Die Datei bleibt bis zur Freigabe bei Ihnen.'}}, {n:'2',b:{ru:'Распознавание',en:'Reading',de:'Einlesen'},s:{ru:'Текст PDF, OCR для фото, строки Excel.',en:'PDF text, OCR for photos, Excel rows.',de:'PDF-Text, OCR für Fotos, Excel-Zeilen.'}}, {n:'3',b:{ru:'Проверка',en:'Review',de:'Prüfen'},s:{ru:'Правите поля руками, если что-то не так.',en:'Fix any field by hand if needed.',de:'Felder bei Bedarf von Hand korrigieren.'}}, {n:'4',b:{ru:'Выгрузка',en:'Export',de:'Export'},s:{ru:'CSV и JSON — уже сейчас. XLSX, PDF, DATEV, XRechnung, ZUGFeRD — в работе.',en:'CSV and JSON today. XLSX, PDF, DATEV, XRechnung, ZUGFeRD in progress.',de:'CSV und JSON heute. XLSX, PDF, DATEV, XRechnung, ZUGFeRD in Arbeit.'}}], dup:{h:{ru:'Дубликаты видно до оплаты',en:'Spot duplicates before you pay',de:'Dubletten sehen, bevor Sie zahlen'}, p:{ru:'Один и тот же счёт часто приходит дважды. Мы помечаем совпадения по номеру, сумме и дате — и подсвечиваем, что совпало.', en:'The same invoice often arrives twice. We flag matches on number, amount and date — and highlight what matched.', de:'Dieselbe Rechnung kommt oft zweimal. Wir markieren Treffer bei Nummer, Betrag und Datum — und heben hervor, was passt.'}, a:{d:{ru:'Rechnung Nr. 4471',en:'Invoice #4471',de:'Rechnung Nr. 4471'},v:'1 240,00 €',date:'12.09.2026',hit:false}, b:{d:{ru:'Rechnung Nr. 4471',en:'Invoice #4471',de:'Rechnung Nr. 4471'},v:'1 240,00 €',date:'14.09.2026',hit:true}}, wip:{ru:'Детект дублей и правка полей — в работе: в прототипе показано, как это будет выглядеть.', en:'Duplicate detection and field editing are in progress: the prototype shows how it will look.', de:'Dublettenerkennung und Feldbearbeitung sind in Arbeit: der Prototyp zeigt, wie es aussehen wird.'}}, us:{k:{ru:'Отслеживание оплаты',en:'Track payments',de:'Zahlungen verfolgen'}, h:{ru:'Видно, что оплачено, что ждёт, что просрочено.',en:'Know what is paid, due and overdue.',de:'Wissen, was bezahlt, offen und überfällig ist.'}, sub:{ru:'Один список счетов со статусами. Если клиент задерживает — напоминаем вежливо, от вашего имени, без неловких писем.', en:'One list of invoices with statuses. If a client runs late, we nudge them politely, in your name — no awkward emails.', de:'Eine Liste mit Status. Zahlt ein Kunde später, erinnern wir höflich in Ihrem Namen — ohne peinliche E-Mails.'}, steps:[{n:'1',b:{ru:'Выставляете счёт',en:'Send the invoice',de:'Rechnung senden'},s:{ru:'За минуты: клиент, позиция, сумма.',en:'In minutes: client, item, amount.',de:'In Minuten: Kunde, Leistung, Betrag.'}}, {n:'2',b:{ru:'Видите статус',en:'See the status',de:'Status sehen'},s:{ru:'Отправлен, ждёт оплаты, оплачен, просрочен.',en:'Sent, awaiting payment, paid, overdue.',de:'Versendet, offen, bezahlt, überfällig.'}}, {n:'3',b:{ru:'Напоминаем сами',en:'We remind for you',de:'Wir erinnern'},s:{ru:'Тон от дружеского до формального.',en:'The tone goes from friendly to formal.',de:'Der Ton von freundlich bis förmlich.'}}], stats:[{l:{ru:'Выставлено',en:'Invoiced',de:'Gestellt'},v:'3'},{l:{ru:'Ждёт оплаты',en:'Awaiting',de:'Offen'},v:'2'}, {l:{ru:'Просрочено',en:'Overdue',de:'Überfällig'},v:'1'},{l:{ru:'Оплачено',en:'Paid',de:'Bezahlt'},v:'1'}], wip:{ru:'Статусы и автонапоминания — в работе: бэкенда пока нет, показан интерфейс.', en:'Statuses and auto-reminders are in progress: there is no backend yet, this is the interface.', de:'Status und automatische Erinnerungen sind in Arbeit: ein Backend gibt es noch nicht, das ist die Oberfläche.'}} }; function chip(a){return `
${a.t[lang]||a.t.ru}${a.s[lang]||a.s.ru}
`} function step(a){return `
${a.n}${a.b[lang]||a.b.ru}${a.s[lang]||a.s.ru}
`} function renderPriority(){ const p=PRIO[country]||PRIO.ru; let extra=''; if(country==='de'){ extra=`
${p.dup.a.d[lang]||p.dup.a.d.ru}
${p.dup.a.v}${p.dup.a.date}
${p.dup.b.d[lang]||p.dup.b.d.ru}${WIP[lang]}
${p.dup.b.v}${p.dup.b.date}
`; extra+=`
${p.wip[lang]||p.wip.ru}
`; } else if(country==='us'){ extra=`
${p.stats.map(s=>`
${s.l[lang]||s.l.ru}
${s.v}
`).join('')}
${p.wip[lang]||p.wip.ru}
`; } const chips=p.chips?`
${p.chips.map(chip).join('')}
`:''; document.getElementById('prio').innerHTML=`
${p.k[lang]||p.k.ru}

${p.h[lang]||p.h.ru}

${p.sub[lang]||p.sub.ru}

${p.steps.map(step).join('')}
${chips}${extra}
`; const dh=document.getElementById('dupH'); if(dh)dh.textContent=(p.dup&&p.dup.h)?(p.dup.h[lang]||p.dup.h.ru):''; } /* ================= БЛОГ ================= */ const BLOG={ ru:[{m:'Выставление',k:'pay',t:'Срок оплаты в счёте: 1 день или 30', s:'Чем короче срок, тем быстрее деньги. Но есть нюанс с QR.', b:['Короткий срок оплаты ускоряет деньги, но повышает шанс, что клиент не успеет. Длинный — удобнее клиенту, но вы ждёте.', 'Важный нюанс: QR для СБП живёт ограниченное время. Поэтому для счёта на несколько дней мы отдаём карточную ссылку — она не истекает вместе с QR.']}, {m:'Разбор входящих',k:'parse',t:'Дубликаты счетов: как не заплатить дважды', s:'Один и тот же счёт часто приходит от поставщика и от бухгалтера.', b:['Один и тот же счёт нередко приходит дважды: напрямую от поставщика и ещё раз через бухгалтера. Заплатить дважды легко, заметить — сложно.', 'Мы сверяем номер, сумму и дату и подсвечиваем совпадения до того, как вы отправите платёж.']}, {m:'Законы и форматы',k:'law',t:'Чек самозанятого: что нужно клиенту', s:'Касса не нужна, но чек формируется — разбираем порядок.', b:['Самозанятому онлайн-касса не требуется: чек формируется в приложении «Мой налог» и отправляется клиенту.', 'Ссылка на оплату — это не чек. Клиенту нужны оба документа, поэтому чек оформляется отдельно после поступления денег.']}, {m:'Оплата из-за границы',k:'pay',t:'Как принимать оплату от иностранных клиентов', s:'Карта, СБП или PayPal — что выбрать и что в России не работает.', b:['Первое, что нужно знать: всё зависит от страны, из которой платит клиент. От неё же зависит валюта счёта.', 'Внутри России работают СБП по QR и карта «Мир»: Visa и Mastercard в России не работают. Для клиентов из Германии, Австрии и Нидерландов доступны карта, Apple Pay, SEPA и PayPal. Для США — карта и PayPal.', 'Один счёт — один способ оплаты. Поэтому способ выбирается при создании счёта, а не после. Криптовалюту мы не принимаем — это принципиально.']}, {m:'Вывод денег',k:'pay',t:'Куда выводятся деньги: карта, самозанятый или USDT', s:'Деньги приходят не нам, а в ваш аккаунт платёжного сервиса.',b:['Деньги от клиентов приходят не нам, а в ваш собственный аккаунт платёжного сервиса. Мы только создаём ссылку на оплату и следим за статусом.','Вывести деньги можно на карту физического лица или самозанятого, либо криптой — USDT. Крипта здесь только способ вывода: принимать оплату в крипте мы не будем принципиально.','Вывод открывается после 3 принятых платежей, плюс до 7 дней уходит на проверку. Верификацию (KYC) лучше пройти сразу: счета работают и без неё, но вывод без неё не откроется.']}, {m:'Напоминания',k:'law',t:'Как напоминать о долге и не испортить отношения', s:'Тон важнее текста: от дружеского до формального — по шагам.',b:['Напоминание — это не претензия. Первое письмо за день до срока работает лучше, чем строгое после просрочки.','Тон нарастает по шагам: дружеское напоминание, затем деловое, и только потом формальное с упоминанием сроков. Каждое следующее письмо — вежливее, чем хочется.','Мы пишем от вашего имени и никогда не угрожаем. Такой подход возвращает деньги чаще: клиенту не стыдно ответить.']}], en:[{m:'Invoicing',k:'pay',t:'What payment terms actually get you paid', s:'Short terms speed up cash. Long terms keep clients happy.', b:['Short terms speed up your money but raise the chance the client misses them. Long terms are easier for the client, harder for your cash flow.', 'One practical catch: QR codes expire. For an invoice with a longer term we hand over a card link instead, which does not expire with the QR.']}, {m:'Parsing',k:'parse',t:'Duplicate invoices: stop paying twice', s:'The same invoice often arrives from the vendor and the accountant.', b:['The same invoice often arrives twice: straight from the vendor and again through the bookkeeper. Paying twice is easy; noticing it is not.', 'We match number, amount and date and flag the overlaps before you send the payment.']}, {m:'Formats and rules',k:'format',t:'Card, PayPal or transfer: let clients choose', s:'One invoice is one payment method — so decide before you create it.', b:['A payment provider fixes one method per invoice. The buyer does not get a free menu of options, so the method has to be chosen when the invoice is created.', 'For US clients that usually means card or PayPal; for EU clients card, Apple Pay or a SEPA transfer.']}, {m:'Getting paid',k:'pay',t:'How to get paid by clients abroad', s:'Card, QR or PayPal — and why one invoice carries one method.', b:['It starts with one question: which country does your client pay from? That decides both the method and the currency of the invoice.', 'Inside Russia, SBP by QR and Mir cards work; Visa and Mastercard do not work in Russia. For clients in Germany, Austria and the Netherlands we can offer card, Apple Pay, SEPA and PayPal; for the US, card and PayPal.', 'One invoice carries one payment method, so you choose it when you create the invoice — not afterwards. We do not accept crypto, by design.']}, {m:'Getting paid out',k:'pay',t:'Where the money goes: card, sole trader or USDT', s:'The money lands in your payment account, not with us.',b:['The money from your client does not come to us. It lands in your own payment account, and we only create the link and watch the status.','You can withdraw to a personal or sole-trader card, or in crypto — USDT. Crypto is a payout rail only: we do not accept crypto payments, by design.','Payouts open after 3 accepted payments, plus up to 7 days of checks. Pass verification (KYC) early: invoicing works without it, but payouts do not.']}, {m:'Reminders',k:'law',t:'Chasing a late payment without burning the bridge', s:'Tone matters more than wording: friendly first, formal later.',b:['A reminder is not a complaint. A first note one day before the due date works better than a stern one after it.','The tone escalates in steps: friendly, then businesslike, and only then formal with the terms spelled out. Each letter is politer than you feel like being.','We write in your name and never threaten. That gets you paid more often — the client is not embarrassed to answer.']}], de:[{m:'Ausstellen',k:'pay',t:'Zahlungsziel: 1 Tag oder 30?', s:'Kurze Fristen bringen Geld schneller — haben aber einen Haken.', b:['Eine kurze Zahlungsfrist bringt Geld schneller, erhöht aber das Risiko, dass der Kunde sie verpasst. Eine lange Frist ist für den Kunden bequemer.', 'Ein praktischer Haken: QR-Codes laufen ab. Für Rechnungen mit längerer Frist geben wir deshalb einen Kartenlink aus — der läuft nicht mit dem QR ab.']}, {m:'Eingang',k:'parse',t:'Doppelte Rechnungen erkennen', s:'Dieselbe Rechnung kommt oft zweimal.', b:['Dieselbe Rechnung kommt häufig doppelt: direkt vom Lieferanten und noch einmal über die Buchhaltung. Doppelt zu zahlen ist schnell passiert.', 'Wir gleichen Nummer, Betrag und Datum ab und markieren Überschneidungen, bevor Sie die Zahlung freigeben.']}, {m:'Formate und Recht',k:'format',t:'XRechnung und ZUGFeRD: was Sie wirklich brauchen', s:'Die Pflicht kommt schrittweise — wir sortieren, was für Sie gilt.', b:['Seit 2025 müssen Unternehmen in Deutschland elektronische Rechnungen empfangen können. Die Pflicht zum Ausstellen kommt gestaffelt: 2027 für größere, 2028 für alle.', 'Für den Austausch mit der Buchhaltung zählt am Ende das Format, das dort gelesen wird — CSV, DATEV oder XML. Wir arbeiten an DATEV, XRechnung und ZUGFeRD.']}, {m:'Zahlungen',k:'pay',t:'Zahlungen aus dem Ausland annehmen', s:'Karte, SEPA oder PayPal — eine Rechnung, eine Zahlungsart.', b:['Entscheidend ist, aus welchem Land der Kunde zahlt. Davon hängen Zahlungsart und Währung der Rechnung ab.', 'Innerhalb Russlands funktionieren SBP per QR und Mir-Karten; Visa und Mastercard funktionieren in Russland nicht. Für Kunden in Deutschland, Österreich und den Niederlanden gibt es Karte, Apple Pay, SEPA und PayPal; für die USA Karte und PayPal.', 'Eine Rechnung, eine Zahlungsart. Deshalb wird sie beim Erstellen gewählt — nicht danach. Krypto nehmen wir nicht an, bewusst nicht.']}, {m:'Auszahlung',k:'pay',t:'Wohin das Geld geht: Karte, Kleinunternehmer oder USDT', s:'Das Geld landet auf Ihrem Zahlungskonto, nicht bei uns.',b:['Das Geld Ihrer Kunden kommt nicht zu uns. Es landet auf Ihrem eigenen Zahlungskonto; wir erstellen nur den Link und verfolgen den Status.','Auszahlen können Sie auf Karte (Privatperson oder Kleinunternehmer) oder in Krypto — USDT. Krypto ist nur ein Auszahlungsweg: Zahlungen in Krypto nehmen wir bewusst nicht an.','Die Auszahlung öffnet nach 3 angenommenen Zahlungen, plus bis zu 7 Tage Prüfung. Verifizierung (KYC) besser früh machen: Rechnungen gehen auch ohne, Auszahlung nicht.']}, {m:'Mahnwesen',k:'law',t:'Mahnung schreiben, ohne Brücken zu verbrennen', s:'Der Ton zählt mehr als der Text: erst freundlich, dann förmlich.',b:['Eine Erinnerung ist keine Beschwerde. Eine kurze Notiz einen Tag vor Fälligkeit wirkt besser als eine strenge danach.','Der Ton steigert sich in Stufen: freundlich, dann sachlich, erst danach förmlich mit Fristen. Jeder Brief ist höflicher, als Ihnen zumute ist.','Wir schreiben in Ihrem Namen und drohen nie. Das bringt öfter Geld — der Kunde muss sich nicht schämen zu antworten.']}] }; const BLOG_CATS=[ {k:'all',n:{ru:'Все',en:'All',de:'Alle'}}, {k:'pay',n:{ru:'Оплата',en:'Payments',de:'Zahlungen'}}, {k:'parse',n:{ru:'Разбор',en:'Parsing',de:'Eingang'}}, {k:'format',n:{ru:'Форматы',en:'Formats',de:'Formate'}}, {k:'law',n:{ru:'Право и налоги',en:'Legal & tax',de:'Recht & Steuern'}} ]; let blogCat='all'; function setBlogCat(k){blogCat=k;renderBlog()} function renderBlog(){ const all=BLOG[lang]||BLOG.ru; const ttl={ru:'Статьи',en:'Articles',de:'Beiträge'}[lang]; const sub={ru:'Про счета, оплату, разбор и форматы. Три языка, дальше переезжает на основной сайт.', en:'On invoices, payments, parsing and formats. Three languages; the base moves to the main site.', de:'Zu Rechnungen, Zahlungen, Eingang und Formaten. Drei Sprachen; die Basis zieht auf die Hauptseite.'}[lang]; const chips=BLOG_CATS.map(cat=>{const n=cat.k==='all'?all.length:all.filter(a=>a.k===cat.k).length; if(!n)return '';return ``}).join(''); const list=all.map((a,i)=>({a:a,i:i})).filter(x=>blogCat==='all'||x.a.k===blogCat); document.getElementById('blog').innerHTML=`
${ttl}

${sub}

${chips}
${list.map(x=>`
${esc(x.a.m)}

${esc(x.a.t)}

${esc(x.a.s)}

${lang==='de'?'Lesen':lang==='en'?'Read':'Читать'} →
`).join('')}
${lang==='de'?'Die Redaktionsbasis liegt im Repo (blog-engine). Beiträge werden später auf die Hauptseite verschoben und verlinkt.' :lang==='en'?'The editorial base lives in the repo (blog-engine). Posts move to the main site later and get interlinked.' :'Редакционная база статей лежит в проекте (blog-engine). Позже переносим их на основной сайт и перелинковываем.'}
`; } function openPost(i){ const a=(BLOG[lang]||BLOG.ru)[i]; if(!a)return; appStep='post';$('#appTitle').textContent=a.m;$('#bnav').style.display='none'; try{history.replaceState(null,'','#article-'+lang+'-'+i)}catch(e){} $('#appContent').innerHTML=`
${esc(a.m)}

${esc(a.t)}

${esc(a.s)}

${a.b.map(p=>`

${esc(p)}

`).join('')}
`; show('app'); } function backToBlog(){show('landing');setTimeout(function(){try{setLang(lang)}catch(e){};const el=document.getElementById('blog');if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth'})},130)} /* ================= ПОРЯДОК СЕКЦИЙ ПО СТРАНЕ ================= */ const SECTIONS={ ru:['prio','what','modules','pricing','referral','blog','faq'], de:['prio','what','modules','pricing_beta','referral','blog','faq'], us:['prio','what','modules','calc','pricing_beta','referral','blog','faq'], other:['prio','what','modules','calc','pricing_beta','referral','blog','faq'] }; function applyOrder(){ const land=document.getElementById('landing'); const hero=land.querySelector('section.hero'); let prev=hero; (SECTIONS[country]||SECTIONS.ru).forEach(id=>{ const el=document.getElementById(id); if(!el)return; land.insertBefore(el,prev.nextSibling); prev=el; }); } /* ================= ПЕРЕОПРЕДЕЛЯЕМ ЯЗЫК/СТРАНУ ================= */ function renderAll(){if(annual)toggleAnnual(true);renderSelectors();renderWhat();renderModules();renderPriority();renderBlog();applyVis();applyOrder();recalcLoss()} setLang=function(v){ lang=(v==='en'||v==='de')?v:'ru'; document.documentElement.lang=lang; renderAll(); if(document.getElementById('app').classList.contains('active'))rerenderApp(); }; setCountry=function(v){ country=(COUNTRIES.some(c=>c.id===v))?v:'ru'; const auto={ru:'ru',de:'de',us:'en'}[country]; if(auto){lang=auto;try{document.documentElement.lang=lang}catch(e){}} currentCabTab=-1; renderAll(); if(document.getElementById('app').classList.contains('active'))rerenderApp(); }; /* ================= ЯКОРЬ «ТАРИФЫ» ================= */ /* #pricing виден только для RU, #pricing_beta — для DE/US. Ссылка в шапке/футере ведёт на #pricing, поэтому при DE/US клик ничего не делал. Ведём на тот блок тарифов, который сейчас виден. */ function scrollPricing(){ const a=document.getElementById('pricing'), b=document.getElementById('pricing_beta'); const pick=(a&&a.offsetParent)?a:((b&&b.offsetParent)?b:a); if(pick&&pick.scrollIntoView)pick.scrollIntoView({behavior:'smooth'}); } document.addEventListener('click',function(e){ const a=e.target.closest?e.target.closest('a[href="#pricing"]'):null; if(a){e.preventDefault();scrollPricing();} }); /* заглушки href="#" (Оферта / Политика) не должны бросать страницу наверх */ document.querySelectorAll('a[href="#"]').forEach(function(a){ a.setAttribute('aria-disabled','true'); a.addEventListener('click',function(ev){ev.preventDefault();}); }); /* ================= ТАРИФЫ, ДОСТУП И ПОДКЛЮЧЕНИЕ LAVA ================= */ /* Платежи и партнёрка требуют Lava.top. Слежение и разбор входящих — не требуют. Ссылку на оплату нельзя выдать, пока пользователь не зарегистрировался в Lava.top, не создал продукт по инструкции и не передал нам API-ключ. */ const PRICES={A:1490,B:390,C:690,D:2570}; const PLANS={ A:{n:{ru:'Выставляю и слежу',en:'Invoice and track your own',de:'Ausstellen und eigene verfolgen'},feat:['issue']}, B:{n:{ru:'Слежу',en:'Track invoices you did not issue',de:'Fremde Rechnungen verfolgen'},feat:['foreign']}, C:{n:{ru:'Разбор входящих',en:'Incoming parsing',de:'Eingang einlesen'},feat:['inbox','api']}, D:{n:{ru:'Всё сразу',en:'Everything',de:'Alles zusammen'},feat:['issue','foreign','inbox','api']} }; const PLAN_ORDER=['A','B','C','D']; function feats(p){return (PLANS[p]&&PLANS[p].feat)||[]} function isUpgrade(to){const cur=feats(plan),tgt=feats(to);return cur.length>0&&cur.every(f=>tgt.indexOf(f)>=0)} let plan=null, lavaStep=0, planPaid=false, freePass=false; /* Тариф храним ПО ПОЧТЕ: другой адрес = новый пользователь, ему снова показываем оплату. */ function stateKey(){return (userEmail||'anon').toLowerCase()} function loadState(){try{plan=localStorage.getItem('db_plan::'+stateKey())||null;lavaStep=+(localStorage.getItem('db_lava::'+stateKey())||0);planPaid=localStorage.getItem('db_paid::'+stateKey())==='1'}catch(e){plan=null;lavaStep=0;planPaid=false}} function saveState(){try{var k=stateKey();if(plan)localStorage.setItem('db_plan::'+k,plan);else localStorage.removeItem('db_plan::'+k);localStorage.setItem('db_lava::'+k,String(lavaStep));localStorage.setItem('db_paid::'+k,planPaid?'1':'0');localStorage.removeItem('db_plan');localStorage.removeItem('db_lava')}catch(e){}} function can(f){ if(!plan)return false; return feats(plan).indexOf(f)>=0; } function fmtRub(n){return n.toLocaleString('ru-RU')+' ₽'} function diffPrice(to){ if(!plan)return PRICES[to]; if(plan===to)return 0; const cur=feats(plan),tgt=feats(to); if(tgt.every(f=>cur.indexOf(f)>=0))return 0; if(cur.every(f=>tgt.indexOf(f)>=0))return Math.max(0,PRICES[to]-PRICES[plan]); return PRICES[to]; } function toLabel(to){ if(!plan||isUpgrade(to))return t('toPay'); return t('switchPlan'); } function payBlocked(){return lavaStepMath.round(PRICES[id]*12*0.6); const card=id=>{ const p=PLANS[id], mine=(plan===id), d=diffPrice(id); const yr=yrOf(id), moEq=Math.round(yr/12); const priceHtml=planAnnual ? `
${yr.toLocaleString('ru-RU')} ₽ / ${lavaT('perYear')} −40%
${lavaT('eqMonth')} ${moEq.toLocaleString('ru-RU')} ₽/мес
` : `
${fmtRub(PRICES[id])} / ${t('perMonth')}
${mine?t('active'):((plan&&d)?toLabel(id)+' '+fmtRub(d):'')}
`; return `
${mine?`${t('yourPlan')}`:''}

${lo(p.n)}

${priceHtml}
${desc[id][lang]}
`; }; $('#appContent').innerHTML=`

${t('plansTitle')}

${lavaT('planSubSimple')}

${PLAN_ORDER.map(card).join('')}
${t('payNote')}
`; } let planAnnual=false; function planRerender(){_navSilent=true;try{gotoPlans()}finally{_navSilent=false}} function togglePlanAnnual(on){planAnnual=on;planRerender()} function gotoPlanWait(){ appStep='planwait';$('#appTitle').textContent=lavaT('planWaitT');$('#bnav').style.display='none'; const p=PLANS[plan]||{};const price=planAnnual?Math.round(PRICES[plan]*12*0.6):PRICES[plan]; $('#appContent').innerHTML=`

${esc(lavaT('planWaitT'))}

${esc(lavaT('planWaitP'))}

${esc(lo(p.n)||String(plan))}
${fmtRub(price)}${planAnnual?' / год':''}
`; } /* тестовый режим: пока платёж не подключён, оплата отмечается этой кнопкой. когда Lava.top заработает — payDemo() заменить на реальную оплату. */ function payDemo(){ymGoal('pay_demo');planPaid=true;saveState();currentCabTab=0;gotoCabinet()} function freeParsePass(){freePass=true;const k=tabs().findIndex(x=>x.k==='inbox');currentCabTab=k<0?0:k;gotoCabinet()} function choosePlan(id){ ymGoal('plan_select'); plan=id;planPaid=false;freePass=false;saveState();currentCabTab=0;gotoPlanWait(); } function lockScreen(need){ appStep='locked';$('#appTitle').textContent=t('lockedTitle');$('#bnav').style.display='none'; const d=diffPrice(need); $('#appContent').innerHTML=`

${t('lockedTitle')}

${t('lockedP')} ${lo(PLANS[need].n)}.

${lo(PLANS[need].n)}
${fmtRub(PRICES[need])} / ${t('perMonth')}
${isUpgrade(need)?'+'+fmtRub(d):fmtRub(d)}
${isUpgrade(need)?t('upgradeNote'):t('switchNote')}
`; } /* ---- подключение приёма оплаты (Lava.top) ---- */ /* Отдельно от партнёрки: здесь только регистрация в Lava.top по нашей ссылке, создание продукта по инструкции и передача API-ключа. Ссылку на оплату нельзя выдать, пока ключ не передан. */ const LAVA_REF='https://lava.top/?ref=778195'; const LAVA_DONE=6; let lavaKey=''; function lavaT(k){const L=LAVATXT[lang]||LAVATXT.ru;const v=L[k];return v===undefined?LAVATXT.ru[k]:v} function gotoLava(){ appStep='lava';$('#appTitle').textContent=lavaT('lavaTitle');$('#bnav').style.display='none'; const L=lavaT('lavaSteps'); const rows=L.map((s,i)=>{ const done=(i${esc(s.btn)}`:''; let extra=''; if(i===5){ extra=(lavaStep>=LAVA_DONE) ? `
${lavaT('lavaKeyOk')}
` : `
${lavaT('lavaKeyErr')}
`; } return `
${done?'✓':i+1}
${esc(s.t)}
${esc(s.d)}
${btn}${extra}
`; }).join(''); const done=(lavaStep>=LAVA_DONE); $('#appContent').innerHTML=`

${lavaT('lavaTitle')}

${lavaT('lavaSub')}

${done?`
${t('lavaOk')}
`:''} ${rows}
${lavaT('lavaInstr')}
${lavaT('lavaInstrP')}
${t('lavaKeyNote')}
${lavaT('lavaNote')}
${lavaT('lavaHonest2')}
`; } function lavaStepTo(i){ if(i===1){try{window.open(LAVA_REF,'_blank','noopener')}catch(e){}} lavaStep=Math.min(LAVA_DONE,i+1);saveState();gotoLava(); } function lavaKeySubmit(){ const el=document.getElementById('lavaKey'); const v=((el&&el.value)||'').trim(); const er=document.getElementById('lavaKeyErr'); if(v.length<16){if(er)er.style.display='block';if(el)el.style.borderColor='var(--danger)';return} lavaKey=v;lavaStep=LAVA_DONE;saveState();gotoLava(); } /* ---- партнёрская программа: отдельный экран, без мастера Lava ---- */ function gotoRef(){ appStep='ref';$('#appTitle').textContent=t('refTitle');$('#bnav').style.display='none'; $('#appContent').innerHTML=`

${t('refTitle')}

${t('refNote')}

${t('refLinkLbl')}
${lavaT('refHowT')}
1. ${lavaT('refHow1')}
2. ${lavaT('refHow2')}
3. ${lavaT('refHow3')}
${lavaT('refHonest')}
`; } /* ===== ДОСТУП ПО ТАРИФУ ===== */ tabs=function(){ const t=[]; if(can('issue'))t.push({k:'inv',ic:ICON.inv,t:'t1'}); if(can('foreign'))t.push({k:'foreign',ic:ICON.track,t:'t0'}); if(can('inbox'))t.push({k:'inbox',ic:ICON.inbox,t:'t2'}); if(plan!=='D'){ if(!can('issue'))t.push({k:'inv',ic:ICON.inv,t:'t1',locked:true,cost:'D'}); if(!can('foreign'))t.push({k:'foreign',ic:ICON.track,t:'t0',locked:true,cost:'D'}); if(!can('inbox'))t.push({k:'inbox',ic:ICON.inbox,t:'t2',locked:true,cost:'D'}); } t.push({k:'more',ic:ICON.more,t:'t3'}); return t; }; renderNav=function(){ const ts=tabs(); document.getElementById('bnav').innerHTML=ts.map((x,i)=>`${x.ic}${t(x.t)}${x.locked?' 🔒':''}`).join(''); }; afterName=function(){ if(afterAuth==='parse'&&pendingParse){finishPendingParse()} else if(afterAuth==='ref'){gotoRef()} /* Явное намерение важнее: если человек шёл «выставить счёт», его нельзя выбрасывать в другой модуль (было: тариф «Разбор входящих» -> открывались «Входящие»). */ else if(afterAuth==='invoice'){ if(!plan){show('app');gotoPlans()} else if(can('issue')){show('app');gotoInvoice()} else {show('app');lockScreen('A')} } else if(!plan){show('app');gotoPlans()} else {show('app');gotoCabinet()} }; /* после регистрации возвращаемся к разбору: файл никуда не делся */ function finishPendingParse(){ const outId=pendingParse; pendingParse=null; show('landing'); setTimeout(function(){ try{renderAll()}catch(e){} const out=document.getElementById(outId); if(out&&window['_f_'+outId]){inboxFile(window['_f_'+outId],outId)} if(out)runParse(outId); const el=document.getElementById('modules'); if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth'}); },150); } gotoCabinet=function(){ if(!plan){show('app');gotoPlans();return} if(!planPaid&&!freePass){show('app');gotoPlanWait();return} if(freePass&&!planPaid){const k=tabs().findIndex(x=>x.k==='inbox');if(k>=0)currentCabTab=k} appStep='cabinet';$('#bnav').style.display='flex'; if(currentCabTab<0)currentCabTab=0; cabTab(currentCabTab); }; /* ================= ТАБЛИЦЫ-CRM ПО ТАРИФАМ ================= Пока нет базы — данные живут в памяти браузера. Разметка таблиц та, что пойдёт в продукт: колонки, статусы, действия и суммы. */ const TRACK={ ru:[{c:'Иван Петров',m:'ivan@studio.ru',no:'2214',sum:'34 000 ₽',d:'12.09',st:'nw'}, {c:'ООО «Логистик»',m:'billing@logistic.ru',no:'8841',sum:'12 300 ₽',d:'10.09',st:'reminded'}, {c:'Типография «Печать+»',m:'pay@pechat.ru',no:'771',sum:'5 400 ₽',d:'05.09',st:'overdue'}], en:[{c:'John Carter',m:'john@carterdesign.com',no:'2214',sum:'$980',d:'12 Sep',st:'nw'}, {c:'Northwind Ltd',m:'billing@northwind.com',no:'8841',sum:'$1 240',d:'10 Sep',st:'reminded'}, {c:'PrintHub Co',m:'pay@printhub.com',no:'771',sum:'$430',d:'05 Sep',st:'overdue'}], de:[{c:'Max Müller',m:'max@mueller-design.de',no:'2214',sum:'1 180 €',d:'12.09',st:'nw'}, {c:'Nordwind GmbH',m:'billing@nordwind.de',no:'8841',sum:'1 540 €',d:'10.09',st:'reminded'}, {c:'Druckhaus AG',m:'pay@druckhaus.de',no:'771',sum:'520 €',d:'05.09',st:'overdue'}] }; const PARSED={ ru:[{v:'ООО «Студия»',no:'1037',d:'12.09',sum:'38 000 ₽',dup:true,st:'parsed'}, {v:'ООО «Поставщик»',no:'2244',d:'11.09',sum:'7 800 ₽',dup:false,st:'checked'}, {v:'ИП Смирнов',no:'91',d:'03.09',sum:'2 300 ₽',dup:false,st:'parsed'}, {v:'ООО «Студия»',no:'1037',d:'13.09',sum:'38 000 ₽',dup:true,st:'parsed'}], en:[{v:'Northwind Ltd',no:'1037',d:'12 Sep',sum:'$640',dup:true,st:'parsed'}, {v:'Vendor Co',no:'2244',d:'11 Sep',sum:'$310',dup:false,st:'checked'}, {v:'Smith LLC',no:'91',d:'03 Sep',sum:'$180',dup:false,st:'parsed'}, {v:'Northwind Ltd',no:'1037',d:'13 Sep',sum:'$640',dup:true,st:'parsed'}], de:[{v:'Nordwind GmbH',no:'1037',d:'12.09',sum:'740 €',dup:true,st:'parsed'}, {v:'Lieferant GmbH',no:'2244',d:'11.09',sum:'310 €',dup:false,st:'checked'}, {v:'Schmidt e.K.',no:'91',d:'03.09',sum:'180 €',dup:false,st:'parsed'}, {v:'Nordwind GmbH',no:'1037',d:'13.09',sum:'740 €',dup:true,st:'parsed'}] }; /* ===== ПОЛНЫЙ НАБОР ГРАФОВ ДЛЯ РАЗБОРА ===== Принцип владельца: показываем максимум полей по рынку; если поле не распознано — оно остаётся в списке ПУСТЫМ (человек впишет или продиктует голосом). */ function PF(ru,en,de){return {ru:ru,en:en,de:de}} const PARSE_FIELDS={ ru:[ {g:PF('Счёт','Invoice','Rechnung'),k:'no',l:PF('Номер счёта','Invoice number','Rechnungsnummer')}, {g:PF('Счёт','Invoice','Rechnung'),k:'d',l:PF('Дата счёта','Invoice date','Rechnungsdatum')}, {g:PF('Счёт','Invoice','Rechnung'),k:'due',l:PF('Срок оплаты','Due date','Fälligkeit')}, {g:PF('Счёт','Invoice','Rechnung'),k:'basis',l:PF('Основание (договор)','Basis (contract)','Grundlage (Vertrag)')}, {g:PF('Счёт','Invoice','Rechnung'),k:'typ',l:PF('Тип документа','Document type','Dokumenttyp')}, {g:PF('Счёт','Invoice','Rechnung'),k:'cur',l:PF('Валюта','Currency','Währung')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'sup',l:PF('Название','Name','Name')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supInn',l:PF('ИНН','INN','INN')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supKpp',l:PF('КПП','KPP','KPP')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supOgrn',l:PF('ОГРН / ОГРНИП','OGRN','OGRN')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supAddr',l:PF('Адрес','Address','Anschrift')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supPhone',l:PF('Телефон','Phone','Telefon')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supBank',l:PF('Банк','Bank','Bank')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supBik',l:PF('БИК','BIK','BIK')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supAcc',l:PF('Расчётный счёт','Account','Konto')}, {g:PF('Поставщик','Vendor','Lieferant'),k:'supCacc',l:PF('Корр. счёт','Corr. account','Unterkonto')}, {g:PF('Покупатель','Bill to','Rechnungsempfänger'),k:'buy',l:PF('Название','Name','Name')}, {g:PF('Покупатель','Bill to','Rechnungsempfänger'),k:'buyInn',l:PF('ИНН','INN','INN')}, {g:PF('Покупатель','Bill to','Rechnungsempfänger'),k:'buyKpp',l:PF('КПП','KPP','KPP')}, {g:PF('Покупатель','Bill to','Rechnungsempfänger'),k:'buyAddr',l:PF('Адрес','Address','Anschrift')}, {g:PF('Суммы','Amounts','Beträge'),k:'net',l:PF('Без НДС','Net','Netto')}, {g:PF('Суммы','Amounts','Beträge'),k:'vatRate',l:PF('Ставка НДС','VAT rate','USt-Satz')}, {g:PF('Суммы','Amounts','Beträge'),k:'vat',l:PF('Сумма НДС','VAT amount','USt-Betrag')}, {g:PF('Суммы','Amounts','Beträge'),k:'sum',l:PF('Итого','Total','Brutto')}, {g:PF('Суммы','Amounts','Beträge'),k:'words',l:PF('Сумма прописью','Amount in words','Betrag in Worten')}, {g:PF('Особые отметки','Notes','Vermerke'),k:'noVat',l:PF('НДС не облагается','VAT exempt','USt-frei')}, {g:PF('Особые отметки','Notes','Vermerke'),k:'signed',l:PF('Подписант','Signed by','Unterzeichner')}, {g:PF('Источник','Source','Quelle'),k:'src',l:PF('Откуда пришёл','Source','Quelle')}, {g:PF('Источник','Source','Quelle'),k:'srcfile',l:PF('Имя файла','File name','Dateiname')}, {g:PF('Источник','Source','Quelle'),k:'from',l:PF('E-mail отправителя','Sender e-mail','Absender-E-Mail')}, {g:PF('Источник','Source','Quelle'),k:'at',l:PF('Дата поступления','Received','Eingang')}, {g:PF('Источник','Source','Quelle'),k:'fmt',l:PF('Формат файла','File format','Dateiformat')}, {g:PF('Дубликат','Duplicate','Dublette'),k:'dupPair',l:PF('Совпадает со №','Matches #','Doppelt zu Nr.')}, {g:PF('Дубликат','Duplicate','Dublette'),k:'st',l:PF('Статус','Status','Status')} ], de:[ {g:PF('Rechnung','Invoice','Rechnung'),k:'no',l:PF('Номер счёта','Invoice number','Rechnungsnummer')}, {g:PF('Rechnung','Invoice','Rechnung'),k:'d',l:PF('Дата счёта','Invoice date','Rechnungsdatum')}, {g:PF('Rechnung','Invoice','Rechnung'),k:'due',l:PF('Срок оплаты','Due date','Fälligkeit')}, {g:PF('Rechnung','Invoice','Rechnung'),k:'period',l:PF('Период услуги','Service period','Leistungszeitraum')}, {g:PF('Rechnung','Invoice','Rechnung'),k:'typ',l:PF('Тип документа','Document type','Dokumenttyp')}, {g:PF('Rechnung','Invoice','Rechnung'),k:'cur',l:PF('Валюта','Currency','Währung')}, {g:PF('Rechnung','Invoice','Rechnung'),k:'pay',l:PF('Способ оплаты','Payment method','Zahlungsart')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'sup',l:PF('Название','Name','Name')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'supAddr',l:PF('Адрес','Address','Anschrift')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'supTax',l:PF('Налоговый номер','Tax number','Steuernummer')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'ust',l:PF('USt-IdNr.','VAT ID','USt-IdNr.')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'supCountry',l:PF('Страна','Country','Land')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'supMail',l:PF('E-mail','E-mail','E-Mail')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'supBank',l:PF('Банк','Bank','Bank')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'supIban',l:PF('IBAN','IBAN','IBAN')}, {g:PF('Исполнитель','Issuer','Rechnungssteller'),k:'supBic',l:PF('BIC','BIC','BIC')}, {g:PF('Заказчик','Customer','Rechnungsempfänger'),k:'buy',l:PF('Название','Name','Name')}, {g:PF('Заказчик','Customer','Rechnungsempfänger'),k:'buyAddr',l:PF('Адрес','Address','Anschrift')}, {g:PF('Заказчик','Customer','Rechnungsempfänger'),k:'buyUst',l:PF('USt-IdNr.','VAT ID','USt-IdNr.')}, {g:PF('Заказчик','Customer','Rechnungsempfänger'),k:'custNo',l:PF('Номер клиента','Customer no.','Kundennummer')}, {g:PF('Заказчик','Customer','Rechnungsempfänger'),k:'orderNo',l:PF('Номер заказа','Order no.','Bestellnummer')}, {g:PF('Заказчик','Customer','Rechnungsempfänger'),k:'leit',l:PF('Leitweg-ID','Leitweg ID','Leitweg-ID')}, {g:PF('Суммы','Amounts','Beträge'),k:'net19',l:PF('Нетто 19 %','Net 19 %','Netto 19 %')}, {g:PF('Суммы','Amounts','Beträge'),k:'net7',l:PF('Нетто 7 %','Net 7 %','Netto 7 %')}, {g:PF('Суммы','Amounts','Beträge'),k:'net0',l:PF('Нетто 0 %','Net 0 %','Netto 0 %')}, {g:PF('Суммы','Amounts','Beträge'),k:'vatRate',l:PF('Ставка НДС','VAT rate','USt-Satz')}, {g:PF('Суммы','Amounts','Beträge'),k:'vat',l:PF('Сумма НДС','VAT amount','USt-Betrag')}, {g:PF('Суммы','Amounts','Beträge'),k:'sum',l:PF('Итого (брутто)','Total (gross)','Bruttobetrag')}, {g:PF('Особые отметки','Notes','Vermerke'),k:'klein',l:PF('§19 — без НДС','§19 — no VAT','§19 — ohne USt')}, {g:PF('Особые отметки','Notes','Vermerke'),k:'rc',l:PF('Reverse Charge','Reverse charge','Reverse Charge')}, {g:PF('Формат и источник','Format & source','Format & Quelle'),k:'fmt',l:PF('Формат','Format','Format')}, {g:PF('Формат и источник','Format & source','Format & Quelle'),k:'prof',l:PF('Профиль','Profile','Profil')}, {g:PF('Формат и источник','Format & source','Format & Quelle'),k:'src',l:PF('Откуда пришёл','Source','Quelle')}, {g:PF('Формат и источник','Format & source','Format & Quelle'),k:'srcfile',l:PF('Имя файла','File name','Dateiname')}, {g:PF('Формат и источник','Format & source','Format & Quelle'),k:'from',l:PF('E-mail отправителя','Sender e-mail','Absender-E-Mail')}, {g:PF('Формат и источник','Format & source','Format & Quelle'),k:'at',l:PF('Дата поступления','Received','Eingang')}, {g:PF('Бухгалтеру (вводит человек)','For the accountant','Für die Buchhaltung'),k:'berater',l:PF('Beraternummer','Advisor number','Beraternummer')}, {g:PF('Бухгалтеру (вводит человек)','For the accountant','Für die Buchhaltung'),k:'mandant',l:PF('Mandantennummer','Client number','Mandantennummer')}, {g:PF('Бухгалтеру (вводит человек)','For the accountant','Für die Buchhaltung'),k:'konto',l:PF('Konto','Account','Konto')}, {g:PF('Бухгалтеру (вводит человек)','For the accountant','Für die Buchhaltung'),k:'gkonto',l:PF('Gegenkonto','Offset account','Gegenkonto')}, {g:PF('Бухгалтеру (вводит человек)','For the accountant','Für die Buchhaltung'),k:'btext',l:PF('Buchungstext','Posting text','Buchungstext')}, {g:PF('Дубликат','Duplicate','Dublette'),k:'dupPair',l:PF('Совпадает со №','Matches #','Doppelt zu Nr.')}, {g:PF('Дубликат','Duplicate','Dublette'),k:'st',l:PF('Статус','Status','Status')} ], us:[ {g:PF('Invoice','Invoice','Rechnung'),k:'no',l:PF('Invoice #','Invoice #','Rechnungsnr.')}, {g:PF('Invoice','Invoice','Rechnung'),k:'d',l:PF('Invoice date','Invoice date','Rechnungsdatum')}, {g:PF('Invoice','Invoice','Rechnung'),k:'due',l:PF('Due date','Due date','Fälligkeit')}, {g:PF('Invoice','Invoice','Rechnung'),k:'po',l:PF('PO number','PO number','Bestellnr.')}, {g:PF('Invoice','Invoice','Rechnung'),k:'terms',l:PF('Terms','Terms','Zahlungsziel')}, {g:PF('Invoice','Invoice','Rechnung'),k:'cur',l:PF('Currency','Currency','Währung')}, {g:PF('Vendor','Vendor','Lieferant'),k:'sup',l:PF('Vendor name','Vendor name','Lieferant')}, {g:PF('Vendor','Vendor','Lieferant'),k:'supAddr',l:PF('Address','Address','Anschrift')}, {g:PF('Vendor','Vendor','Lieferant'),k:'supEin',l:PF('EIN / TIN','EIN / TIN','EIN / TIN')}, {g:PF('Vendor','Vendor','Lieferant'),k:'supMail',l:PF('E-mail','E-mail','E-Mail')}, {g:PF('Vendor','Vendor','Lieferant'),k:'supPhone',l:PF('Phone','Phone','Telefon')}, {g:PF('Bill to','Bill to','Rechnungsempfänger'),k:'buy',l:PF('Bill to','Bill to','Rechnungsempfänger')}, {g:PF('Bill to','Bill to','Rechnungsempfänger'),k:'buyAddr',l:PF('Address','Address','Anschrift')}, {g:PF('Amounts','Amounts','Beträge'),k:'net',l:PF('Subtotal','Subtotal','Netto')}, {g:PF('Amounts','Amounts','Beträge'),k:'taxRate',l:PF('Tax rate','Tax rate','Steuersatz')}, {g:PF('Amounts','Amounts','Beträge'),k:'tax',l:PF('Sales tax','Sales tax','Umsatzsteuer')}, {g:PF('Amounts','Amounts','Beträge'),k:'sum',l:PF('Total','Total','Gesamt')}, {g:PF('Source','Source','Quelle'),k:'src',l:PF('Source','Source','Quelle')}, {g:PF('Source','Source','Quelle'),k:'srcfile',l:PF('File name','File name','Dateiname')}, {g:PF('Source','Source','Quelle'),k:'from',l:PF('Sender e-mail','Sender e-mail','Absender-E-Mail')}, {g:PF('Source','Source','Quelle'),k:'at',l:PF('Received','Received','Eingang')}, {g:PF('Source','Source','Quelle'),k:'fmt',l:PF('File format','File format','Dateiformat')}, {g:PF('Duplicate','Duplicate','Dublette'),k:'dupPair',l:PF('Matches #','Matches #','Doppelt zu Nr.')}, {g:PF('Duplicate','Duplicate','Dublette'),k:'st',l:PF('Status','Status','Status')} ] }; PARSE_FIELDS.other=PARSE_FIELDS.ru; function parseFields(){return PARSE_FIELDS[country]||PARSE_FIELDS.other} /* ===== Реквизиты получателя и бухгалтера (ввод руками/голосом) ===== */ let HDR={firma:'',anschrift:'',steuer:'',ust:'',mail:'',berater:'',mandant:''}; function hdrLoad(){try{HDR=Object.assign(HDR,JSON.parse(localStorage.getItem('db_hdr')||'{}'))}catch(e){}} function hdrSet(k,v){HDR[k]=v;try{localStorage.setItem('db_hdr',JSON.stringify(HDR))}catch(e){}} /* ===== Пары дубликатов: одинаковые = один цвет ===== */ function dupPairs(){ const rows=parsedRows(),map={},pairOf={},colorOf={}; rows.forEach(function(r,i){const k=String(r.no||'').trim()+'|'+String(r.sum||'').trim(); if(!String(r.no||'').trim())return;(map[k]=map[k]||[]).push(i)}); let n=0; Object.keys(map).forEach(function(k){const idx=map[k]; if(idx.length>1){n++;idx.forEach(function(i){pairOf[i]=idx.filter(function(x){return x!==i}).map(function(x){return rows[x].no}).join(', ');colorOf[i]=((n-1)%3)+1})}}); return {count:n,pairOf:pairOf,colorOf:colorOf}; } /* ===== Правка полей строки + голосовой ввод ===== */ let CARD_I=-1; function parsedSave(){try{localStorage.setItem('db_parsed::'+(userEmail||'anon').toLowerCase(),JSON.stringify(parsedRows()))}catch(e){}} function parsedLoad(){try{const raw=localStorage.getItem('db_parsed::'+(userEmail||'anon').toLowerCase()); if(raw){const arr=JSON.parse(raw);if(arr&&arr.length){PARSEDD={lang:lang,rows:arr}}}}catch(e){}} function parsedSet(k,v){const r=parsedRows()[CARD_I];if(!r)return;r[k]=v;parsedSave();if(k==='st')crmRerender()} function dictate(id){ const el=document.getElementById(id);if(!el)return; const SR=window.SpeechRecognition||window.webkitSpeechRecognition; if(!SR){el.placeholder=(lang==='de'?'Diktat wird von diesem Browser nicht unterstützt':lang==='en'?'Dictation is not supported by this browser':'Диктовка не поддерживается этим браузером');return} const rec=new SR();rec.lang=(lang==='ru'?'ru-RU':lang==='de'?'de-DE':'en-US');rec.interimResults=false;rec.maxAlternatives=1; el.classList.add('listening'); rec.onresult=function(e){const t=String(e.results[0][0].transcript||'').trim(); el.value=(el.value?el.value+' ':'')+t; const k=el.getAttribute('data-pk');if(k)parsedSet(k,el.value);else{const hk=el.getAttribute('data-hk');if(hk)hdrSet(hk,el.value)} el.classList.remove('listening')}; rec.onerror=function(){el.classList.remove('listening')}; rec.onend=function(){el.classList.remove('listening')}; try{rec.start()}catch(e){el.classList.remove('listening')} } /* ===== Действия со строкой разбора ===== */ function parsedChecked(i){const r=parsedRows()[i];if(!r)return;r.st='checked';parsedSave();crmRerender()} function parsedVoid(i){const r=parsedRows()[i];if(!r)return;askDelete(lavaT('delAsk'),function(){r.st='void';parsedSave();crmRerender()})} function parsedDel(i){const r=parsedRows()[i];if(!r)return; askDelete(lavaT('delAsk'),function(){const rows=parsedRows();const j=rows.indexOf(r);if(j>=0)rows.splice(j,1);parsedSave();gotoCabinet()})} /* Колонки обзора зависят от страны пользователя: он видит графы своей стороны. Подписи переводятся вместе с языком интерфейса. */ const INBOX_COLS={ ru:[{k:'v',l:{ru:'Поставщик',en:'Vendor',de:'Lieferant'}}, {k:'inn',l:{ru:'ИНН',en:'INN (tax ID)',de:'INN (Steuernr.)'}}, {k:'no',l:{ru:'Номер',en:'Number',de:'Nummer'}}, {k:'d',l:{ru:'Дата',en:'Date',de:'Datum'}}, {k:'sum',l:{ru:'Сумма',en:'Amount',de:'Betrag'},num:true}, {k:'nds',l:{ru:'НДС',en:'VAT',de:'USt'}}, {k:'src',l:{ru:'Откуда',en:'Source',de:'Quelle'}}, {k:'dup',l:{ru:'Дубликат',en:'Duplicate',de:'Dublette'}}, {k:'st',l:{ru:'Статус',en:'Status',de:'Status'}}], de:[{k:'v',l:{ru:'Поставщик',en:'Vendor',de:'Lieferant'}}, {k:'no',l:{ru:'№ счёта',en:'Invoice no.',de:'Rechnungsnr.'}}, {k:'d',l:{ru:'Дата счёта',en:'Invoice date',de:'Rechnungsdatum'}}, {k:'sum',l:{ru:'Сумма',en:'Amount',de:'Betrag'},num:true}, {k:'ust',l:{ru:'USt-IdNr',en:'VAT ID',de:'USt-IdNr'}}, {k:'leit',l:{ru:'Leitweg-ID',en:'Leitweg ID',de:'Leitweg-ID'}}, {k:'fmt',l:{ru:'Формат',en:'Format',de:'Format'}}, {k:'src',l:{ru:'Откуда',en:'Source',de:'Quelle'}}, {k:'dup',l:{ru:'Дубликат',en:'Duplicate',de:'Dublette'}}, {k:'st',l:{ru:'Статус',en:'Status',de:'Status'}}], us:[{k:'v',l:{ru:'Поставщик',en:'Vendor',de:'Lieferant'}}, {k:'no',l:{ru:'№ счёта',en:'Invoice #',de:'Rechnungsnr.'}}, {k:'po',l:{ru:'№ заказа (PO)',en:'PO number',de:'Bestellnr.'}}, {k:'d',l:{ru:'Дата',en:'Date',de:'Datum'}}, {k:'sum',l:{ru:'Сумма',en:'Amount',de:'Betrag'},num:true}, {k:'tax',l:{ru:'Sales tax',en:'Sales tax',de:'Sales tax'}}, {k:'terms',l:{ru:'Условия',en:'Terms',de:'Zahlungsziel'}}, {k:'src',l:{ru:'Откуда',en:'Source',de:'Quelle'}}, {k:'dup',l:{ru:'Дубликат',en:'Duplicate',de:'Dublette'}}, {k:'st',l:{ru:'Статус',en:'Status',de:'Status'}}] }; INBOX_COLS.other=INBOX_COLS.ru.map(c=>Object.assign({},c)); function inboxCols(){return INBOX_COLS[country]||INBOX_COLS.other} /* демо-значения добавочных полей: у каждой страны свои реквизиты */ Object.keys(PARSED).forEach(function(l){PARSED[l].forEach(function(r,i){ /* Германия */ r.ust='DE8112345'+(i+1); r.leit=(i===0?'04011000-12345-31':''); r.fmt=(i===0?'ZUGFeRD':'XRechnung'); r.po='PO-441'+(i+1); r.terms='Net 30'; r.tax=(i===0?'6.25% · $40.00':''); r.inn=['7712345678','502345678901','773456789012'][i]||'7712345678'; r.nds=(i===0?'НДС 22 %':'НДС не облагается (НПД)'); r.cur=(l==='ru'?'₽':l==='de'?'€':'$'); r.period=(i===0?'01.09–30.09.2026':''); r.supInn=r.inn; r.supKpp=(l==='ru'?'773401001':''); r.supOgrn=(i===0?'1157746123456':''); r.supAddr=(l==='ru'?'г. Москва, ул. Ленина, 1':l==='de'?'Musterstraße 12, 10115 Berlin':'500 Market St, San Francisco, CA'); r.supBank=(l==='ru'?'АО «Т-Банк»':l==='de'?'Deutsche Bank':'Chase'); r.supBik=(l==='ru'?'044525974':''); r.supAcc=(l==='ru'?'40702810600000012345':''); r.supCacc=(l==='ru'?'30101810145250000974':''); r.supIban=(l==='de'?'DE89 3704 0044 0532 0130 00':''); r.supBic=(l==='de'?'DEUTDEFFXXX':''); r.supTax=(l==='de'?'12/345/67890':''); r.supMail=(i===0?'rechnung@'+(l==='de'?'lieferant.de':'vendor.com'):''); r.supPhone=(i===0?'+7 495 123-45-67':''); r.supEin=(l==='us'?'84-1234567':''); r.buyInn=(l==='ru'?'502345678901':''); r.buyKpp=(l==='ru'?'502301001':''); r.buyUst=(l==='de'?'DE998877665':''); r.buyAddr=r.supAddr; r.custNo=(l==='de'?'K-1042':''); r.orderNo=(l==='de'?'PO-4417':''); r.buy=(l==='ru'?'ООО «Студия»':''); r.net19=(l==='de'&&i===0?'3 361,34 €':''); r.net7=''; r.net0=(l==='de'&&i!==0?'310,00 €':''); r.vatRate=(l==='ru'?'22 %':l==='de'?(i===0?'19 %':'0 %'):''); r.vat=(l==='de'&&i===0?'638,66 €':''); r.net=(l==='ru'?(i===0?'38 000 ₽':''):(l==='us'?(i===0?'$640.00':''):'')); r.words=(l==='ru'&&i===0?'Тридцать восемь тысяч рублей 00 копеек':''); r.basis=(l==='ru'?'Договор № 14/2026':''); r.typ=(i===0?(l==='ru'?'Счёт на оплату':l==='de'?'Rechnung':'Invoice'):''); r.signed=(l==='ru'&&i===0?'Иванов И. И.':''); r.noVat=(l==='ru'&&i!==0?'НПД, НДС не облагается':''); r.klein=(l==='de'&&i!==0?'§19 UStG':'') ; r.rc=''; r.pay=(l==='de'?'Überweisung':''); r.src=(i%2===0?'mail':'file'); r.srcfile=(i===0?'rechnung-1037.pdf':(i===1?'invoice-2244.xlsx':'scan-91.jpg')); r.from=(i%2===0?('buchhaltung@'+(l==='de'?'lieferant.de':'vendor.com')):''); r.at=(i===0?'12.09':(i===1?'11.09':'03.09')); r.prof=(l==='de'?(i===0?'EN 16931 (Comfort)':'XR'):''); r.berater=(l==='de'?'1234567':''); r.mandant=(l==='de'?'7654321':''); r.konto=(l==='de'?'1200':''); r.gkonto=(l==='de'?'70000':''); r.btext=(l==='de'?'Eingangsrechnung':''); r.dupPair=''; /* умышленно оставлены ПУСТЫМИ (поле есть, значения нет — человек впишет или продиктует) */ r.due=''; r.po=(l==='us')?r.po:r.po; r.taxRate=(l==='us'&&i===0?'6.25%':''); if(i===2){r.from='';r.srcfile='';r.at='';r.src='file'} })}); function countryExtra(){ if(country==='de')return 'USt-IdNr DE811234567 · Leitweg-ID 04011000'; if(country==='us')return 'Net 30 · PO-4417'; if(country==='ru')return (lang==='de'?'USt 22% (seit 2026)':'НДС 22% (с 2026) · ИНН в реквизитах'); return ''; } let TRACKD=null, PARSEDD=null; function trackRows(){if(!TRACKD||TRACKD.lang!==lang)TRACKD={lang:lang,rows:(TRACK[lang]||TRACK.ru).map(r=>Object.assign({},r))};return TRACKD.rows} function parsedRows(){if(!PARSEDD||PARSEDD.lang!==lang)PARSEDD={lang:lang,rows:(PARSED[lang]||PARSED.ru).map(r=>Object.assign({},r))};return PARSEDD.rows} const STLBL={ partial:{ru:'Оплачен частично',en:'Part paid',de:'Teilzahlung'}, paused:{ru:'Пауза',en:'Paused',de:'Pause'}, draft:{ru:'Черновик',en:'Draft',de:'Entwurf'},sent:{ru:'Ждёт оплаты',en:'Awaiting',de:'Offen'}, reminded:{ru:'Напомнили',en:'Reminded',de:'Erinnert'},overdue:{ru:'Просрочен',en:'Overdue',de:'Überfällig'}, paid:{ru:'Оплачен',en:'Paid',de:'Bezahlt'},nw:{ru:'Пришёл из почты',en:'From email',de:'Aus E-Mail'}, parsed:{ru:'Разобран',en:'Parsed',de:'Eingelesen'},checked:{ru:'Проверен',en:'Checked',de:'Geprüft'}}; function stl(k){const x=STLBL[k];return x?(x[lang]||x.ru):k} function numOf(v){const s=String(v||'');const c=(s.match(/[₽€$]/)||[''])[0];const n=parseFloat(s.replace(/[^\d.,]/g,'').replace(/[\s,]/g,'').replace(',','.'))||parseFloat(s.replace(/[^\d.]/g,''))||0;return {n:n,c:c||''}} function totLine(rows,get){ const t={}; rows.forEach(r=>{const {n,c}=numOf(get(r));const k=c||'—';t[k]=(t[k]||0)+n}); const parts=Object.keys(t).filter(k=>k!=='—').map(k=>t[k].toLocaleString('ru-RU')+' '+k); const n=rows.length; return (lang==='de'?n+' Rechnung(en)':lang==='en'?n+' invoice(s)':n+' счёт(ов)')+(parts.length?' · '+parts.join(' · '):''); } function pill(k){return ''+esc(stl(k))+''} let searchQ=''; function crmSearch(v){searchQ=(v||'').toLowerCase();crmRerender()} function crmRerender(){_navSilent=true;try{cabTab(currentCabTab)}finally{_navSilent=false}} function filterRow(list,key){ const l=(searchQ||''); return l?list.filter(x=>{const {n,c}=numOf('');return (String(x.c||x.v||'')+' '+String(x.m||'')+' '+String(x.no||'')+' '+String(x.item||'')+' '+String(x.sum||'')).toLowerCase().indexOf(l)>=0}):list; } /* --- кабинет 1: «Выставляю» — таблица своих счетов --- */ function renderInvTab(){ const all=cards(); const cnt={all:all.length, wait:all.filter(c=>c.stamp==='sent'||c.stamp==='overdue'||c.stamp==='reminded').length, over:all.filter(c=>c.stamp==='overdue').length, paid:all.filter(c=>c.stamp==='paid').length}; const b=(k,cls,label)=>``; let rows=all.filter(c=>invFilter==='all'?true:invFilter==='wait'?(c.stamp==='sent'||c.stamp==='overdue'||c.stamp==='reminded'):invFilter==='over'?c.stamp==='overdue':invFilter==='paid'?c.stamp==='paid':true); const q=(searchQ||''); if(q)rows=rows.filter(c=>(String(c.no)+' '+String(c.client)+' '+String(c.item)).toLowerCase().indexOf(q)>=0); const paid=all.filter(c=>c.stamp==='paid'); const work=all.filter(c=>c.stamp!=='paid'); const recv=paid.reduce((a,c)=>a+(numOf(c.sum).n||0),0); const curC=(numOf((all[0]||{}).sum)||{}).c||'₽'; const wait=all.filter(c=>c.stamp==='sent'||c.stamp==='overdue'||c.stamp==='reminded'); const nexts=all.map(c=>nextOf(c)).filter(x=>x&&x!=='—'); const kpi=`
${recv.toLocaleString('ru-RU')} ${curC}${esc(lavaT('kpiRecv'))}
${work.length}${esc(lavaT('kpiWork'))}
${wait.length}${esc(lavaT('kpiWait'))}
${nexts.length?nexts[0]:'—'}${esc(lavaT('kpiNext'))}
`; const d0=DRAFT[lang]; const draftRow=(invFilter==='all'&&(d0.client||d0.sum||d0.item))?`${esc(d0.client||t('draftClient'))}
${t('draftTitle')} · ${esc(d0.item||t('draftEmpty'))}
${d0.sum?esc(d0.sum)+' '+esc(d0.cur):'—'}
${esc(d0.due||'')}
${esc(stl('draft'))}
${esc(lavaT('noPlan'))}
`:''; const body=draftRow+rows.map(c=>{ const i=all.indexOf(c); const od=odOf(c), nx=nextOf(c); const menu=(openMenuRow===i)?`
${['loy','neu','firm'].map(k=>``).join('')}
`:''; return `${toneDot(c)}${esc(c.client)}
${esc(c.no)} · ${esc(c.item)}
${countryExtra()?`
${esc(countryExtra())}
`:''} ${esc(c.sum)}
${od?esc(lavaT('odDays').replace('{n}',od)):esc(c.due)}
${pill(c.stamp)}
${nx&&nx!=='—'?esc(lavaT('nextRem').replace('{d}',nx)):esc(lavaT('noPlan'))}
`+menu; }).join(''); const ladder=scheduleBlock('inv'); return kpi+`
${b('all','',''+t('statsAll'))}${b('wait','c-acc',t('statsWait'))}${b('over','c-dan',t('statsOver'))}${b('paid','c-ok',t('statsPaid'))}
${rows.length?`
${body}
${esc(lavaT('thClient'))}${esc(lavaT('thSum'))}${esc(lavaT('thStatus'))}
`:`
${esc(lavaT('statsEmpty'))}
`}
${totLine(rows,c=>c.sum)}${esc(lavaT('protoNote'))}
${ladder}`; } /* --- кабинет 2: «Слежу» — таблица счетов из почты --- */ let trkStyle='ladder', trkPeriod='3d', trkChannel='mail', trkReport=true, trackFormOpen=false, tfFile='', trkSetOpen=false; function toggleTrkSet(){trkSetOpen=!trkSetOpen;crmRerender()} function setTrk(k,v){ if(k==='style')trkStyle=v; if(k==='period')trkPeriod=v; if(k==='channel')trkChannel=v; crmRerender() } function toggleTrkReport(){trkReport=!trkReport;crmRerender()} function toggleTrackForm(){trackFormOpen=!trackFormOpen;crmRerender()} function tfPick(inp){tfFile=(inp.files&&inp.files[0]&&inp.files[0].name)||'';crmRerender()} function addTrackRow(){ const g=id=>((document.getElementById(id)||{}).value||'').trim(); const c=g('tf-client'), sum=g('tf-sum'); if(!c||!sum){['tf-client','tf-sum'].forEach(id=>{const e=document.getElementById(id);if(e)e.style.borderColor='var(--danger)'});return} const no=g('tf-no')||String(2300+trackRows().length); trackRows().unshift({c:c,m:g('tf-mail')||'—',no:no,sum:sum,d:g('tf-due')||t('today'),st:'nw', tone:(trkStyle==='loy'?'loy':trkStyle==='neu'?'neu':trkStyle==='firm'?'firm':'loy'),next:lavaT('inDays'),agree:g('tf-agree'),file:tfFile}); trackFormOpen=false;tfFile='';crmRerender(); const m=document.getElementById('trackMsg'); if(m)m.textContent=lavaT('addP'); } function showLetterFor(i){ const r=trackRows()[i]||{c:'Иван',no:'2214',sum:'34 000 ₽',d:'12.09'}; const style=(trkStyle==='ladder')?(r.st==='overdue'?'firm':(r.st==='nw'?'loy':'neu')):(trkStyle==='loy'?'loy':trkStyle==='neu'?'neu':'firm'); const L=(LETTERS[lang]||LETTERS.ru)[style]; const txt=L.replace('{name}',r.c).replace('{no}',r.no).replace('{sum}',r.sum).replace('{d}',r.d); appStep='letter';$('#appTitle').textContent=lavaT('letterT');$('#bnav').style.display='none'; $('#appContent').innerHTML=`

${esc(lavaT('letterT'))}

${esc((TONEL[style]||{})[lang]||'')}

${esc(txt)}
${esc(lavaT('letterNote'))}
`; show('app'); } function renderTrackTab(){ const rows=filterRow(trackRows()); const gmail=window.__gmail; const chip=(k,v,lbl)=>``; const allT=trackRows(); const rep={n:allT.length,w:allT.filter(r=>r.st!=='paid').length,o:allT.filter(r=>r.st==='overdue').length, rec:0}; allT.forEach(r=>{if(r.st==='paid')rep.rec+=(numOf(r.sum).n||0)}); const curC=((numOf((allT[0]||{}).sum)||{}).c)||'₽'; const settings=`
${esc(lavaT('trkT'))}
${esc(lavaT('trkChannel'))}${chip('channel','mail',lavaT('perMail'))}${chip('channel','tg',lavaT('perTg'))}
${esc(lavaT('repT'))}
${esc(lavaT('repP'))}
${esc(lavaT('repLine'))}: ${rep.n} ${esc(lavaT('repRows'))} · ${rep.w} ${esc(lavaT('repWait'))} · ${rep.o} ${esc(lavaT('repOver'))} · ${rep.rec.toLocaleString('ru-RU')} ${curC} ${esc(lavaT('repRecv'))}
`; const form=trackFormOpen?`
${esc(lavaT('addT'))}
${esc(lavaT('addP'))}
`:''; const addBtn=trackFormOpen?'':`
`; const body=rows.map((r,idx)=>{ const i=trackRows().indexOf(r); const menu=(openMenuRow===(1000+i))?`
${['loy','neu','firm'].map(k=>``).join('')}
`:''; return `${esc(r.c)}
${esc(r.m)} · № ${esc(r.no)}
${r.agree?`
${esc(r.agree)}
`:''} ${esc(r.sum)}
${esc(r.d)}
${pill(r.st)}
${r.next?esc(lavaT('nextRem').replace('{d}',r.next)):esc(lavaT('noPlan'))}
`+menu; }).join(''); return `
${t('gmailT')}
${t('gmailP')}
${gmail?`
${t('gmailOk')}
` :``}
${t('gmailDemo')}
${addBtn}${form}
${esc(lavaT('politeTag'))}
${rows.length?`
${body}
${esc(lavaT('thWho'))}${esc(lavaT('thSum'))}${esc(lavaT('thStatus'))}
`:`
${esc(lavaT('statsEmpty'))}
`}
${totLine(rows,r=>r.sum)}${esc(lavaT('protoNote'))}
${settings}${scheduleBlock('track')}`; } function toggleTrackMenu(i,ev){if(ev)ev.stopPropagation();openMenuRow=(openMenuRow===(1000+i)?-1:1000+i);crmRerender()} function trackTone(i,k){ const r=trackRows()[i]; if(!r)return; r.tone=k; r.st='reminded'; r.next=lavaT('inDays'); openMenuRow=-1; crmRerender(); const m=document.getElementById('trackMsg'); if(m)m.textContent=(TONEASK[k][lang]||TONEASK[k].ru)+': '+lavaT('demoRemind'); } function reportNow(){ const el=document.getElementById('trackMsg'); if(el)el.textContent=lavaT('repSent'); } function trackPaid(i){const r=trackRows()[i];if(!r)return;r.st='paid';crmRerender()} function trackHide(i){const r=trackRows()[i];askDelete(lavaT('delAsk'),function(){trackRows().splice(i,1);crmRerender()})} /* --- кабинет 3: «Разбор входящих» — таблица разобранного --- */ function renderInboxTab(){ hdrLoad(); parsedLoad(); const rows=filterRow(parsedRows()); const DP=dupPairs(); const cols=inboxCols(); const srcLbl=v=>(v==='mail'?lavaT('srcMail'):v==='file'?lavaT('srcFile'):''); const body=rows.map(r=>{ const i=parsedRows().indexOf(r); const cc=DP.colorOf[i]; const cells=cols.map(col=>{ const L=esc(lo(col.l)); if(col.k==='v')return `${esc(r.v||'')}`; if(col.k==='no')return `${esc(r.no?((lang==='ru'?'№ ':lang==='de'?'Nr. ':'')+r.no):'')}`; if(col.k==='sum')return `${esc(r.sum||'')}`; if(col.k==='src')return `${esc(srcLbl(r.src))}${r.srcfile?`
${esc(r.srcfile)}
`:''}`; if(col.k==='dup')return `${cc?`${esc(lavaT('dupPairN').replace('{n}',String(cc)))}${(DP.pairOf[i]||r.dupPair)?`
${esc(lavaT('dupPair'))} ${esc(DP.pairOf[i]||r.dupPair)}
`:''}`:''}`; if(col.k==='st')return `${pill(r.st)}`; return `${esc(r[col.k]||'')}`; }).join(''); return `${cells}
` +`` +(r.st==='checked'?'':``) +`
`; }).join(''); const head=cols.map(c=>`${esc(lo(c.l))}`).join('')+''; const ctryLbl=(country==='de'?(lang==='de'?'Felder für Deutschland':lang==='en'?'Fields for Germany':'Графы для Германии') :country==='us'?(lang==='de'?'Felder für die USA':lang==='en'?'Fields for the US':'Графы для США') :(lang==='de'?'Felder für Russland':lang==='en'?'Fields for Russia':'Графы для России')); const hdrRow=(k,l)=>`
` +`` +`
`; const hdrBox=`
${esc(lavaT('ourHdrT'))} ${hdrRow('firma',lavaT('hdrFirma'))}${hdrRow('anschrift',lavaT('hdrAddr'))}${hdrRow('steuer',lavaT('hdrTax'))} ${hdrRow('ust',lavaT('hdrUst'))}${hdrRow('mail',lavaT('hdrMail'))} ${hdrRow('berater',lavaT('hdrBerater'))}${hdrRow('mandant',lavaT('hdrMandant'))}
${esc(lavaT('hdrNote'))}
`; return hdrBox+`
${t('emptyInbox')}
${lang==='de'?'Die Datei bleibt bei Ihnen — nichts wird hochgeladen':lang==='en'?'The file stays with you — nothing is uploaded':'Файл остаётся у вас — загрузки на сервер нет'}
${esc(ctryLbl)}
${rows.length?`
${head}${body}
`:`
${esc(lavaT('statsEmpty'))}
`}
${totLine(rows,r=>r.sum)}${esc(lavaT('protoNote'))}
${t('mailOpt')}
`; } /* Карточка строки разбора: ВСЕ графы рынка по группам. Пустое поле остаётся пустым — лучше пустая графа, чем отсутствующая: человек впишет или продиктует голосом. */ function parseCard(i){ CARD_I=i; const r=parsedRows()[i]; if(!r)return; appStep='pcard'; $('#appTitle').textContent=lavaT('parseCardT'); $('#bnav').style.display='none'; const groups={}; parseFields().forEach(x=>{const g=lo(x.g);(groups[g]=groups[g]||[]).push(x)}); let html=`

${esc(r.v||lavaT('parseCardT'))}

${esc((r.no?((lang==='ru'?'№ ':lang==='de'?'Nr. ':'')+r.no):'')+(r.sum?' · '+r.sum:''))}

`; Object.keys(groups).forEach(function(g){ html+=`
${esc(g)}
`+groups[g].map(function(x){ const DP2=dupPairs(); const k=x.k; let val=(r[k]==null?'':String(r[k])); if(k==='dupPair')val=DP2.pairOf[parsedRows().indexOf(r)]||val; if(k==='st')val=r.st||''; if(k==='st')return `
${pill(r.st)}
`; return `
` +`` +`
`; }).join('')+`
`; }); html+=`
`; $('#appContent').innerHTML=html; } /* Расписание напоминаний: шаги (день · тон · канал), тумблеры и предпросмотр письма. */ let SCHED=[ {d:'−3',tone:'loy',ch:'email',on:true}, {d:'0',tone:'loy',ch:'email',on:true}, {d:'+5',tone:'neu',ch:'email',on:true}, {d:'+10',tone:'neu',ch:'email',on:true}, {d:'+18',tone:'firm',ch:'email',on:true} ]; let escAuto=true; const SECTOPEN={inv:false,track:false}; let openMenuRow=-1, ladderOpen=false; function toggleSched(scope){SECTOPEN[scope]=!SECTOPEN[scope];crmRerender()} function toggleEscAuto(){escAuto=!escAuto;crmRerender()} function schSet(i,f,v){const r=SCHED[i];if(!r)return;r[f]=v;crmRerender()} function schToggle(i){const r=SCHED[i];if(!r)return;r.on=!r.on;crmRerender()} function schDel(i){askDelete(lavaT('delAsk'),function(){SCHED.splice(i,1);crmRerender()})} function schAdd(){SCHED.push({d:'+'+(25*SCHED.length),tone:'firm',ch:'email',on:true});crmRerender()} function showLetterTone(tone){ const L=(LETTERS[lang]||LETTERS.ru)[tone]||(LETTERS[lang]||LETTERS.ru).loy; const txt=L.replace('{name}','Иван Петров').replace('{no}','1042').replace('{sum}','12 500 ₽').replace('{d}','5 дн.'); appStep='letter';$('#appTitle').textContent=lavaT('letterT');$('#bnav').style.display='none'; $('#appContent').innerHTML=`

${esc(lavaT('letterT'))}

${esc((TONEL[tone]||{})[lang]||'')}

${esc(txt)}
${esc(lavaT('letterNote'))}
`; show('app'); } function scheduleBlock(scope){ const open=SECTOPEN[scope]; const on=SCHED.filter(x=>x.on).length; const rows=SCHED.map((r,i)=>`
${esc(lavaT('schDay'))}
`).join(''); return `
${esc(lavaT('ladderT'))}
${esc(lavaT('schedSum').replace('{n}',on))}
${esc(lavaT('escAuto'))}
${open?`
${rows}
${esc(lavaT('schedNote'))}
`:''}
`; } function toggleRowMenu(i,ev){if(ev)ev.stopPropagation();openMenuRow=(openMenuRow===i?-1:i);crmRerender()} function rowTone(i,k){ const c=cards()[i]; if(!c)return; c.tone=k; c.stamp='reminded'; c.next=lavaT('inDays'); openMenuRow=-1; crmRerender(); const m=document.getElementById('invMsg'); if(m)m.textContent=(TONEASK[k][lang]||TONEASK[k].ru)+': '+lavaT('demoSend'); } function rowToneSet(i,k){const c=cards()[i];if(!c)return;c.tone=k;crmRerender()} function trackToneSet(i,k){const r=trackRows()[i];if(!r)return;r.tone=k;crmRerender()} function rowPause(i){const c=cards()[i];if(!c)return;c.stamp='paused';c.next='—';c.tone=(c.tone||'loy');openMenuRow=-1;crmRerender()} function rowVoid(i){const c=cards()[i];if(!c)return;askDelete(lavaT('delAsk'),function(){c.stamp='void';c.sl=lavaT('voided');c.next='—';openMenuRow=-1;crmRerender()})} function rowPaid(i){const c=cards()[i];if(!c)return;c.stamp='paid';c.sl=t('markPaid');c.due=t('markPaid');c.next='—';openMenuRow=-1;crmRerender()} function markChecked(i){const r=parsedRows()[i];if(!r)return;r.st='checked';crmRerender()} function addParsedRow(sum){ const v=(window._f_cabInboxOut&&window._f_cabInboxOut.name)||''; parsedRows().unshift({v:(lang==='de'?'Datei ':lang==='en'?'File ':'Файл ')+String(v).slice(0,22),no:'—',d:t('today'),sum:sum||'—',dup:false,st:'parsed'}); } /* ================= ТЕКСТЫ ПОДКЛЮЧЕНИЯ ОПЛАТЫ ================= */ const LAVATXT={ ru:{srcMail:'почта',srcFile:'файл',dupPairN:'Дубликат {n}',delRow:'Удалить строку',dictate:'Продиктовать',parseCardT:'Разбор счёта',ourHdrT:'Наши реквизиты и бухгалтер',hdrFirma:'Наша фирма / имя',hdrAddr:'Адрес',hdrTax:'Steuernummer / налоговый номер',hdrUst:'USt-IdNr. / ИНН',hdrMail:'E-mail',hdrBerater:'Beraternummer (номер консультанта)',hdrMandant:'Mandantennummer (номер у консультанта)',hdrNote:'Эти поля в счёте не печатаются — их вводит человек. Они уходят в выгрузку для бухгалтера.',planWaitT:'Тариф выбран — ожидает оплаты',planWaitP:'Кабинет откроется после оплаты. Оплата пока идёт в тестовом режиме — нажмите «Оплатить (демо)», чтобы продолжить.',planPayDemo:'Оплатить (демо)',planChange:'Сменить тариф',planFreeParse:'Разобрать 1 документ бесплатно',actVoid:'Аннулировать',voided:'Аннулирован',delDraft:'Удалить черновик',draftKept:'Черновик сохранён на этом устройстве',demoLink:'пример — ссылка не работает',delAsk:'Удалить? Это действие нельзя отменить.',delYes:'Удалить',dupMark:'дубликат',newInv:'Выставить счёт',statsEmpty:'В этой группе пока пусто.', fClient:'Клиент',fItem:'Позиция',fSum:'Сумма',fTerms:'Срок',fStatus:'Статус',fUnpaid:'Вернуть в «ждёт оплаты»', intlHint:'Точный список способов для этой страны подтверждаем при создании счёта. Если способ не подойдёт — подберём другой.', planSubSimple:'Кабинет открывается после оплаты тарифа. Ничего настраивать заранее не нужно — сначала оплата, подключение и настройки уже внутри.', monthly:'Помесячно',yearly:'За год',perYear:'год',eqMonth:'выходит ≈',payYear:'Оплатить за год', freeUsed:'Бесплатный разбор использован. Чтобы разбирать счета дальше, нужен тариф «Разбор входящих».', choosePlan:'Выбрать тариф', show:'Показать',hide:'Скрыть',trkT:'Настройки напоминаний',trkStyle:'Стиль писем',styleLadder:'Нарастающие (по просрочке)',styleLoy:'Только вежливые',styleNeu:'Деловые',styleFirm:'Формальные', trkPeriod:'Период',per3:'каждые 3 дня',per7:'раз в неделю',perStage:'по стадиям',trkChannel:'Канал',perMail:'Google-почта',perTg:'Telegram-отчёт', showLetter:'Показать письмо',letterT:'Пример письма',letterNote:'Демо-текст. В продукте подставляются имя, номер, сумма и срок из вашего счёта.', addT:'Добавить в слежение',addP:'Договор, номер счёта, сумма и срок — дальше напоминаем сами.',addBtn:'Добавить счёт',addOpen:'Добавить счёт в слежение', fClientN:'Клиент',fMailN:'Почта клиента',fNoN:'Номер счёта',fSumN:'Сумма',fDueN:'Оплатить до',fAgree:'О чём договорились',fContract:'Договор или счёт (файл)', agreePh:'напр. сайт + логотип, 1 этап',reqT:'Или оплата по вашим реквизитам', repT:'Отчётность',repP:'Раз в неделю присылаем отчёт: кто оплатил, кто должен и сколько.',repBtn:'Отправить отчёт сейчас',repOn:'Отчёт раз в неделю: включён',repOff:'Отчёт раз в неделю: выключен',repSent:'Демо: отчёт не отправлен. В продукте уходит вам на почту по расписанию.', repLine:'Отчёт за неделю',repRows:'счетов',repWait:'ждут',repOver:'просрочено',repRecv:'взыскано', reqOn:'Оплата по реквизитам (без ссылки)',reqOff:'Вернуться к платёжной ссылке',reqP:'Клиент оплатит напрямую вам: приложите свои реквизиты, мы просто передадим их вместе со счётом. Деньги идут мимо нас.', reqBank:'Банк',reqBik:'БИК',reqAcc:'Расчётный счёт',reqCard:'Карта',reqFile:'Приложить файл с реквизитами',reqBad:'Заполните реквизиты или приложите файл', reqDoneT:'Счёт с вашими реквизитами готов',reqDoneP:'Отправьте клиенту: платёжная ссылка здесь не нужна.',reqCopy:'Скопировать реквизиты', ladderSteps:['Дружеское — за 3 дня до срока','Деловое — в день срока','Настойчивое — через 5 дней после срока','Деловое — через 10 дней после срока','Формальное — через 18 дней после срока'], ladderT:'Стиль напоминаний',ladderP:'Чем больше просрочка, тем жёстче письмо. Пять шагов — от дружеского до требования.', searchCountry:'Начните вводить страну — список сузится', schDay:'День',schOn:'Шаг включён / выключен',schAdd:'Добавить шаг',schDel:'Удалить шаг', schedSum:'Активных шагов: {n}. Чем больше просрочка, тем жёстче тон.',escAuto:'Эскалировать автоматически',remindNow:'Напомнить сейчас', schedNote:'Демо: напоминания не отправляются. В продукте шаги уходят по расписанию от вашего имени.', kpiRecv:'Взыскано',kpiWork:'В работе',kpiWait:'Ждёт оплаты',kpiNext:'Ближайшее напоминание', chooseAction:'Выбор действия',actPause:'Пауза',odDays:'просрочено {n} дн.',nextRem:'след. напоминание {d}',noPlan:'напоминания не идут',inDays:'через 2 дня', demoSend:'демо: письмо не отправлено. В продукте уходит от вашего имени.', histT:'История отправок',histNote:'Демо-история. В продукте — реальные стадии догона с фактами открытия.', paidAsk:'Оплатил?',paidYes:'Да, оплачен',paidPart:'Частично',paidNo:'Ещё нет, назад', searchPh:'Поиск: клиент, номер, сумма',protoNote:'Прототип: данные живут в браузере, база ещё не подключена — разметка таблицы пойдёт в продукт.', thClient:'Клиент',thItem:'Позиция',thSum:'Сумма',thStatus:'Статус',thWho:'От кого',thInvoice:'Счёт',thVendor:'Поставщик', politeTag:'Вежливые напоминания — доброжелательно, без давления',dupTag:'Дубликат',markChecked:'Проверен', demoRemind:'Демо: письмо не отправлено. В продукте напоминание уходит от вашего имени.',showInTable:'Показать в таблице',hideRow:'Скрыть строку', lavaTitle:'Подключение приёма оплаты', lavaSubShort:'Приём денег идёт через Lava.top. Пока не подключите — ссылку на оплату выдать нельзя.', lavaSub:'Шесть шагов. Деньги принимает и держит на вашем балансе Lava.top — мы их не касаемся. Пока ключ не передан, ссылка на оплату недоступна.', lavaNote:'Мы отправляем вас в Lava.top по нашей пригласительной ссылке. Для вас условия и комиссия Lava.top не меняются — мы получаем небольшой процент от сервиса.', lavaInstr:'Как создать продукт в Lava.top', lavaInstrP:'Нужен ровно один продукт, у которого включён флаг «цена по запросу через API» (Price on request via API). Тогда мы сможем выставлять счета на любую сумму. Пока флага нет — сумма в счёте фиксированная.', lavaKeyLbl:'API-ключ из Lava.top', lavaKeyPh:'вставьте ключ целиком', lavaKeyBtn:'Передать ключ', lavaKeyOk:'Ключ принят. Ссылки на оплату открыты — можно выставлять счета.', lavaKeyErr:'Похоже, это не ключ. Скопируйте значение целиком из раздела Public API.', lavaHonest2:'Честно про сроки: верификация (KYC) занимает до 7 рабочих дней, но счета работают и без неё. Вывод денег — после 3 принятых платежей, плюс до 7 дней проверки.', refHowT:'Как это работает', refHow1:'Делитесь ссылкой с теми, кто выставляет счета.', refHow2:'Человек оформляет любой тариф.', refHow3:'Вам капает 20% от его подписки каждый месяц, пока он платит.', refHonest:'Выплаты делаем вручную, помесячно. Процент считается только от фактически оплаченных месяцев — без возвратов и чарджбэков.', lavaSteps:[ {t:'Прочитайте инструкцию ниже',d:'Пять минут: что подготовить и какие галочки поставить в Lava.top.',btn:'Понятно, дальше'}, {t:'Перейдите в Lava.top по нашей ссылке',d:'Откроется сайт Lava.top. Регистрация: email → код из письма.',btn:'Открыть Lava.top →'}, {t:'Зарегистрируйтесь и включите 2FA',d:'Минимальный профиль и двухфакторная защита в настройках.',btn:'Зарегистрировался'}, {t:'Вернитесь и создайте продукт',d:'Один продукт с флагом «цена по запросу через API».',btn:'Создал продукт'}, {t:'Получите API-ключ',d:'В Lava.top: Интеграции → Public API → создать ключ.',btn:'Ключ получен'}, {t:'Передайте ключ нам',d:'Вставьте ключ в поле ниже — после этого ссылки на оплату откроются.',btn:''} ]}, en:{srcMail:'email',srcFile:'file',dupPairN:'Duplicate {n}',delRow:'Delete row',dictate:'Dictate',parseCardT:'Invoice parsing',ourHdrT:'Our details and accountant',hdrFirma:'Our company / name',hdrAddr:'Address',hdrTax:'Tax number',hdrUst:'VAT ID',hdrMail:'E-mail',hdrBerater:'Advisor number',hdrMandant:'Client number at advisor',hdrNote:'These fields are not printed on invoices — the person fills them in. They go to the accountant export.',planWaitT:'Plan selected — awaiting payment',planWaitP:'The workspace opens after payment. Payment is still in test mode — press “Pay (demo)” to continue.',planPayDemo:'Pay (demo)',planChange:'Change plan',planFreeParse:'Parse 1 document free',actVoid:'Void',voided:'Voided',delDraft:'Delete draft',draftKept:'Draft saved on this device',demoLink:'sample — link is not live',delAsk:'Delete? This cannot be undone.',delYes:'Delete',dupMark:'duplicate',newInv:'Create invoice',statsEmpty:'Nothing in this group yet.', fClient:'Client',fItem:'Item',fSum:'Amount',fTerms:'Terms',fStatus:'Status',fUnpaid:'Back to “awaiting payment”', intlHint:'The exact method list for this country is confirmed when the invoice is created. If a method does not fit, we will find another.', planSubSimple:'The app opens after you pay for a plan. Nothing to set up beforehand — payment first, connecting and settings come inside.', monthly:'Monthly',yearly:'Per year',perYear:'year',eqMonth:'that is ≈',payYear:'Pay for a year', freeUsed:'Your free parse is used up. To keep parsing invoices you need the “Incoming parsing” plan.', choosePlan:'Choose a plan', show:'Show',hide:'Hide',trkT:'Reminder settings',trkStyle:'Letter style',styleLadder:'Escalating (by overdue)',styleLoy:'Friendly only',styleNeu:'Businesslike',styleFirm:'Formal', trkPeriod:'Period',per3:'every 3 days',per7:'once a week',perStage:'by stages',trkChannel:'Channel',perMail:'Google mail',perTg:'Telegram digest', showLetter:'Show the letter',letterT:'Sample letter',letterNote:'Demo text. In the product the name, number, amount and due date come from your invoice.', addT:'Add to tracking',addP:'Contract, invoice number, amount and due date — we remind after that.',addBtn:'Add invoice',addOpen:'Add an invoice to tracking', fClientN:'Client',fMailN:'Client email',fNoN:'Invoice number',fSumN:'Amount',fDueN:'Pay by',fAgree:'What was agreed',fContract:'Contract or invoice (file)', agreePh:'e.g. website + logo, stage 1',reqT:'Or payment to your own bank details', repT:'Reporting',repP:'Once a week we send a report: who paid, who owes and how much.',repBtn:'Send the report now',repOn:'Weekly report: on',repOff:'Weekly report: off',repSent:'Demo: no report is sent. In the product it reaches your inbox on schedule.', repLine:'This week',repRows:'invoices',repWait:'awaiting',repOver:'overdue',repRecv:'recovered', reqOn:'Pay to my bank details (no link)',reqOff:'Back to the payment link',reqP:'The client pays you directly: attach your details, we simply pass them on with the invoice. The money never touches us.', reqBank:'Bank',reqBik:'SWIFT / sort code',reqAcc:'Account number',reqCard:'Card',reqFile:'Attach a file with the details',reqBad:'Fill in the details or attach a file', reqDoneT:'Invoice with your bank details is ready',reqDoneP:'Send it to the client: no payment link is needed here.',reqCopy:'Copy the details', ladderSteps:['Friendly — 3 days before due','Businesslike — on the due date','Firm nudge — 5 days late','Businesslike — 10 days late','Formal notice — 18 days late'], ladderT:'Reminder style',ladderP:'The longer it is overdue, the firmer the letter. Five steps — from friendly to a formal demand.', searchCountry:'Start typing a country — the list narrows', schDay:'Day',schOn:'Step on / off',schAdd:'Add a step',schDel:'Delete step', schedSum:'Active steps: {n}. The longer overdue, the firmer the tone.',escAuto:'Escalate automatically',remindNow:'Remind now', schedNote:'Demo: nothing is sent. In the product the steps go out on schedule in your name.', kpiRecv:'Recovered',kpiWork:'Active',kpiWait:'Awaiting',kpiNext:'Next reminder', chooseAction:'Choose action',actPause:'Pause',odDays:'{n} days overdue',nextRem:'next reminder {d}',noPlan:'no reminders running',inDays:'in 2 days', demoSend:'demo: no email is sent. In the product it goes out in your name.', histT:'Send history',histNote:'Demo history. In the product these are the real chase stages with open facts.', paidAsk:'Paid?',paidYes:'Yes, paid',paidPart:'Partially',paidNo:'Not yet, back', searchPh:'Search: client, number, amount',protoNote:'Prototype: data lives in the browser, there is no database yet — the table layout is what ships.', thClient:'Client',thItem:'Item',thSum:'Amount',thStatus:'Status',thWho:'From',thInvoice:'Invoice',thVendor:'Vendor', politeTag:'Polite reminders — friendly, no pressure',dupTag:'Duplicate',markChecked:'Checked', demoRemind:'Demo: no email is sent. In the product the reminder goes out in your name.',showInTable:'Show in the table',hideRow:'Hide row', lavaTitle:'Connect payment acceptance', lavaSubShort:'Money comes in through Lava.top. Until you connect it, no payment link can be issued.', lavaSub:'Six steps. Lava.top accepts the money and keeps it on your own balance — we never touch it. Until the key is handed over, the payment link stays locked.', lavaNote:'We send you to Lava.top through our invitation link. Your terms and the Lava.top fee stay the same — we receive a small share from the service.', lavaInstr:'How to create the product in Lava.top', lavaInstrP:'You need exactly one product with the flag “Price on request via API” enabled. Then we can issue invoices for any amount. Without the flag the amount is fixed.', lavaKeyLbl:'API key from Lava.top', lavaKeyPh:'paste the full key', lavaKeyBtn:'Hand over the key', lavaKeyOk:'Key accepted. Payment links are unlocked — you can issue invoices.', lavaKeyErr:'That does not look like a key. Copy the whole value from the Public API section.', lavaHonest2:'Honest about timing: verification (KYC) takes up to 7 working days, but invoicing works without it. Withdrawal starts after 3 accepted payments, plus up to 7 days of checks.', refHowT:'How it works', refHow1:'Share the link with people who invoice.', refHow2:'They pick any plan.', refHow3:'You get 20% of their subscription every month they keep paying.', refHonest:'We pay out manually, monthly. The share is calculated only on months actually paid — no refunds or chargebacks.', lavaSteps:[ {t:'Read the instructions below',d:'Five minutes: what to prepare and which boxes to tick in Lava.top.',btn:'Got it, next'}, {t:'Open Lava.top via our link',d:'The Lava.top site opens. Sign-up: email → code from the email.',btn:'Open Lava.top →'}, {t:'Sign up and turn on 2FA',d:'A minimal profile and two-factor protection in settings.',btn:'Signed up'}, {t:'Come back and create the product',d:'One product with “price on request via API” enabled.',btn:'Product created'}, {t:'Get the API key',d:'In Lava.top: Integrations → Public API → create a key.',btn:'Key obtained'}, {t:'Hand the key over to us',d:'Paste the key below — payment links unlock right after.',btn:''} ]}, de:{srcMail:'E-Mail',srcFile:'Datei',dupPairN:'Dublette {n}',delRow:'Zeile löschen',dictate:'Diktieren',parseCardT:'Rechnung einlesen',ourHdrT:'Unsere Angaben und Buchhaltung',hdrFirma:'Firma / Name',hdrAddr:'Anschrift',hdrTax:'Steuernummer',hdrUst:'USt-IdNr.',hdrMail:'E-Mail',hdrBerater:'Beraternummer',hdrMandant:'Mandantennummer',hdrNote:'Diese Felder stehen nicht auf der Rechnung — sie werden von Hand ergänzt. Sie gehen in den Export für die Buchhaltung.',planWaitT:'Tarif gewählt — wartet auf Zahlung',planWaitP:'Der Bereich öffnet nach der Zahlung. Die Zahlung läuft noch im Testmodus — „Bezahlen (Demo)“ drücken.',planPayDemo:'Bezahlen (Demo)',planChange:'Tarif wechseln',planFreeParse:'1 Dokument kostenlos einlesen',actVoid:'Stornieren',voided:'Storniert',delDraft:'Entwurf löschen',draftKept:'Entwurf auf diesem Gerät gespeichert',demoLink:'Beispiel — Link ist nicht aktiv',delAsk:'Löschen? Das lässt sich nicht rückgängig machen.',delYes:'Löschen',dupMark:'Dublette',newInv:'Rechnung erstellen',statsEmpty:'In dieser Gruppe ist noch nichts.', fClient:'Kunde',fItem:'Leistung',fSum:'Betrag',fTerms:'Zahlungsziel',fStatus:'Status',fUnpaid:'Zurück auf „offen“', intlHint:'Die genaue Liste der Zahlungsarten für dieses Land bestätigen wir beim Erstellen der Rechnung. Passt eine nicht, finden wir eine andere.', planSubSimple:'Die App öffnet sich nach der Zahlung des Tarifs. Vorher ist nichts einzurichten — erst zahlen, Verbinden und Einstellungen kommen danach.', monthly:'Monatlich',yearly:'Jährlich',perYear:'Jahr',eqMonth:'entspricht ≈',payYear:'Für ein Jahr zahlen', freeUsed:'Der kostenlose Vorgang ist verbraucht. Zum Weiterlesen brauchen Sie den Tarif „Eingang einlesen“.', choosePlan:'Tarif wählen', show:'Anzeigen',hide:'Ausblenden',trkT:'Erinnerungseinstellungen',trkStyle:'Briefstil',styleLadder:'Eskalierend (nach Verzug)',styleLoy:'Nur freundlich',styleNeu:'Sachlich',styleFirm:'Förmlich', trkPeriod:'Zeitraum',per3:'alle 3 Tage',per7:'einmal pro Woche',perStage:'nach Stufen',trkChannel:'Kanal',perMail:'Google-Mail',perTg:'Telegram-Bericht', showLetter:'Brief anzeigen',letterT:'Beispielbrief',letterNote:'Demo-Text. Im Produkt kommen Name, Nummer, Betrag und Frist aus Ihrer Rechnung.', addT:'Zum Verfolgen hinzufügen',addP:'Vertrag, Rechnungsnummer, Betrag und Frist — danach erinnern wir.',addBtn:'Rechnung hinzufügen',addOpen:'Rechnung zum Verfolgen hinzufügen', fClientN:'Kunde',fMailN:'E-Mail des Kunden',fNoN:'Rechnungsnummer',fSumN:'Betrag',fDueN:'Zahlen bis',fAgree:'Was vereinbart wurde',fContract:'Vertrag oder Rechnung (Datei)', agreePh:'z. B. Website + Logo, Stufe 1',reqT:'Oder Zahlung auf Ihre eigenen Bankdaten', repT:'Berichtswesen',repP:'Einmal pro Woche ein Bericht: wer bezahlt hat, wer schuldet und wie viel.',repBtn:'Bericht jetzt senden',repOn:'Wochenbericht: an',repOff:'Wochenbericht: aus',repSent:'Demo: Es wird kein Bericht gesendet. Im Produkt kommt er planmäßig per Mail.', repLine:'Diese Woche',repRows:'Rechnungen',repWait:'offen',repOver:'überfällig',repRecv:'eingezogen', reqOn:'Zahlung auf meine Bankdaten (ohne Link)',reqOff:'Zurück zum Zahlungslink',reqP:'Der Kunde zahlt direkt an Sie: Bankdaten anhängen, wir geben sie nur mit der Rechnung weiter. Das Geld berührt uns nicht.', reqBank:'Bank',reqBik:'BIC',reqAcc:'Kontonummer',reqCard:'Karte',reqFile:'Datei mit Bankdaten anhängen',reqBad:'Bankdaten ausfüllen oder Datei anhängen', reqDoneT:'Rechnung mit Ihren Bankdaten ist fertig',reqDoneP:'An den Kunden senden: ein Zahlungslink ist hier nicht nötig.',reqCopy:'Bankdaten kopieren', ladderSteps:['Freundlich — 3 Tage vor Frist','Sachlich — am Fälligkeitstag','Bestimmter Hinweis — 5 Tage danach','Sachlich — 10 Tage danach','Förmliche Aufforderung — 18 Tage danach'], ladderT:'Erinnerungsstil',ladderP:'Je länger überfällig, desto bestimmter der Brief. Fünf Stufen — von freundlich bis Aufforderung.', searchCountry:'Land eintippen — die Liste wird kürzer', schDay:'Tag',schOn:'Schritt an / aus',schAdd:'Schritt hinzufügen',schDel:'Schritt löschen', schedSum:'Aktive Schritte: {n}. Je länger überfällig, desto bestimmter der Ton.',escAuto:'Automatisch eskalieren',remindNow:'Jetzt erinnern', schedNote:'Demo: Es wird nichts versendet. Im Produkt laufen die Schritte planmäßig in Ihrem Namen.', kpiRecv:'Eingezogen',kpiWork:'Aktiv',kpiWait:'Offen',kpiNext:'Nächste Erinnerung', chooseAction:'Aktion wählen',actPause:'Pause',odDays:'{n} Tage überfällig',nextRem:'nächste Erinnerung {d}',noPlan:'keine Erinnerungen aktiv',inDays:'in 2 Tagen', demoSend:'Demo: Es wird keine E-Mail gesendet. Im Produkt geht sie in Ihrem Namen raus.', histT:'Versandhistorie',histNote:'Demo-Historie. Im Produkt sind das die echten Mahnstufen mit Öffnungsdaten.', paidAsk:'Bezahlt?',paidYes:'Ja, bezahlt',paidPart:'Teilweise',paidNo:'Noch nicht, zurück', searchPh:'Suche: Kunde, Nummer, Betrag',protoNote:'Prototyp: Daten liegen im Browser, eine Datenbank gibt es noch nicht — das Tabellenlayout ist das Produktlayout.', thClient:'Kunde',thItem:'Leistung',thSum:'Betrag',thStatus:'Status',thWho:'Von',thInvoice:'Rechnung',thVendor:'Lieferant', politeTag:'Höfliche Erinnerungen — freundlich, ohne Druck',dupTag:'Dublette',markChecked:'Geprüft', demoRemind:'Demo: Es wird keine E-Mail gesendet. Im Produkt geht die Erinnerung in Ihrem Namen raus.',showInTable:'In der Tabelle zeigen',hideRow:'Zeile ausblenden', lavaTitle:'Zahlungsannahme verbinden', lavaSubShort:'Das Geld läuft über Lava.top. Ohne Verbindung kann kein Zahlungslink ausgegeben werden.', lavaSub:'Sechs Schritte. Lava.top nimmt das Geld an und führt es auf Ihrem eigenen Guthaben — wir berühren es nicht. Bis der Schlüssel übergeben ist, bleibt der Zahlungslink gesperrt.', lavaNote:'Wir schicken Sie über unseren Einladungslink zu Lava.top. Ihre Konditionen und die Lava.top-Gebühr bleiben gleich — wir erhalten einen kleinen Anteil vom Dienst.', lavaInstr:'So legen Sie das Produkt in Lava.top an', lavaInstrP:'Sie brauchen genau ein Produkt mit aktivem Flag „Preis auf Anfrage via API“. Dann können wir Rechnungen über jeden Betrag ausstellen. Ohne Flag ist der Betrag fest.', lavaKeyLbl:'API-Schlüssel aus Lava.top', lavaKeyPh:'Schlüssel vollständig einfügen', lavaKeyBtn:'Schlüssel übergeben', lavaKeyOk:'Schlüssel angenommen. Zahlungslinks sind frei — Sie können Rechnungen ausstellen.', lavaKeyErr:'Das sieht nicht nach einem Schlüssel aus. Kopieren Sie den ganzen Wert aus dem Bereich Public API.', lavaHonest2:'Ehrlich zu den Fristen: Die Verifizierung (KYC) dauert bis zu 7 Werktage, Rechnungen funktionieren aber auch ohne. Auszahlung ab 3 angenommenen Zahlungen, plus bis zu 7 Tage Prüfung.', refHowT:'So funktioniert es', refHow1:'Link an Menschen weitergeben, die Rechnungen schreiben.', refHow2:'Die Person wählt einen Tarif.', refHow3:'Sie bekommen 20 % vom Abo — jeden Monat, solange gezahlt wird.', refHonest:'Wir zahlen manuell aus, monatlich. Der Anteil wird nur auf tatsächlich bezahlte Monate berechnet — ohne Erstattungen und Chargebacks.', lavaSteps:[ {t:'Lesen Sie die Anleitung unten',d:'Fünf Minuten: was vorzubereiten ist und welche Häkchen zu setzen sind.',btn:'Verstanden, weiter'}, {t:'Über unseren Link zu Lava.top',d:'Die Lava.top-Seite öffnet sich. Anmeldung: E-Mail → Code aus der E-Mail.',btn:'Lava.top öffnen →'}, {t:'Registrieren und 2FA einschalten',d:'Minimales Profil und Zwei-Faktor-Schutz in den Einstellungen.',btn:'Registriert'}, {t:'Zurückkommen und Produkt anlegen',d:'Ein Produkt mit „Preis auf Anfrage via API“.',btn:'Produkt angelegt'}, {t:'API-Schlüssel holen',d:'In Lava.top: Integrationen → Public API → Schlüssel erstellen.',btn:'Schlüssel erhalten'}, {t:'Schlüssel an uns übergeben',d:'Schlüssel unten einfügen — danach sind Zahlungslinks frei.',btn:''} ]} }; /* ================= КАБИНЕТ: ФИЛЬТРЫ И КАРТОЧКИ ================= */ let invFilter='all'; function setInvFilter(f){invFilter=f;if(appStep==='cabinet')cabTab(currentCabTab)} function newInvoice(){gotoInvoice()} function openCard(ev,i){ const tg=ev&&ev.target; if(tg&&tg.closest&&tg.closest('button'))return; invoiceDetail(i); } /* ================= ИСТОРИЯ ЭКРАНОВ ================= */ /* Задача: «назад» (стрелка в приложении И кнопка браузера) возвращает на предыдущий экран, а не выбрасывает на лендинг. Стек экранов живёт в памяти, каждый экран кладёт запись в историю браузера. */ let NAV_STACK=[], _navSilent=false; function navPush(d){ if(_navSilent)return; const top=NAV_STACK[NAV_STACK.length-1]; if(top&&top.s===d.s&&top.tab===d.tab&&top.i===d.i&&top.need===d.need)return; NAV_STACK.push(d); ymHit('screen:'+d.s); try{history.pushState({d:NAV_STACK.length},'',location.pathname+location.search+(d.s==='post'?('#article-'+lang+'-'+d.i):('#'+d.s)))}catch(e){} } function navRender(d){ _navSilent=true; try{ show('app'); if(d.s==='auth')gotoAuth(); else if(d.s==='check')gotoCheck(); else if(d.s==='name')gotoName(); else if(d.s==='plans')gotoPlans(); else if(d.s==='lava')gotoLava(); else if(d.s==='post')openPost(d.i); else if(d.s==='locked')lockScreen(d.need); else if(d.s==='invoice')gotoInvoice(d.pre); else if(d.s==='done')gotoDone(); else if(d.s==='detail')invoiceDetail(d.i); else if(d.s==='ref')gotoRef(d.linked); else if(d.s==='cabinet'){currentCabTab=(d.tab==null?0:d.tab);gotoCabinet()} }finally{_navSilent=false} } function navWrap(name,make){ const f=window[name]; if(typeof f!=='function')return; window[name]=function(){ const r=f.apply(this,arguments); const d=make.apply(null,arguments); if(d&&appStep===d.step)navPush(d); /* если функция увела на другой экран — не пушим */ return r; }; } navWrap('gotoAuth',()=>({s:'auth',step:'auth'})); navWrap('gotoCheck',()=>({s:'check',step:'check'})); navWrap('gotoName',()=>({s:'name',step:'name'})); navWrap('gotoPlans',()=>({s:'plans',step:'plans'})); navWrap('gotoLava',()=>({s:'lava',step:'lava'})); navWrap('openPost',i=>({s:'post',step:'post',i:i})); navWrap('lockScreen',n=>({s:'locked',step:'locked',need:n})); navWrap('gotoInvoice',p=>({s:'invoice',step:'invoice',pre:p})); navWrap('gotoDone',()=>({s:'done',step:'done'})); navWrap('invoiceDetail',i=>({s:'detail',step:'detail',i:i})); navWrap('gotoRef',l=>({s:'ref',step:'ref',linked:l})); navWrap('gotoCabinet',()=>({s:'cabinet',step:'cabinet',tab:currentCabTab})); /* широкая колонка — только у таблиц; на формах и карточках возвращаем узкую */ function setWide(on){try{document.body.classList.toggle('app-wide',!!on)}catch(e){}} ['gotoAuth','gotoCheck','gotoName','gotoInvoice','gotoDone','invoiceDetail','gotoPlans','lockScreen','gotoLava','gotoRef','showLetterFor','showLetterStage','openPost'].forEach(function(n){ const f=window[n]; if(typeof f==='function')window[n]=function(){setWide(false);return f.apply(this,arguments)}; }); appBack=function(){ if(NAV_STACK.length>1){try{history.back();return}catch(e){}} NAV_STACK=[];show('landing'); try{history.replaceState(null,'',location.pathname+location.search)}catch(e){} }; /* после входа стек обрезаем: «назад» из кабинета ведёт на сайт, а не на экран входа */ const _afterName=afterName; afterName=function(){ _afterName.apply(this,arguments); NAV_STACK=[{s:appStep,step:appStep,tab:currentCabTab}]; }; window.addEventListener('popstate',function(){ if(NAV_STACK.length>1){NAV_STACK.pop();navRender(NAV_STACK[NAV_STACK.length-1])} else{NAV_STACK=[];show('landing');try{history.replaceState(null,'',location.pathname+location.search)}catch(e){}} }); /* «Статьи» из кабинета */ function openBlog(){ show('landing'); try{setLang(lang);renderBlog()}catch(e){} setTimeout(function(){const el=document.getElementById('blog');if(el&&el.scrollIntoView)el.scrollIntoView({behavior:'smooth'})},150); } /* демо-сброс оплаты: чтобы можно было заново прокликать шлюз оплаты */ function resetDemo(){ askDelete(lavaT('delAsk'),function(){ plan=null;lavaStep=0; try{localStorage.removeItem('db_plan::'+stateKey());localStorage.removeItem('db_lava::'+stateKey())}catch(e){} NAV_STACK=[];show('app');gotoPlans(); }); } /* ================= СТАРТ ================= */ document.addEventListener('DOMContentLoaded',function(){ document.querySelectorAll('[data-was]').forEach(el=>{el.dataset.wasLabel=el.textContent.trim()}); setCountry('ru'); try{draftLoad()}catch(e){} try{hdrLoad()}catch(e){} /* прямая ссылка на статью: #article-ru-4 — так их потом линкуем с основного сайта */ var m=String(location.hash||'').match(/^#article-(ru|en|de)-(\d+)$/); if(m){try{lang=m[1];setLang(lang);openPost(+m[2])}catch(e){NAV_STACK=[];show('landing')}} });