Μένουν στον πελάτη: μεταφέρονται μόνα τους σε κάθε επόμενη δουλειά του, και το σκάφος προσυμπληρώνει το «Σκάφος / δουλειά».
Συμπληρώνονται μόνο για επιχειρήσεις — μπαίνουν στην προσφορά και μεταφέρονται μόνα τους στην επόμενη δουλειά του ίδιου πελάτη.
Ίδια ρύθμιση με τις Ρυθμίσεις — ισχύει για όλες τις προσφορές.
Ο νέος λογαριασμός είναι «εκκρεμής» μέχρι το άτομο να ορίσει κωδικό μόνο του: ανοίγει το site → «Πρώτη φορά; Όρισε κωδικό» → βάζει το email του και διαλέγει κωδικό. Ξέχασε τον κωδικό; πάτα Reset δίπλα του — τον ξαναορίζει. Ρόλοι: διαχειριστής (+ διαχείριση χρηστών), χρήστης (κανονική δουλειά), μόνο ανάγνωση.
Τα leads μπαίνουν στο στάδιο «Προσφορά» με τιμή, έτοιμα για να δοκιμάσεις το χαρτί της προσφοράς. Οι 8 δουλειές αντικαθιστούν ό,τι υπάρχει τώρα.
Δεν διατίθενται όταν η εφαρμογή είναι συνδεδεμένη στην κοινή βάση — τα φανταστικά δεδομένα θα τα έβλεπαν όλοι. Δοκίμασε στην προεπισκόπηση, που δουλεύει τοπικά.
Στέλνονται μόνο οι δουλειές που έχουν εξοφληθεί. Επίσης, με το «Εκτύπωση» κάθε προσφορά αποθηκεύεται ως PDF σε φάκελο «AnyWeather — Προσφορές» στο Drive σου. Η διεύθυνση μένει μόνο σε αυτή τη συσκευή — δεν μπαίνει πουθενά στον κώδικα.
Επεκτάσεις → Apps Script.Ανάπτυξη → Νέα ανάπτυξη, τύπος Εφαρμογή ιστού.Εγώ · Πρόσβαση: Οποιοσδήποτε. Η δεύτερη ρύθμιση είναι απαραίτητη./exec και κόλλησέ την παραπάνω.Ανάπτυξη → Διαχείριση αναπτύξεων → ✏️ → Νέα έκδοση (η διεύθυνση μένει ίδια).var SHEET_NAME = 'Δουλειές';
var HEAD = ['ID','Σκάφος','Περιγραφή','Πελάτης','Τηλέφωνο','Ποσό','Προκαταβολή',
'Υπόλοιπο','Ύρες','Ποιος','Παράδοση','Εξοφλήθηκε','Υλικά','Σημειώσεις','Ενημερώθηκε'];
function json(o){
return ContentService.createTextOutput(JSON.stringify(o))
.setMimeType(ContentService.MimeType.JSON);
}
function doGet(){ return json({ ok: true }); }
function doPost(e){
var lock = LockService.getScriptLock();
lock.waitLock(20000);
try {
var d = JSON.parse(e.postData.contents);
if (d.ping) return json({ ok: true, ping: true });
if (d.quote) return saveQuotePdf_(d.quote);
var j = d.job;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);
if (sh.getLastRow() === 0) sh.appendRow(HEAD);
var row = [j.id, j.name, j.desc, j.client, j.phone, j.price, j.deposit,
j.balance, j.hours, j.who, j.due, j.paid ? 'ΝΑΙ' : 'ΟΧΙ',
j.mat, j.notes, new Date()];
var n = sh.getLastRow() - 1;
var ids = n > 0 ? sh.getRange(2, 1, n, 1).getValues() : [];
var at = ids.findIndex(function (r) { return String(r[0]) === String(j.id); });
if (at >= 0) sh.getRange(at + 2, 1, 1, row.length).setValues([row]);
else sh.appendRow(row);
return json({ ok: true });
} catch (err) {
return json({ ok: false, error: String(err) });
} finally {
lock.releaseLock();
}
}
var QUOTE_FOLDER = 'AnyWeather — Προσφορές';
function quoteFolder_() {
var it = DriveApp.getFoldersByName(QUOTE_FOLDER);
return it.hasNext() ? it.next() : DriveApp.createFolder(QUOTE_FOLDER);
}
// Το CRM στέλνει το HTML της προσφοράς· το γυρνάμε PDF και το βάζουμε στο Drive.
// Ένα αρχείο ανά δουλειά: αν ξαναέρθει, το παλιό πάει στον κάδο.
function saveQuotePdf_(q) {
var pdf = Utilities.newBlob(q.html, 'text/html', 'q.html').getAs('application/pdf');
pdf.setName((q.no || 'Προσφορά') + (q.boat ? ' — ' + q.boat : '') + '.pdf');
if (q.fileId) {
try { DriveApp.getFileById(q.fileId).setTrashed(true); } catch (e) {}
}
var f = quoteFolder_().createFile(pdf);
return json({ ok: true, fileId: f.getId(), url: f.getUrl() });
}