diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..43c251ab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.22.2 AS builder + +WORKDIR /app +COPY . . + +RUN go build -o app + +FROM ubuntu:latest + +WORKDIR /app +COPY --from=builder /app/app ./ +COPY --from=builder /app/web ./web + +EXPOSE 7540 + +CMD ["/app/app"] diff --git a/README.md b/README.md index 597678ae..2e1c0af8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,35 @@ -# Файлы для итогового задания +Проект предназначен для мониторинга, создания, редактирования и изменения статуса задач. Поддерживает правила повторения событий и хранит данные в SQLite. -В директории `tests` находятся тесты для проверки API, которое должно быть реализовано в веб-сервере. -Директория `web` содержит файлы фронтенда. \ No newline at end of file +# **Функциональность:** + +Создание и хранение задач в базе данных SQLite. + +Редактирование задач, включая изменение даты и повторяющихся правил. + +Рассчет следующей даты выполнения задачи на основе правил повторения. + +API для управления задачами. + +Интеграция с фронтендом (директория web). + +Тестирование API (директория tests). + + +# **Структура проекта:** + +db.go – инициализация базы данных, проверка существования, создание пути и подключение к SQLite. + +nextdate.go – реализация логики повторяющихся задач, проверка даты на корректность и обработка високосных годов. + +tasks.go – обработчики API с проверкой входных данных. + +Dockerfile – описание сборки контейнера. + +go.mod / go.sum – управление зависимостями. + +main.go – стартовая точка проекта, запуск веб-сервера (порт 7540), инициализация БД, маршрутизация API. + +Команды для сборки +docker build -t YourGoPlanner . +docker run -d -p 7540:7540 -v ${PWD}/scheduler.db:/app/scheduler.db go_final_project diff --git a/app b/app new file mode 100644 index 00000000..a0808d71 Binary files /dev/null and b/app differ diff --git a/db.go b/db.go new file mode 100644 index 00000000..b36cbc41 --- /dev/null +++ b/db.go @@ -0,0 +1,53 @@ +package main + +import ( + "database/sql" + "log" + "os" + + "path/filepath" + + _ "modernc.org/sqlite" +) + +func InitDB() (*sql.DB, error) { + appPath, err := os.Executable() + if err != nil { + log.Fatal(err) + } + dbFile := filepath.Join(filepath.Dir(appPath), "scheduler.db") + + _, err = os.Stat(dbFile) + install := os.IsNotExist(err) + log.Println("Using database file:", dbFile) + + db, err := sql.Open("sqlite", dbFile) + if err != nil { + return nil, err + } + + if install { + err = createTables(db) + if err != nil { + return nil, err + } + log.Println("Database created") + } + + return db, nil +} + +func createTables(db *sql.DB) error { + query := ` + CREATE TABLE IF NOT EXISTS scheduler ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + title TEXT NOT NULL, + comment TEXT, + repeat TEXT CHECK(length(repeat) <= 128) + ); + CREATE INDEX IF NOT EXISTS idx_date ON scheduler(date); + ` + _, err := db.Exec(query) + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..d6ac3fb8 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module go_final_project + +go 1.22.2 + +require ( + github.com/jmoiron/sqlx v1.4.0 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/stretchr/testify v1.10.0 + modernc.org/sqlite v1.34.5 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..6d88b4bb --- /dev/null +++ b/go.sum @@ -0,0 +1,64 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= +modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= +modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/main.go b/main.go new file mode 100644 index 00000000..72866433 --- /dev/null +++ b/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "log" + "net/http" + "time" + + _ "modernc.org/sqlite" +) + +const ( + port = ":7540" + webDir = "./web" + dateFmt = "20060102" +) + +func nextDateHandler(w http.ResponseWriter, r *http.Request) { + nowStr := r.URL.Query().Get("now") + dateStr := r.URL.Query().Get("date") + repeatStr := r.URL.Query().Get("repeat") + + if nowStr == "" || dateStr == "" || repeatStr == "" { + http.Error(w, "Not all parameters passed", http.StatusBadRequest) + return + } + + now, err := time.Parse(dateFmt, nowStr) + if err != nil { + http.Error(w, "Incorrect format now", http.StatusBadRequest) + return + } + + next, err := NextDate(now, dateStr, repeatStr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte(next)) +} + +func main() { + db, err := InitDB() + if err != nil { + log.Fatal("Error creating database:", err) + } + defer db.Close() + + http.Handle("/", http.FileServer(http.Dir(webDir))) + + http.HandleFunc("/api/nextdate", nextDateHandler) + http.HandleFunc("/api/task", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + handleCreateTask(w, r, db) + } else if r.Method == http.MethodGet { + if r.URL.Query().Has("id") { + handleGetTaskByID(w, r, db) + } else { + http.Error(w, `{"error":"Не указан ID задачи"}`, http.StatusBadRequest) + } + } else if r.Method == http.MethodPut { + handleUpdateTask(w, r, db) + } else if r.Method == http.MethodDelete { + handleDeleteTask(w, r, db) + } else { + http.Error(w, `{"error":"Метод не поддерживается"}`, http.StatusMethodNotAllowed) + } + }) + + http.HandleFunc("/api/tasks", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + handleGetTasks(w, r, db) + } else { + http.Error(w, `{"error":"Метод не поддерживается"}`, http.StatusMethodNotAllowed) + } + }) + + http.HandleFunc("/api/task/done", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + handleTaskDone(w, r, db) + } else { + http.Error(w, `{"error":"Метод не поддерживается"}`, http.StatusMethodNotAllowed) + } + }) + + log.Printf("Server running http://localhost%s\n", port) + err = http.ListenAndServe(port, nil) + if err != nil { + log.Fatal(err) + } +} diff --git a/nextdate.go b/nextdate.go new file mode 100644 index 00000000..4aea1948 --- /dev/null +++ b/nextdate.go @@ -0,0 +1,50 @@ +package main + +import ( + "strconv" + "strings" + "time" +) + +func NextDate(now time.Time, date string, repeat string) (string, error) { + startDate, err := time.Parse("20060102", date) + if err != nil || startDate.Year() < 1600 || startDate.Year() > 9999 { + return "", nil + } + + parts := strings.Split(repeat, " ") + switch parts[0] { + case "y": + if len(parts) > 1 { + return "", nil + } + nextDate := startDate.AddDate(1, 0, 0) + for !nextDate.After(now) { + nextDate = nextDate.AddDate(1, 0, 0) + } + if startDate.Month() == time.February && startDate.Day() == 29 && !isLeapYear(nextDate.Year()) { + nextDate = time.Date(nextDate.Year(), time.March, 1, 0, 0, 0, 0, time.UTC) + } + return nextDate.Format("20060102"), nil + + case "d": + if len(parts) < 2 { + return "", nil + } + days, err := strconv.Atoi(parts[1]) + if err != nil || days <= 0 || days > 365 { + return "", nil + } + nextDate := startDate.AddDate(0, 0, days) + for !nextDate.After(now) { + nextDate = nextDate.AddDate(0, 0, days) + } + return nextDate.Format("20060102"), nil + } + + return "", nil +} + +func isLeapYear(year int) bool { + return (year%4 == 0 && year%100 != 0) || (year%400 == 0) +} diff --git a/scheduler.db b/scheduler.db new file mode 100644 index 00000000..95bb6ecf Binary files /dev/null and b/scheduler.db differ diff --git a/tasks.go b/tasks.go new file mode 100644 index 00000000..82ea6bd9 --- /dev/null +++ b/tasks.go @@ -0,0 +1,285 @@ +package main + +import ( + "database/sql" + "encoding/json" + "net/http" + "regexp" + "strconv" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type Task struct { + ID string `json:"id"` + Date string `json:"date"` + Title string `json:"title"` + Comment string `json:"comment"` + Repeat string `json:"repeat"` +} + +var ( + dateRegex = regexp.MustCompile(`^\d{8}$`) + repeatRegex = regexp.MustCompile(`^(d \d+|y|w (\d,?)+)$`) +) + +func handleCreateTask(w http.ResponseWriter, r *http.Request, db *sql.DB) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + const dateFormat = "20060102" + var task Task + + if err := json.NewDecoder(r.Body).Decode(&task); err != nil { + http.Error(w, `{"error":"Ошибка десериализации JSON"}`, http.StatusBadRequest) + return + } + + if task.Title == "" { + http.Error(w, `{"error":"Не указан заголовок задачи"}`, http.StatusBadRequest) + return + } + if task.Date == "" { + task.Date = time.Now().Format(dateFormat) + } + + if !dateRegex.MatchString(task.Date) { + http.Error(w, `{"error":"Дата указана в неверном формате"}`, http.StatusBadRequest) + return + } + + date, err := time.Parse(dateFormat, task.Date) + if err != nil { + http.Error(w, `{"error":"Дата указана в неверном формате"}`, http.StatusBadRequest) + return + } + + now := time.Now() + if date.Format(dateFormat) < now.Format(dateFormat) { + if task.Repeat == "" { + date = now + } else { + nextDateStr, err := NextDate(now, task.Date, task.Repeat) + if err != nil { + http.Error(w, `{"error":"Ошибка при вычислении следующей даты"}`, http.StatusBadRequest) + return + } + date, err = time.Parse(dateFormat, nextDateStr) + if err != nil { + http.Error(w, `{"error":"Ошибка обработки следующей даты"}`, http.StatusInternalServerError) + return + } + } + } + task.Date = date.Format(dateFormat) + + if task.Repeat != "" && !repeatRegex.MatchString(task.Repeat) { + http.Error(w, `{"error":"Неверный формат правила повторения"}`, http.StatusBadRequest) + return + } + + query := `INSERT INTO scheduler (date, title, comment, repeat) VALUES (?, ?, ?, ?)` + res, err := db.Exec(query, task.Date, task.Title, task.Comment, task.Repeat) + if err != nil { + http.Error(w, `{"error":"Ошибка базы данных"}`, http.StatusInternalServerError) + return + } + + id, err := res.LastInsertId() + if err != nil { + http.Error(w, `{"error":"Не удалось получить ID"}`, http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"id": strconv.FormatInt(id, 10)}) +} +func handleGetTasks(w http.ResponseWriter, r *http.Request, db *sql.DB) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + const limit = 50 + query := `SELECT id, date, title, comment, repeat FROM scheduler ORDER BY date ASC LIMIT ?` + + rows, err := db.Query(query, limit) + if err != nil { + http.Error(w, `{"error":"Ошибка базы данных"}`, http.StatusInternalServerError) + return + } + defer rows.Close() + + var tasks []map[string]string + for rows.Next() { + var id int + var date, title, comment, repeat string + if err := rows.Scan(&id, &date, &title, &comment, &repeat); err != nil { + http.Error(w, `{"error":"Ошибка чтения данных"}`, http.StatusInternalServerError) + return + } + + task := map[string]string{ + "id": strconv.Itoa(id), + "date": date, + "title": title, + "comment": comment, + "repeat": repeat, + } + + tasks = append(tasks, task) + } + + if tasks == nil { + tasks = []map[string]string{} + } + + json.NewEncoder(w).Encode(map[string]interface{}{"tasks": tasks}) +} +func handleGetTaskByID(w http.ResponseWriter, r *http.Request, db *sql.DB) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error":"Не указан идентификатор"}`, http.StatusBadRequest) + return + } + + query := `SELECT id, date, title, comment, repeat FROM scheduler WHERE id = ?` + row := db.QueryRow(query, id) + + var task Task + err := row.Scan(&task.ID, &task.Date, &task.Title, &task.Comment, &task.Repeat) + if err == sql.ErrNoRows { + http.Error(w, `{"error":"Задача не найдена"}`, http.StatusNotFound) + return + } else if err != nil { + http.Error(w, `{"error":"Ошибка базы данных"}`, http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(task) +} +func handleUpdateTask(w http.ResponseWriter, r *http.Request, db *sql.DB) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + var task Task + if err := json.NewDecoder(r.Body).Decode(&task); err != nil { + http.Error(w, `{"error":"Ошибка десериализации JSON"}`, http.StatusBadRequest) + return + } + + if task.ID == "" { + http.Error(w, `{"error":"Не указан ID задачи"}`, http.StatusBadRequest) + return + } + if task.Title == "" { + http.Error(w, `{"error":"Не указан заголовок задачи"}`, http.StatusBadRequest) + return + } + if !dateRegex.MatchString(task.Date) { + http.Error(w, `{"error":"Дата указана в неверном формате"}`, http.StatusBadRequest) + return + } + + // Проверяем, можно ли распарсить дату + if _, err := time.Parse("20060102", task.Date); err != nil { + http.Error(w, `{"error":"Дата указана в неверном формате"}`, http.StatusBadRequest) + return + } + + if task.Repeat != "" && !repeatRegex.MatchString(task.Repeat) { + http.Error(w, `{"error":"Неверный формат правила повторения"}`, http.StatusBadRequest) + return + } + + query := `UPDATE scheduler SET date = ?, title = ?, comment = ?, repeat = ? WHERE id = ?` + res, err := db.Exec(query, task.Date, task.Title, task.Comment, task.Repeat, task.ID) + if err != nil { + http.Error(w, `{"error":"Ошибка базы данных"}`, http.StatusInternalServerError) + return + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + http.Error(w, `{"error":"Ошибка обработки результата"}`, http.StatusInternalServerError) + return + } + if rowsAffected == 0 { + http.Error(w, `{"error":"Задача не найдена"}`, http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(map[string]string{}) +} +func handleTaskDone(w http.ResponseWriter, r *http.Request, db *sql.DB) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error":"Не указан идентификатор"}`, http.StatusBadRequest) + return + } + + var task Task + query := `SELECT id, date, repeat FROM scheduler WHERE id = ?` + err := db.QueryRow(query, id).Scan(&task.ID, &task.Date, &task.Repeat) + if err == sql.ErrNoRows { + http.Error(w, `{"error":"Задача не найдена"}`, http.StatusNotFound) + return + } else if err != nil { + http.Error(w, `{"error":"Ошибка базы данных"}`, http.StatusInternalServerError) + return + } + + if task.Repeat == "" { + // Если repeat пуст, удаляем задачу + _, err := db.Exec(`DELETE FROM scheduler WHERE id = ?`, id) + if err != nil { + http.Error(w, `{"error":"Ошибка удаления задачи"}`, http.StatusInternalServerError) + return + } + } else { + // Вычисляем следующую дату + now := time.Now() + nextDate, err := NextDate(now, task.Date, task.Repeat) + if err != nil { + http.Error(w, `{"error":"Ошибка вычисления следующей даты"}`, http.StatusBadRequest) + return + } + + // Обновляем дату задачи + _, err = db.Exec(`UPDATE scheduler SET date = ? WHERE id = ?`, nextDate, id) + if err != nil { + http.Error(w, `{"error":"Ошибка обновления даты"}`, http.StatusInternalServerError) + return + } + } + + json.NewEncoder(w).Encode(map[string]string{}) +} +func handleDeleteTask(w http.ResponseWriter, r *http.Request, db *sql.DB) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error":"Не указан идентификатор задачи"}`, http.StatusBadRequest) + return + } + + query := `DELETE FROM scheduler WHERE id = ?` + res, err := db.Exec(query, id) + if err != nil { + http.Error(w, `{"error":"Ошибка базы данных"}`, http.StatusInternalServerError) + return + } + + affected, err := res.RowsAffected() + if err != nil { + http.Error(w, `{"error":"Ошибка при проверке удаления"}`, http.StatusInternalServerError) + return + } + + if affected == 0 { + http.Error(w, `{"error":"Задача не найдена"}`, http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{}) +}