Tänane treening
Treeni
Teisiti.
Profi harjutused palli kontrolli, jalgade töö ja visete jaoks.
1
Algaja
0 / 100 XP
0
Päeva järjest
0
Harjutust
0h
Sel nädalal
💡 CourtIQ parendus arendajale
Kirjuta üks lühike ülesanne, mida arendajad peaksid tegema. See salvestatakse meie Google Drive'i nimekirja.
0
Päeva järjest
0
Harjutust
0
Sel nädalal
🏀 Palli värv ja AI ▾
Harjutused—
AI analüüsVali harjutus
Vali harjutus loendist — AI statistika kogub valitud harjutuse kohta.
🏀 Pall: Auto
See nädal
Sinu treeningud
Harjutuste kogu
—
Mängijad
Vali mängija kannete ja arengu vaatamiseks
Harjutuste haldus
Loo ja halda meeskonna harjutusi
💡 Siin lood harjutusi mis on kohe nähtavad kõigile mängijatele. Aktiveeri/deaktiveeri nupp peidab/näitab harjutuse.
🏆 Edetabel & saavutused
Mängijate võrdlus ja statistika
🤖 Nädala AI liidrid · viimased 7 päeva
🏅 Saavutuste ülevaade
Treeneri vaade — kõik võimalikud saavutused koos tingimustega. Mängijad näevad ainult avatud märgid.
0 / 0 avatud
Mängijate edetabel
Oskuste areng
Palli kontroll0%
Jalgade töö0%
Viske tehnika0%
Kaitse0%
Viimased 7 päeva
📋 Treeningkava
Kava koostatakse automaatselt nõrgima oskuse põhjal — iga kord kui logitakse uus treening, kava uueneb.
📈 Areng ajas
🎯 Eesmärgid
Treeningud
Planeeritud treeningud ja osalemine
📊 AI statistika
Video AI kogub põrgatusi, viskeid ja tabamusi treeningu ajal
🔴 Reaalajas — —
0
Põrgatused
0/0
Korvid / visked
—
AI skoor
—
Rütm
🏆 Nädala AI saavutused · MVP, snaiper, targeti jälgimine
Viimased 7 päeva
Seaded
Meeskonna sünkroonimine ja rakenduse haldus
Sinu profiil ja rakenduse seaded
🏀 AI kaamera — palli värv
Vali, millist palli AI jälgib. Sama seade on ka avalehel nupu Alusta treeningut all.
Google Drive / Sheets sünkroonimine
Kõik andmed salvestatakse automaatselt brauseri
Google Sheets sünkroonib üheksa eraldi lehte:
Mitme seadme jaoks: kasuta kõigil telefonidel/tablettidel sama Apps Script URL-i. Rakendus loeb Drive'ist uuendusi automaatselt (~30 s) ja saadab sinu muudatused sinna — nii näevad treener ja mängijad samu andmeid.
localStorage'sse.Google Sheets sünkroonib üheksa eraldi lehte:
Kanded, Mängijad (sh. Särginumber, PINHash), Harjutused, Treeningud, Rekordid, Eesmärgid, Saavutused, NädalaLiidrid, Parendused. Olemasolevad read uuendatakse ID järgi — uusi ridu lisatakse ainult uute kannete või muudetud andmete puhul.Mitme seadme jaoks: kasuta kõigil telefonidel/tablettidel sama Apps Script URL-i. Rakendus loeb Drive'ist uuendusi automaatselt (~30 s) ja saadab sinu muudatused sinna — nii näevad treener ja mängijad samu andmeid.
Kuidas seadistada Apps Script? ▾
1. Ava sheets.google.com → loo uus tabel (lehtede nimed pole olulised — skript loob need ise ära)
2. Ava Laiendused → Apps Script
3. Kustuta vana kood, kleebi see kood:
4. Vali Juuruta → Uus juurutus → Veebirakendus, sea juurdepääs "Kõigile"
5. Kopeeri URL siia ja vajuta "Testi ühendust"
Oluline: kui muudad koodi hiljem, vali alati Uus juurutus (mitte "Manage deployments"), muidu vana URL ei kasuta uut koodi.
2. Ava Laiendused → Apps Script
3. Kustuta vana kood, kleebi see kood:
4. Vali Juuruta → Uus juurutus → Veebirakendus, sea juurdepääs "Kõigile"
5. Kopeeri URL siia ja vajuta "Testi ühendust"
Oluline: kui muudad koodi hiljem, vali alati Uus juurutus (mitte "Manage deployments"), muidu vana URL ei kasuta uut koodi.
function getSheet(name, headers) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sh = ss.getSheetByName(name);
if (!sh) {
sh = ss.insertSheet(name);
sh.appendRow(headers);
}
return sh;
}
function upsertRow(sh, idCol, id, rowData) {
const rows = sh.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
if (String(rows[i][idCol]) === String(id)) {
sh.getRange(i+1, 1, 1, rowData.length).setValues([rowData]);
return;
}
}
sh.appendRow(rowData);
}
const SESSION_HEADERS = ['ID','Kuupäev','Mängija','Harjutus','Kestus','Intensiivsus','Märkmed','AI','Kellaaeg','Salvestatud'];
const RECORD_HEADERS = ['MängijaID','ParimSkoor','ParimPõrgatused','TrenniPõrgatused','TrenniSkoor','ParimRütm','ParimKorvid','Uuendatud'];
const BADGE_HEADERS = ['MängijaID','Mängija','Märgid','Kokku','Uuendatud'];
const WEEKLY_LEADER_HEADERS = ['Nädal','MVP','Snaiper','Target','Tööpurustaja','Põrgataja','Hommik','Öö','Uuendatud'];
const FEEDBACK_HEADERS = ['ID','Kuupäev','Kellaaeg','Mängija','Roll','Ülesanne','Salvestatud'];
const PLAYER_HEADERS = ['ID','Nimi','Positsioon','Vanus','Särginumber','PINHash','Uuendatud'];
function ensureHeaders(sh, headers) {
if (!sh) return;
const lastCol = Math.max(sh.getLastColumn(), 1);
const existing = sh.getRange(1, 1, 1, lastCol).getValues()[0].map(String);
headers.forEach(function(h, targetIdx) {
if (existing.indexOf(h) !== -1) return;
if (targetIdx >= existing.length) {
sh.getRange(1, existing.length + 1).setValue(h);
existing.push(h);
} else {
const insertAt = targetIdx + 1;
sh.insertColumnBefore(insertAt);
sh.getRange(1, insertAt).setValue(h);
existing.splice(targetIdx, 0, h);
}
});
}
function buildRowByHeaders(sh, orderedHeaders, fieldMap) {
ensureHeaders(sh, orderedHeaders);
const lastCol = Math.max(sh.getLastColumn(), 1);
const headerRow = sh.getRange(1, 1, 1, lastCol).getValues()[0].map(String);
return headerRow.map(function(h) {
return Object.prototype.hasOwnProperty.call(fieldMap, h) ? fieldMap[h] : '';
});
}
function upsertSession(item) {
const sh = getSheet('Kanded', SESSION_HEADERS);
ensureHeaders(sh, SESSION_HEADERS);
const id = item.id || ('s' + Date.now());
const aiVal = item.ai ? (typeof item.ai === 'string' ? item.ai : JSON.stringify(item.ai)) : '';
const rowData = [id, item['kuupäev'], item['mängija'], item.harjutus, item.kestus, item.intensiivsus, item['märkmed']||'', aiVal, item.kellaaeg||'', new Date().toISOString()];
upsertRow(sh, 0, id, rowData);
}
function upsertRecord(item) {
const sh = getSheet('Rekordid', RECORD_HEADERS);
ensureHeaders(sh, RECORD_HEADERS);
const rowData = [item.id, item.bestScore||0, item.bestBounces||0, item.bestWorkoutBounces||0, item.bestWorkoutScore||0, item.bestRhythm||0, item.bestMakes||0, new Date().toISOString()];
upsertRow(sh, 0, item.id, rowData);
}
function upsertGoal(item) {
const sh = getSheet('Eesmärgid', ['MängijaID','Nädalane','Tunnid','Fookus','Uuendatud']);
const rowData = [item.id, item.weekly||4, item.hours||20, item.focus||'', new Date().toISOString()];
upsertRow(sh, 0, item.id, rowData);
}
function upsertBadge(item) {
const sh = getSheet('Saavutused', BADGE_HEADERS);
ensureHeaders(sh, BADGE_HEADERS);
const id = item.id;
const marks = typeof item.märgid === 'string' ? item.märgid : JSON.stringify(item.märgid || []);
const rowData = [id, item.mängija || '', marks, item.kokku || 0, new Date().toISOString()];
upsertRow(sh, 0, id, rowData);
}
function upsertWeeklyLeader(item) {
const sh = getSheet('NädalaLiidrid', WEEKLY_LEADER_HEADERS);
ensureHeaders(sh, WEEKLY_LEADER_HEADERS);
const week = item.nädal || item.weekKey;
const rowData = [
week,
item.mvp || '',
item.shooter || '',
item.target || '',
item.grind || '',
item.bounce || '',
item.early || '',
item.night || '',
new Date().toISOString()
];
upsertRow(sh, 0, week, rowData);
}
function appendFeedback(item) {
const sh = getSheet('Parendused', FEEDBACK_HEADERS);
const id = item.id || ('fb' + Date.now());
sh.appendRow([
id,
item['kuupäev'] || new Date().toISOString().slice(0, 10),
item.kellaaeg || '',
item['mängija'] || '',
item.roll || '',
item['ülesanne'] || item.tekst || '',
new Date().toISOString()
]);
return id;
}
function asDateText(v) {
if (!v) return '';
if (typeof v === 'string') return v.slice(0, 10);
if (v instanceof Date) return Utilities.formatDate(v, Session.getScriptTimeZone(), 'yyyy-MM-dd');
return String(v).slice(0, 10);
}
function asTimeText(v) {
if (!v) return '18:00';
if (typeof v === 'string' && /^\d{1,2}:\d{2}/.test(v)) return v.slice(0, 5);
if (v instanceof Date) return Utilities.formatDate(v, Session.getScriptTimeZone(), 'HH:mm');
const m = String(v).match(/T(\d{2}:\d{2})/);
return m ? m[1] : '18:00';
}
function upsertTraining(item) {
const sh = getSheet('Treeningud', ['ID','Kuupäev','Kellaaeg','Kestus','Asukoht','Märkmed','Saadavus','Uuendatud']);
const rows = sh.getDataRange().getValues();
const rowData = [
item.id,
asDateText(item['kuupäev']),
asTimeText(item.kellaaeg),
item.kestus,
item.asukoht || '',
item['märkused'] || '',
item.saadavus || '{}',
new Date().toISOString()
];
let found = false;
for (let i = 1; i < rows.length; i++) {
if (String(rows[i][0]) === String(item.id)) {
sh.getRange(i+1,1,1,rowData.length).setValues([rowData]);
found = true; break;
}
}
if (!found) sh.appendRow(rowData);
}
function upsertPlayer(item) {
const sh = getSheet('Mängijad', PLAYER_HEADERS);
const id = item.id || ('p' + Date.now());
const fieldMap = {
'ID': id,
'Nimi': item.nimi || item.name || '',
'Positsioon': item.positsioon || item.pos || '',
'Vanus': item.vanus || item.age || '',
'Särginumber': item.sarginumber != null ? String(item.sarginumber) : (item.jersey != null ? String(item.jersey) : ''),
'PINHash': item.pinHash || item.PINHash || '',
'Uuendatud': new Date().toISOString()
};
const rowData = buildRowByHeaders(sh, PLAYER_HEADERS, fieldMap);
upsertRow(sh, 0, id, rowData);
}
function doPost(e) {
try {
const d = JSON.parse(e.postData.contents);
const type = d.type || 'session';
if (type === 'session') {
upsertSession(d);
} else if (type === 'record') {
upsertRecord(d);
} else if (type === 'goal') {
upsertGoal(d);
} else if (type === 'badge') {
upsertBadge(d);
} else if (type === 'weeklyLeader') {
upsertWeeklyLeader(d);
} else if (type === 'feedback') {
appendFeedback(d);
} else if (type === 'player') {
upsertPlayer(d);
} else if (type === 'drill') {
const sh = getSheet('Harjutused', ['ID','Nimi','Ikoon','Kategooria','Tase','Kestus','Seeriad','SeeriaSek','Puhkus','Kirjeldus','Sammud','Vead','Aktiivne','Uuendatud']);
const rows = sh.getDataRange().getValues();
const rowData = [d.id, d.nimi, d.ikoon||'🏀', d.kategooria, d.tase, d.kestus, d.seeriad||3, d.seeriaSek||30, d.puhkus||15, d.kirjeldus||'', JSON.stringify(d.sammud||[]), JSON.stringify(d.vead||[]), d.aktiivne, new Date().toISOString()];
let found = false;
for (let i = 1; i < rows.length; i++) {
if (String(rows[i][0]) === String(d.id)) {
sh.getRange(i+1,1,1,rowData.length).setValues([rowData]);
found = true; break;
}
}
if (!found) sh.appendRow(rowData);
} else if (type === 'training') {
upsertTraining(d);
} else if (type === 'batch') {
const items = d.items || [];
for (const item of items) {
const t = item.type;
if (t === 'session') {
upsertSession(item);
} else if (t === 'record') {
upsertRecord(item);
} else if (t === 'goal') {
upsertGoal(item);
} else if (t === 'badge') {
upsertBadge(item);
} else if (t === 'weeklyLeader') {
upsertWeeklyLeader(item);
} else if (t === 'feedback') {
appendFeedback(item);
} else if (t === 'player') {
upsertPlayer(item);
} else if (t === 'drill') {
const sh = getSheet('Harjutused', ['ID','Nimi','Ikoon','Kategooria','Tase','Kestus','Seeriad','SeeriaSek','Puhkus','Kirjeldus','Sammud','Vead','Aktiivne','Uuendatud']);
const rows = sh.getDataRange().getValues();
const rowData = [item.id, item.nimi, item.ikoon||'🏀', item.kategooria, item.tase, item.kestus, item.seeriad||3, item.seeriaSek||30, item.puhkus||15, item.kirjeldus||'', JSON.stringify(item.sammud||[]), JSON.stringify(item.vead||[]), item.aktiivne, new Date().toISOString()];
let found = false;
for (let i = 1; i < rows.length; i++) {
if (String(rows[i][0]) === String(item.id)) {
sh.getRange(i+1,1,1,rowData.length).setValues([rowData]);
found = true; break;
}
}
if (!found) sh.appendRow(rowData);
} else if (t === 'training') {
upsertTraining(item);
}
}
} else if (type === 'delete') {
const sheetName = d.kind === 'player' ? 'Mängijad' : d.kind === 'drill' ? 'Harjutused' : d.kind === 'training' ? 'Treeningud' : null;
if (sheetName) {
const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (sh) {
const rows = sh.getDataRange().getValues();
for (let i = rows.length - 1; i >= 1; i--) {
if (String(rows[i][0]) === String(d.id)) {
sh.deleteRow(i + 1);
break;
}
}
}
}
}
return ContentService.createTextOutput(JSON.stringify({ok:true}))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ok:false, error:String(err)}))
.setMimeType(ContentService.MimeType.JSON);
}
}
function sheetToObjects(sh) {
const rows = sh.getDataRange().getValues();
if (rows.length < 2) return [];
const headers = rows[0];
return rows.slice(1).map(r => {
const o = {};
headers.forEach((h, i) => o[h] = r[i]);
return o;
});
}
function doGet(e) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const out = {ok: true};
const kanded = ss.getSheetByName('Kanded');
const mangijad = ss.getSheetByName('Mängijad');
const harjutused = ss.getSheetByName('Harjutused');
const treeningud = ss.getSheetByName('Treeningud');
const rekordid = ss.getSheetByName('Rekordid');
const eesmargid = ss.getSheetByName('Eesmärgid');
const saavutused = ss.getSheetByName('Saavutused');
const nadalaLiidrid = ss.getSheetByName('NädalaLiidrid');
const parendused = ss.getSheetByName('Parendused');
out.kanded = kanded ? sheetToObjects(kanded) : [];
out.mangijad = mangijad ? sheetToObjects(mangijad) : [];
out.harjutused = harjutused ? sheetToObjects(harjutused) : [];
out.treeningud = treeningud ? sheetToObjects(treeningud) : [];
out.rekordid = rekordid ? sheetToObjects(rekordid) : [];
out.eesmärgid = eesmargid ? sheetToObjects(eesmargid) : [];
out.saavutused = saavutused ? sheetToObjects(saavutused) : [];
out.nadalaLiidrid = nadalaLiidrid ? sheetToObjects(nadalaLiidrid) : [];
out.parendused = parendused ? sheetToObjects(parendused) : [];
return ContentService.createTextOutput(JSON.stringify(out))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ok:false, error:String(err)}))
.setMimeType(ContentService.MimeType.JSON);
}
}
Andmed
Tühjenda mängijate nimekiri
Eemaldab kõik mängijad (kanded jäävad)
Eemalda duplikaat harjutused
Kustutab korduvad ja sisseehitatud koopiad
Kustuta kõik kanded
Mängijad ja harjutused jäävad alles
Lähtesta kogu rakendus
Kustutab kõik andmed
Versioon
Court IQTühjendab vahemälu
Kui näed vana versiooni (nt v3.0), ava brauseris:
soss111.github.io/courtiq/reset.html
soss111.github.io/courtiq/reset.html