🧪 killBottleneck je ve veřejné betě — cloud i self-host.🧪 killBottleneck je v betě.Beta na GitHubu →
Skip to content

Napojení na Google Sheets

Máte evidenci v Google Sheets — třeba reklamace — a chcete, aby se z každého nového řádku v killBottlenecku sám rozjel celý postup řešení? Tahle stránka je návod krok za krokem. Programovat umět nemusíte: kód je hotový, vyplníte v něm pár řádků.

Jako příklad tu jede reklamace řešená metodou 8D, ale stejně se to dá použít na cokoli jiného — objednávky, nábor, servisní zásahy. Mění se jen názvy.

Co přesně se stane

  1. Do tabulky přibude řádek s novou reklamací.
  2. Do mapy se založí cíl „Reklamace 0001 — Auto Novák s.r.o.".
  3. killBottleneck mu sám rozbalí osm disciplín D1–D8 a dá vědět, komu to spadlo.
  4. Jak lidé disciplíny odškrtávají, píše se zpátky do tabulky „D4 – Kořenová příčina (3/8)".

Používáte n8n? Jděte rovnou jinam

Tahle stránka vede tabulku přes Google Apps Script, který je součástí Sheets a nic dalšího nepotřebuje. Máte-li n8n, je to pro vás jednodušší i mocnější cesta — kroky 1–3 níže jsou společné a pak pokračujte v Napojení přes n8n.

Než začnete

Potřebujete tři věci a zhruba půl hodiny:

  • účet v killBottlenecku — funguje to úplně stejně v cloudu i na vlastním serveru,
  • tabulku v Google Sheets, kde má první řádek názvy sloupců,
  • deset minut na klikání a deset na vložení hotového kódu.

Vlastní server ve firemní síti? Přečtěte si tohle první

Skripty Google Apps Script neběží u vás v kanceláři, ale na serverech Googlu. Aby Google mohl do killBottlenecku zapsat, musí na něj z internetu dosáhnout.

  • Cloudová instance (vasefirma.killbottleneck.com) — dosáhne, nic neřešíte.
  • Vlastní server na veřejné adrese — dosáhne, jen ať má HTTPS.
  • Vlastní server jen ve firemní sítinedosáhne. Buď ho vystavíte na veřejnou adresu, nebo jděte cestou přes n8n a rozběhněte ho ve stejné síti. Opačný směr — killBottleneck píšící ven do tabulky — funguje odkudkoli.

1. Připravte si mapu: stromeček, nebo nástěnka?

Nejdřív se rozhodněte, jak má práce v mapě vypadat. Obojí umí totéž, liší se pohledem — a od téhle volby se pak liší dva řádky nastavení v receptu, nic víc.

Varianta A — stromeček (výchozí)

Každá reklamace má svých osm disciplín pod sebou.

Stromeček: řádek z tabulky se stal cílem a osm disciplín 8D se mu rozbalilo samo.

Založte nový projekt (třeba Reklamace 2026) a pod hlavní cíl dejte jeden cíl, kterému budete říkat třeba Nové reklamace. Právě pod něj se budou nové řádky věšet.

Varianta B — kanbanová nástěnka

Osm sloupců vedle sebe a reklamace je karta, která jimi putuje.

Nástěnka: Reklamace 0001 už je ve třetím sloupci, 0002 ve druhém. Karta se po „Hotovo“ přesune sama.

Založíte ji na dvě kliknutí: Nový projekt → Ze šablony → 8D report — kanban.

Kdy sáhnout po které
StromečekChcete u každé reklamace vidět všech osm kroků najednou, komu patří a kdy mají být hotové.
NástěnkaChcete na jeden pohled vidět, kolik případů visí na které disciplíně.

Ať už zvolíte cokoli, id mapy najdete v adrese, když ji máte otevřenou: …/map/y2cmuebk7guaev1. Zapište si ho, budete ho potřebovat.

2. Ať se postup rozjede sám

Nástěnka — nemusíte dělat nic

Šablona 8D report — kanban přináší sloupce i sedm pravidel posunu. Každé říká: karta pod Dx přepnuta na Hotovo → přesuň ji pod Dx+1 → vrať ji na Založeno. Hotovo, pokračujte krokem 3.

U stromečku si jedno pravidlo přidáte:

  1. V mapě klikněte na blesk → Pravidla (na užší obrazovce je v nabídce ⋮).
  2. Nové pravidlo a nastavte:
    • Když: Uzel založen
    • Pokud: nadřazený cíl je Nové reklamace
    • Udělej: Založ podcíle a vypište osm disciplín D1 až D8

Tu podmínku „pokud" nevynechávejte

Bez ní se osm kroků rozbalí úplně každému novému cíli v mapě — tedy i těm osmi, které pravidlo právě založilo. Podmínka na nadřazený cíl je to, co tomu brání.

Chcete rovnou i přiřazení a upozornění? Přidejte akce Přiřaď zodpovědnou osobu a Pošli upozornění. Víc v Automatizačních pravidlech.

3. Vytvořte API klíč

Klíč je heslo, kterým se tabulka killBottlenecku představí.

  1. Vpravo nahoře uživatelské menu → API klíče.
  2. Popis třeba Google Sheets, Oprávnění: Čtení i zápis.
  3. Vytvořit a klíč si hned zkopírujte — ukáže se jen jednou. Když ho ztratíte, nedá se zpětně zjistit, jen zrušit a vydat nový.
Dialog API klíče. Pro zápis z tabulky musí být oprávnění „Čtení i zápis“.

Klíč platí za vás a jen na vaše mapy. Přes API se nedá dělat správcovská práce, ani když se ztratí — podrobnosti v REST API.

4. Bonus: práce, která se vrací sama

Reklamace přijde, vyřeší se a je hotová. Spousta práce se ale vrací v rytmu — a nemá smysl ji pokaždé zakládat znovu. Od verze 0.35 to umí cíl sám: v Zadání cíle je přepínač Opakování (denně / týdně / měsíčně).

Co se stane, když takový cíl odbavíte na Hotovo: sám se vrátí na Založeno a termín se posune o jeden interval. U cíle svítí odznak 🔁.

Typicky se hodí na tohle:

CoRytmusTermín nastavte na
Uzavřít týden a poslat sumář prácetýdněnejbližší pátek
Měsíční závěrkaměsíčněden v měsíci, kdy ji děláte (např. 15.)
Pravidelný auditměsíčněkonec měsíce (31. se v kratších měsících přisune)

Rytmus drží i na zpožděnou práci

Tohle je na tom to hlavní. Kdyby to bylo obyčejné „+7 dní", stačilo by jednou odbavit uzávěrku o tři týdny později a pátky by se rozjely na úterky. Opakování proto počítá od původního termínu a prošlé výskyty přeskočí na nejbližší budoucí.

Ověřeno: uzávěrka s propadlým termínem 31. 7. (pátek) odbavená 17. 8. dostala nový termín 21. 8. — zase pátek, ne 7. 8. Stejně tak 31. leden vyjde na 28. února, ne na 3. březen.

Opakování není žádný nový pojem — je to obyčejné automatizační pravidlo, takže ho uvidíte v Pravidlech mapy, přenese se šablonou a dá se založit i z receptu. Když si chcete pravidelný cíl založit rovnou z tabulky nebo z n8n, pošlete po založení cíle ještě tohle na POST /api/kb/v1/maps/{id}/rules:

json
{
  "name": "Opakování: Uzavřít týden",
  "node_id": "ID_CÍLE",
  "trigger": { "type": "node_status_changed", "status": "done" },
  "actions": [
    { "type": "set_status", "status": "todo" },
    { "type": "set_deadline", "advance": "weekly" }
  ]
}

advance bere daily, weekly nebo monthly. Cíl musí mít termín — bez něj není co posouvat.

Tady končí společná část. Pokračujete-li přes n8n, jděte na Napojení přes n8n. Zbytek téhle stránky je cesta přes Apps Script.

5. Sloupce v tabulce

Připravte si tyhle sloupce (na pořadí nezáleží, na názvech ano):

Číslo reklamaceZákazníkPopis problémukillBottleneckStav 8D
0001Auto Novák s.r.o.Prasklý svar na držáku.(nechte prázdné)(nechte prázdné)

Poslední dva sloupce si vyplňuje skript sám — do killBottleneck si ukládá, který cíl v mapě řádku odpovídá, do Stav 8D píše, jak řešení pokročilo.

6. Vložte skript

V tabulce Rozšíření → Apps Script, smažte, co tam je, a vložte celý kód níže.

Celý kód pro Apps Script (rozbalit a zkopírovat)
javascript
/**
 * killBottleneck ↔ Google Sheets
 * ------------------------------------------------------------------
 * Co to umí:
 *   1) z každého nového řádku v evidenci reklamací založí v mapě uzel
 *      a killBottleneck mu sám rozbalí 8D report,
 *   2) zpátky do tabulky píše, jak řešení pokročilo („D4 – Kořenová příčina (3/8)").
 *
 * Nemusíte umět programovat. Stačí vyplnit nastavení hned pod tímhle textem.
 */

// ⚙️ ------------------- VYPLŇTE NASTAVENÍ -------------------
/** Adresa vaší instance. V cloudu je to to, co máte v prohlížeči, BEZ lomítka na konci. */
const KB_ADRESA = 'https://vasefirma.killbottleneck.com';
/** API klíč. Vytvoříte ho v aplikaci: vaše jméno vpravo nahoře → API klíče → Nový klíč,
 *  a MUSÍ mít oprávnění „čtení i zápis". Ukáže se jen jednou, tak si ho hned zkopírujte. */
const KB_KLIC = 'kb_user_sem_vlozte_svuj_klic';
/** Id mapy. Je vidět v adrese, když máte mapu otevřenou: /mapa/y2cmuebk7guaev1 */
const KB_MAPA = 'sem_vlozte_id_mapy';
/** Pod který cíl se nové reklamace věší.
 *  Klasická mapa: 'Nové reklamace'. Kanbanová nástěnka: první sloupec, tedy 'D1 – Sestavení týmu'. */
const KB_VETEV = 'Nové reklamace';
/** Máte kanbanovou nástěnku (karta se po „Hotovo" stěhuje po sloupcích)? Dejte true. */
const KB_KANBAN = false;
// ⚙️ ----------------------------------------------------------

/**
 * Jak se jmenují sloupce v tabulce. Skript je hledá podle NÁZVU v prvním řádku,
 * takže je můžete mít v libovolném pořadí — jen se musí jmenovat takhle.
 */
const SLOUPCE = {
  cislo: 'Číslo reklamace',
  zakaznik: 'Zákazník',
  popis: 'Popis problému',
  odkaz: 'killBottleneck', // sem si skript sám uloží uzel — needitujte ho
  stav: 'Stav 8D',         // sem si skript sám píše, jak to pokročilo
};

/** Nabídka v tabulce (objeví se po znovuotevření souboru). */
function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('killBottleneck')
    .addItem('Odeslat nové reklamace', 'odeslatNoveReklamace')
    .addItem('Načíst stav řešení', 'nacistStavy')
    .addToUi();
}

/**
 * KROK 1: každý řádek, který ještě nemá vyplněný sloupec „killBottleneck",
 * založí v mapě jako nový cíl. Řádky, které tam už jsou, přeskočí — takže
 * se nic nezdvojí, ani když funkci pustíte desetkrát po sobě.
 */
function odeslatNoveReklamace() {
  const list = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const data = list.getDataRange().getValues();
  const sl = kbNajdiSloupce_(data[0]);

  let zalozeno = 0;
  for (let r = 1; r < data.length; r++) {
    const radek = data[r];
    if (radek[sl.odkaz]) continue;              // už odesláno
    if (!radek[sl.cislo] && !radek[sl.popis]) continue; // prázdný řádek

    const nazev = 'Reklamace ' + radek[sl.cislo] + ' — ' + radek[sl.zakaznik];
    const popis = String(radek[sl.popis] || '');

    const uzelId = kbZalozUzel_(nazev, popis);
    list.getRange(r + 1, sl.odkaz + 1).setValue(uzelId);
    zalozeno++;
  }
  kbHlaska_(zalozeno ? 'Odesláno nových reklamací: ' + zalozeno : 'Nic nového k odeslání.');
}

/**
 * KROK 2: načte mapu a ke každému řádku dopíše, kde řešení stojí.
 * Tohle je ta „zpětná vazba do evidence" — když někdo v mapě splní disciplínu,
 * do tabulky se to propíše.
 */
function nacistStavy() {
  const list = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const data = list.getDataRange().getValues();
  const sl = kbNajdiSloupce_(data[0]);

  const mapa = kbVolani_('GET', '/api/kb/v1/maps/' + KB_MAPA);
  const podleId = kbRejstrik_(mapa.tree);

  let aktualizovano = 0;
  for (let r = 1; r < data.length; r++) {
    const uzelId = data[r][sl.odkaz];
    if (!uzelId) continue;
    const zaznam = podleId[uzelId];
    const text = zaznam ? kbPopisPokroku_(zaznam) : 'v mapě už není';
    if (data[r][sl.stav] !== text) {
      list.getRange(r + 1, sl.stav + 1).setValue(text);
      aktualizovano++;
    }
  }
  kbHlaska_('Aktualizováno řádků: ' + aktualizovano);
}

/**
 * Jedna věta do tabulky. Podle KB_KANBAN se čte jinak:
 *
 *   KLASICKÁ mapa — reklamace má osm disciplín pod sebou, počítá se, kolik je hotových.
 *   KANBANOVÁ    — reklamace je karta bez podcílů, která se po „Hotovo" sama stěhuje
 *                  pod další sloupec; hlásí se sloupec, ve kterém právě stojí.
 */
function kbPopisPokroku_(zaznam) {
  const uzel = zaznam.uzel;

  if (KB_KANBAN) {
    const sloupce = zaznam.radaRodice || [];
    const poradi = sloupce.indexOf(zaznam.rodic) + 1;
    if (!poradi) return 'není pod žádným sloupcem';
    if (poradi === sloupce.length && uzel.status === 'done') return '✅ uzavřeno (' + poradi + '/' + sloupce.length + ')';
    return zaznam.rodic.title + ' (' + poradi + '/' + sloupce.length + ')';
  }

  const kroky = uzel.children || [];
  if (!kroky.length) return uzel.status === 'done' ? 'uzavřeno' : 'čeká na rozbalení 8D';

  const hotovo = kroky.filter(function (k) { return k.status === 'done'; }).length;
  if (hotovo === kroky.length) return '✅ uzavřeno (' + hotovo + '/' + kroky.length + ')';

  const prvniNehotovy = kroky.filter(function (k) { return k.status !== 'done'; })[0];
  return prvniNehotovy.title + ' (' + hotovo + '/' + kroky.length + ')';
}

/**
 * Založí jeden uzel pod cílem z KB_VETEV a vrátí jeho id.
 *
 * Proč to není jedno jediné volání: killBottleneck u každého zápisu chce vědět,
 * jakou verzi mapy jste viděli (`base_updated`). Když někdo mapu mezitím změnil,
 * odpoví 409 a chce, ať si ji načtete znovu. Je to pojistka proti přepsání cizí
 * práce — my ji tady prostě obsloužíme a zkusíme to znovu.
 */
function kbZalozUzel_(nazev, popis) {
  for (let pokus = 1; pokus <= 4; pokus++) {
    const mapa = kbVolani_('GET', '/api/kb/v1/maps/' + KB_MAPA);
    const vetev = kbNajdiPodleNazvu_(mapa.tree, KB_VETEV);
    if (!vetev) throw new Error('V mapě chybí cíl „' + KB_VETEV + '".');

    const odpoved = kbVolani_('POST', '/api/kb/v1/maps/' + KB_MAPA + '/nodes', {
      base_updated: mapa.updated,
      parent_id: vetev.id,
      items: [{ title: nazev, description: popis }],
    }, true);

    if (odpoved.kod === 200) return odpoved.telo.added_ids[0];
    if (odpoved.kod === 409) { Utilities.sleep(700 * pokus); continue; } // mapu někdo změnil
    if (odpoved.kod === 429) { Utilities.sleep(3000 * pokus); continue; } // příliš rychle
    throw new Error('killBottleneck odpověděl ' + odpoved.kod + ': ' + JSON.stringify(odpoved.telo));
  }
  throw new Error('Mapa se pořád mění, zkuste to za chvíli znovu.');
}

/** Jedno volání do killBottlenecku. */
function kbVolani_(metoda, cesta, telo, vratitKod) {
  const odpoved = UrlFetchApp.fetch(KB_ADRESA + cesta, {
    method: metoda.toLowerCase(),
    contentType: 'application/json',
    headers: { Authorization: 'Bearer ' + KB_KLIC },
    payload: telo ? JSON.stringify(telo) : undefined,
    muteHttpExceptions: true,
  });
  const kod = odpoved.getResponseCode();
  let obsah = {};
  try { obsah = JSON.parse(odpoved.getContentText()); } catch (e) {}

  if (vratitKod) return { kod: kod, telo: obsah };
  if (kod === 401) throw new Error('Neplatný API klíč — zkontrolujte KB_KLIC.');
  if (kod === 404) throw new Error('Mapa nenalezena — zkontrolujte KB_MAPA.');
  if (kod !== 200) throw new Error('killBottleneck odpověděl ' + kod + ': ' + odpoved.getContentText());
  return obsah;
}

/** Najde čísla sloupců podle názvů v prvním řádku. */
function kbNajdiSloupce_(hlavicka) {
  const vysledek = {};
  Object.keys(SLOUPCE).forEach(function (klic) {
    const index = hlavicka.indexOf(SLOUPCE[klic]);
    if (index === -1) throw new Error('V tabulce chybí sloupec „' + SLOUPCE[klic] + '".');
    vysledek[klic] = index;
  });
  return vysledek;
}

/**
 * Rejstřík uzlů. U každého si pamatuje rodiče a ŘADU, VE KTERÉ STOJÍ RODIČ —
 * u kanbanové karty je to přesně ta řada sloupců, takže jde spočítat kolikátý je.
 */
function kbRejstrik_(strom) {
  const rejstrik = {};
  const projdi = function (uzly, rodic, radaRodice) {
    (uzly || []).forEach(function (u) {
      rejstrik[u.id] = { uzel: u, rodic: rodic, radaRodice: radaRodice };
      projdi(u.children, u, uzly);
    });
  };
  projdi(strom, null, []);
  return rejstrik;
}

function kbNajdiPodleNazvu_(uzly, nazev) {
  let nalezeny = null;
  kbProjdiStrom_(uzly, function (u) { if (!nalezeny && u.title === nazev) nalezeny = u; });
  return nalezeny;
}

function kbProjdiStrom_(uzly, akce) {
  (uzly || []).forEach(function (u) { akce(u); kbProjdiStrom_(u.children, akce); });
}

/** Hláška uživateli — v tabulce bublina, při automatickém běhu jen do logu. */
function kbHlaska_(text) {
  try { SpreadsheetApp.getActiveSpreadsheet().toast(text, 'killBottleneck', 5); }
  catch (e) { Logger.log(text); }
}

7. Vyplňte nastavení

Nahoře ve skriptu je pět řádků. První tři platí vždycky:

javascript
const KB_ADRESA = 'https://vasefirma.killbottleneck.com';
const KB_KLIC = 'kb_user_sem_vlozte_svuj_klic';
const KB_MAPA = 'sem_vlozte_id_mapy';

Poslední dva se řídí tím, co jste zvolili v kroku 1:

Tvar mapyKB_VETEVKB_KANBAN
Stromeček'Nové reklamace'false
Nástěnka'D1 – Sestavení týmu'true

Uložte (💾), nahoře vyberte funkci odeslatNoveReklamace a dejte Spustit. Google se poprvé zeptá na oprávnění — potvrďte ho. V tabulce se u odeslaných řádků objeví id cíle.

8. Stav řešení zpátky do tabulky

Druhý směr už máte taky — je to funkce nacistStavy z téhož skriptu. Načte mapu a ke každému řádku dopíše, kde práce stojí: u stromečku D2 – Popis problému (1/8), u nástěnky sloupec, ve kterém karta právě stojí.

9. Ať to jezdí samo

Zatím se obojí spouští ručně. Automatické spouštění zapnete v Apps Scriptu vlevo pod ikonou budíku (Triggery / Aktivační události) → Přidat aktivační událost:

FunkceZdrojTypK čemu
odeslatNoveReklamaceZ tabulkyPři změněnový řádek putuje do mapy hned
nacistStavyČasově řízenéPo hodinách / po minutáchstav řešení se vrací do tabulky

Po znovuotevření tabulky navíc přibude nahoře nabídka killBottleneck, odkud jde obojí spustit i ručně.

Když to nefunguje

Co vidíteCo se děje
Neplatný API klíč (401)Klíč je špatně zkopírovaný, zrušený, nebo prošla platnost. Vytvořte nový.
Mapa nenalezena (404)Špatné id mapy — nebo klíč patří někomu, kdo na tu mapu nedosáhne.
403 u zápisuKlíč má oprávnění jen na čtení. Vytvořte nový s Čtení i zápis.
409 pořád dokolaNěkdo mapu zrovna hodně upravuje. Skript to sám opakuje; když to trvá, zkuste to za chvíli.
429Moc zápisů za minutu (strop je 30). Posílejte řádky po dávkách, ne po jednom.
V mapě chybí cíl …KB_VETEV nesedí na název v mapě. U nástěnky to musí být první sloupec.
Cíl vznikl, ale 8D se nerozbaliloPravidlo neexistuje, je vypnuté, nebo podmínka „nadřazený cíl" míří jinam. Zkontrolujte v Pravidla → log běhů.
V tabulce chybí sloupecSkript hledá sloupce podle názvu v prvním řádku. Překlep v hlavičce = chyba.
Nic se nestalo a nikde nic neníAktivační událost se nespustila — zkontrolujte Spuštění (Executions) v levém sloupci Apps Scriptu.

Co čekat a co ne

Aby vás pak nic nepřekvapilo:

  • Termíny jsou po dnech, ne po hodinách. Když máte v předpisu „okamžité opatření do 24 hodin", nejjemnější, co pravidlo umí nastavit, je zítra.
  • Pořadí cílů v mapě není pořadí zakládání. Mapa se po přidání přerovná, proto si recept pamatuje id cíle, ne jeho pozici. Nespoléhejte na „poslední v seznamu".
  • Pravidla platí jen do budoucna. Reklamace, které v mapě už jsou, se zpětně nerozbalí ani nezačnou posouvat.
  • Spuštění pravidel se nepočítá ani neúčtuje, strop je 50 pravidel na mapu.
  • killBottleneck nevyrobí hotový 8D formulář pro zákazníka. Hlídá průběh a termíny; dokument, který pošlete odběrateli, si dál veďte tam, kde ho vedete teď.

Kam dál

fair-code — self-hosting a interní použití zdarma, přeprodej jako hostovaná služba ne.