9 Commits 221f46b75a ... a2ab929823

Author SHA1 Message Date
  0xc3 221f46b75a dev 1 day ago
  0xc3 a2ab929823 dev 1 day ago
  0xc3 8fca4287b5 dev 1 day ago
  0xc3 b0f3a71a3b dev 2 days ago
  0xc3 4c469b5cbf dev 3 days ago
  0xc3 7050b20fb2 dev 4 days ago
  0xc3 380633dbc3 dev 4 days ago
  0xc3 4842588ad9 init 4 days ago
  0xc3 3d454861c6 init 1 day ago

+ 0 - 2
.env-temp

@@ -1,2 +0,0 @@
-PORT=":8080"
-DB_URL="crm.db"

+ 3 - 4
.gitignore

@@ -1,5 +1,4 @@
+.cache/
 build/
-.env
-*.xls
-*.xlsx
-*.db
+vcpkg_installed/
+imgui.ini

+ 68 - 0
CMakeLists.txt

@@ -0,0 +1,68 @@
+cmake_minimum_required(VERSION 3.15...4.0)
+project(app)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+# === CCACHE АВТОМАТИЧЕСКИЙ ЗАПУСК ===
+find_program(CCACHE_PROGRAM ccache)
+if(CCACHE_PROGRAM)
+    message(STATUS "ccache найден: ${CCACHE_PROGRAM}")
+
+    # Это работает и с Ninja, и с Makefiles
+    set(CMAKE_C_COMPILER_LAUNCHER   "${CCACHE_PROGRAM}")
+    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
+
+    # Для старых версий CMake (< 3.4) можно использовать альтернативу:
+    # set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
+endif()
+
+set(OUTPUT_DIR ${CMAKE_BINARY_DIR}/$<CONFIG>)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_DIR})
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIR})
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_DIR})
+
+file(GLOB_RECURSE LIB_SOURCES
+    "${PROJECT_SOURCE_DIR}/src/saura/main/main.cpp"
+    "${PROJECT_SOURCE_DIR}/src/saura/**/*.cpp"
+)
+
+# Создаем библиотеку
+add_library(${PROJECT_NAME}_lib STATIC ${LIB_SOURCES})
+
+# Пути
+set(SRC_RES_DIR "${CMAKE_SOURCE_DIR}/res")
+set(RES_SYMLINK "${OUTPUT_DIR}/res")
+
+find_package(SDL3 CONFIG REQUIRED)
+find_package(spdlog CONFIG REQUIRED)
+find_package(imgui CONFIG REQUIRED)
+find_package(glm CONFIG REQUIRED)
+find_package(cpr CONFIG REQUIRED)
+
+# Добавляем include-пути
+target_include_directories(${PROJECT_NAME}_lib PUBLIC
+    ${PROJECT_SOURCE_DIR}
+    ${PROJECT_SOURCE_DIR}/src
+)
+
+target_include_directories(${PROJECT_NAME}_lib PRIVATE
+    ${PROJECT_SOURCE_DIR}
+    ${PROJECT_SOURCE_DIR}/src
+)
+
+target_link_libraries(${PROJECT_NAME}_lib PUBLIC
+    SDL3::SDL3
+    spdlog::spdlog
+    imgui::imgui
+    glm::glm
+    cpr::cpr
+)
+
+file(GLOB CMD_PROJECTS "src/saura/cmd/*")
+foreach(cmd_path ${CMD_PROJECTS})
+    if(IS_DIRECTORY ${cmd_path} AND EXISTS ${cmd_path}/CMakeLists.txt)
+        add_subdirectory(${cmd_path})
+    endif()
+endforeach()

+ 2 - 32
README.md

@@ -1,34 +1,4 @@
-```shell
-go build -o ./build/cmd/server/main.exe ./src/cmd/server/main.go
-./build/cmd/server/main.exe
 ```
-
-```shell
-go run src/cmd/server/main.go
-```
-
-# migrations-up
-```shell
-goose -dir ./migrations/ sqlite3 ${DB_URL} up
-```
-
-# migrations-reset
-```shell
-goose -dir ./migrations/ sqlite3 ${DB_URL} reset
-```
-
-```shell
-libreoffice --headless --convert-to xlsx file.xls
-```
-
-## Зависимости
-
-```bash
-go get -u github.com/mattn/go-sqlite3@latest
-go get -u github.com/joho/godotenv/autoload@latest
-go get -u github.com/labstack/echo/v4@latest
-go get -u github.com/labstack/echo/v4/middleware@latest
-go get -u github.com/google/uuid@latest
-go get -u github.com/xuri/excelize/v2@latest
-go install github.com/pressly/goose/v3/cmd/goose@latest
+cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=/home/deck/.local/share/vcpkg/scripts/buildsystems/vcpkg.cmake
+cmake --build build --config Debug -j $(nproc)
 ```

+ 0 - 29
go.mod

@@ -1,29 +0,0 @@
-module saura
-
-go 1.24.0
-
-require (
-	github.com/google/uuid v1.6.0
-	github.com/joho/godotenv v1.5.1
-	github.com/labstack/echo/v4 v4.14.0
-	github.com/mattn/go-sqlite3 v1.14.32
-	github.com/xuri/excelize/v2 v2.10.0
-)
-
-require (
-	github.com/labstack/gommon v0.4.2 // indirect
-	github.com/mattn/go-colorable v0.1.14 // indirect
-	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/richardlehane/mscfb v1.0.4 // indirect
-	github.com/richardlehane/msoleps v1.0.4 // indirect
-	github.com/tiendc/go-deepcopy v1.7.2 // indirect
-	github.com/valyala/bytebufferpool v1.0.0 // indirect
-	github.com/valyala/fasttemplate v1.2.2 // indirect
-	github.com/xuri/efp v0.0.1 // indirect
-	github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
-	golang.org/x/crypto v0.46.0 // indirect
-	golang.org/x/net v0.48.0 // indirect
-	golang.org/x/sys v0.39.0 // indirect
-	golang.org/x/text v0.32.0 // indirect
-	golang.org/x/time v0.14.0 // indirect
-)

+ 0 - 73
go.sum

@@ -1,73 +0,0 @@
-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/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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
-github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
-github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
-github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
-github.com/labstack/echo/v4 v4.14.0 h1:+tiMrDLxwv6u0oKtD03mv+V1vXXB3wCqPHJqPuIe+7M=
-github.com/labstack/echo/v4 v4.14.0/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
-github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
-github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
-github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-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.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
-github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
-github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
-github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
-github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
-github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
-github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/tiendc/go-deepcopy v1.6.1 h1:uVRTItFeNHkMcLueHS7OCsxgxT9P8MzGB/taUa2Y4Tk=
-github.com/tiendc/go-deepcopy v1.6.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
-github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
-github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
-github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
-github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
-github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
-github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
-github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
-github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
-github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
-github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
-github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
-github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
-github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
-golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
-golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
-golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
-golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
-golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
-golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
-golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
-golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
-golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
-golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
-golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
-golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
-golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
-golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
-golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
-golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
-golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
-golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 0 - 31
migrations/00001_clients_table.sql

@@ -1,31 +0,0 @@
--- +goose Up
-CREATE TABLE clients (
-  id TEXT PRIMARY KEY,
-  id2 TEXT,
-  mark TEXT,
-  contractor INTEGER,
-  full_name TEXT,
-  type TEXT,
-  email TEXT,
-  legal_address TEXT,
-  physical_address TEXT,
-  registration_date TEXT,
-  ad_channel TEXT,
-  reg_data_1 TEXT,
-  reg_data_2 TEXT,
-  note TEXT,
-  request_count INTEGER,
-  birthday TEXT,
-  income NUMERIC -- NUMERIC (или REAL) для числового значения
-);
-
-CREATE TABLE client_phones (
-  client_id INTEGER,
-  phone TEXT,
-  FOREIGN KEY (client_id) REFERENCES clients (id)
-);
-
--- +goose Down
-DROP TABLE client_phones;
-
-DROP TABLE clients;

+ 0 - 22
migrations/00002_products_table.sql

@@ -1,22 +0,0 @@
--- +goose Up
-CREATE TABLE products (
-  id TEXT PRIMARY KEY,
-  id2 TEXT,
-  name TEXT,
-  serial_number TEXT,
-  article TEXT,
-  date TEXT,
-  quantity TEXT, -- Кол-во
-  retail_price TEXT, -- Розничная цена
-  purchase_price TEXT, -- Закупочная цена
-  exchange_rate_pc TEXT, -- Курс ПК
-  exchange_rate_pr TEXT, -- Курс ПР
-  warehouse TEXT, -- Склад
-  location TEXT,
-  customer_order TEXT, -- Заказ клиента
-  supplier_order TEXT, -- Заказ поставщику
-  supplier TEXT -- Поставщик
-);
-
--- +goose Down
-DROP TABLE products;

+ 0 - 4
migrations/00003_calls_table.sql

@@ -1,4 +0,0 @@
--- +goose Up
--- CREATE TABLE IF NOT EXISTS calls (id TEXT PRIMARY KEY, id2 TEXT);
--- +goose Down
--- DROP TABLE calls;

+ 0 - 15
migrations/00004_client_ad_channels_table.sql

@@ -1,15 +0,0 @@
--- +goose Up
-CREATE TABLE ad_channels (id INTEGER PRIMARY KEY, name TEXT);
-
-INSERT INTO
-  ad_channels (name)
-VALUES
-  ('Интернет'),
-  ('Партнер'),
-  ('По рекомендации'),
-  ('Постоянные клиенты'),
-  ('Проходящий поток'),
-  ('СЦ ТРУД');
-
--- +goose Down
-DROP TABLE ad_channels;

+ 0 - 19
migrations/00005_client_marks_table.sql

@@ -1,19 +0,0 @@
--- +goose Up
-CREATE TABLE marks (id INTEGER PRIMARY KEY, name TEXT);
-
-INSERT INTO
-  marks (name)
-VALUES
-  ('-5%'),
-  ('-10%'),
-  ('-20%'),
-  ('-30%'),
-  ('blacklist'),
-  ('discount'),
-  ('regular'),
-  ('VIP'),
-  ('Животное'),
-  ('Мудак');
-
--- +goose Down
-DROP TABLE marks;

+ 7 - 0
scripts/build-debug.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+VCPKG_ROOT="$HOME/.local/share/vcpkg"
+PATH="$PATH:$VCPKG_ROOT"
+
+cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
+cmake --build build --config Debug -j $(nproc)

+ 7 - 0
scripts/build-release.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+
+VCPKG_ROOT="$HOME/.local/share/vcpkg"
+PATH="$PATH:$VCPKG_ROOT"
+
+cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
+cmake --build build --config Release -j $(nproc)

+ 3 - 0
scripts/run-debug.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+./build/Debug/sandbox

+ 3 - 0
scripts/run-release.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+./build/Release/sandbox

+ 13 - 0
scripts/setup.sh

@@ -0,0 +1,13 @@
+#!/bin/sh
+
+VCPKG_ROOT="$HOME/.local/share/vcpkg"
+PATH="$PATH:$VCPKG_ROOT"
+
+# Создаём директорию и сразу клонируем официальный репозиторий Microsoft
+git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT
+
+# Делаем первый bootstrap (собирает сам vcpkg)
+$VCPKG_ROOT/bootstrap-vcpkg.sh
+
+vcpkg version
+vcpkg install

+ 0 - 11
src/app/app.go

@@ -1,11 +0,0 @@
-package app
-
-import "os"
-
-type App struct {
-	Port string
-}
-
-func Init(appCtx *App) {
-	appCtx.Port = os.Getenv("PORT")
-}

+ 0 - 144
src/cmd/immigration/clients/main.go

@@ -1,144 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"saura/src/common"
-	clientApi "saura/src/server/client"
-	"saura/src/server/db/repo"
-	"strconv"
-	"strings"
-
-	"github.com/xuri/excelize/v2"
-
-	_ "github.com/joho/godotenv/autoload"
-)
-
-var FILE_PATH string
-
-func ReadClientsFromFile(file_path string) ([]repo.ClientInfo, error) {
-	clients := []repo.ClientInfo{}
-
-	file, err := excelize.OpenFile(file_path)
-	if err != nil {
-		fmt.Println(err)
-		return clients, err
-	}
-
-	// Get all the rows in the Sheet1.
-	rows, err := file.GetRows("Клиенты")
-	if err != nil {
-		fmt.Println(err)
-		return clients, err
-	}
-
-	// Пропускаем заголовок и обрабатываем каждую строку
-	for i, row := range rows {
-		if i == 0 {
-			continue
-		}
-		client := repo.ClientInfo{}
-		client.Id2 = row[0]
-		if len(row) > 1 {
-			client.Mark = row[1]
-		}
-		if len(row) > 2 {
-			if row[2] == "Да" {
-				client.Contractor = true
-			} else {
-				client.Contractor = false
-			}
-		}
-		if len(row) > 3 {
-			client.FullName = row[3]
-		}
-		if len(row) > 4 {
-			client.Type = row[4]
-		}
-		if len(row) > 5 {
-			if row[5] != "" {
-				phones := strings.Split(row[5], ",")
-				client.Phones = phones
-				// fmt.Println("PHones:", client.Phones)
-			}
-		}
-		if len(row) > 6 {
-			client.Email = row[6]
-		}
-		if len(row) > 7 {
-			client.LegalAddress = row[7]
-		}
-		if len(row) > 8 {
-			client.PhysicalAddress = row[8]
-		}
-		if len(row) > 9 {
-			if row[9] != "" {
-				client.RegistrationDate = row[9]
-			}
-		}
-		if len(row) > 10 {
-			client.AdChannel = row[10]
-		}
-		if len(row) > 11 {
-			client.RegData1 = row[11]
-		}
-		if len(row) > 12 {
-			client.RegData2 = row[12]
-		}
-		if len(row) > 13 {
-			client.Note = row[13]
-		}
-		if len(row) > 14 {
-			if row[14] != "" {
-				value, err := strconv.Atoi(row[14])
-				if err != nil {
-					fmt.Println("Error string to int:", err)
-					return clients, err
-				}
-				client.RequestCount = value
-			}
-		}
-		if len(row) > 15 {
-			client.Birthday = row[15]
-		}
-		if len(row) > 16 {
-			str := row[16]
-			if str != "" {
-				if strings.Contains(str, ".") {
-					res := strings.ReplaceAll(row[16], ".", "")
-					parse, _ := strconv.ParseInt(res, 10, 64)
-					client.Income = common.Money(parse)
-				} else {
-					parse, _ := strconv.ParseInt(str, 10, 64)
-					client.Income = common.Money(parse * 100)
-				}
-			}
-		}
-		// fmt.Println("Client:", client)
-		clients = append(clients, client)
-	}
-
-	return clients, err
-}
-
-func main() {
-	portEnv := os.Getenv("PORT")
-	url := "http://localhost" + portEnv + "/api/v1/client"
-	// fmt.Println(url)
-
-	if len(os.Args) > 1 {
-		FILE_PATH = os.Args[1]
-		clients, err := ReadClientsFromFile(FILE_PATH)
-		if err != nil {
-			fmt.Println(err)
-		}
-
-		for _, client := range clients {
-			// fmt.Println(client)
-			err = clientApi.PostData[repo.ClientInfo](url, client)
-			if err != nil {
-				fmt.Println(err)
-			}
-		}
-	}
-}

+ 0 - 50
src/cmd/server/main.go

@@ -1,50 +0,0 @@
-package main
-
-import (
-	"context"
-	"log"
-	"net/http"
-	"os"
-	"os/signal"
-	"saura/src/app"
-	myServer "saura/src/server"
-	"syscall"
-	"time"
-)
-
-func main() {
-	log.Println("Server running...")
-
-	appCtx := new(app.App)
-	app.Init(appCtx)
-
-	serverCtx := new(myServer.Server)
-	http_server := myServer.Init(serverCtx, appCtx)
-
-	// Channel to listen for OS signals
-	quit := make(chan os.Signal, 1)
-	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
-
-	// Start the HTTP server in a goroutine
-	go func() {
-		if err := http_server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
-			log.Fatalf("Server failed to listen and serve: %v", err)
-		}
-	}()
-	log.Println("Server started on " + appCtx.Port)
-
-	// Block until a signal is received
-	<-quit
-	log.Println("Shutting down server...")
-
-	// Create a context with a timeout for shutdown
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	// Attempt to gracefully shut down the server
-	if err := http_server.Shutdown(ctx); err != nil {
-		log.Fatalf("Server shutdown failed: %v", err)
-	}
-
-	log.Println("Server gracefully shut down.")
-}

+ 0 - 11
src/common/common.go

@@ -1,11 +0,0 @@
-package common
-
-import "fmt"
-
-type Money int64
-
-func (m Money) String() string {
-	rubles := int64(m) / 100
-	kopeks := int64(m) % 100
-	return fmt.Sprintf("%d.%02d", rubles, kopeks)
-}

+ 56 - 0
src/saura/app/app.cpp

@@ -0,0 +1,56 @@
+#include "app.hpp"
+
+#include "saura/core/app_base/app_base.hpp"
+
+namespace saura {
+App::App() {
+  is_running = true;
+
+  os_ctx = std::make_unique<OS>();
+  os_ctx->set_root_window_title(get_app_base()->get_title());
+
+  server_api_ctx = std::make_shared<ServerAPI>();
+}
+
+App::~App() {}
+
+void App::run() {
+  const double fps_max = 60.0;
+  const double period_max = 1.0 / fps_max;
+  const double perf_frequency = os_ctx->get_performance_frequency();
+
+  double time = 0.0;
+  double begin_counter = 0.0;
+  double end_counter = 0.0;
+
+  get_app_base()->start();
+
+  while (is_running) {
+    double counter_elapsed = begin_counter - end_counter;
+    double dt = counter_elapsed / perf_frequency;
+    double fps = perf_frequency / counter_elapsed;
+
+    begin_counter = os_ctx->get_performance_counter();
+
+    if (dt >= period_max) {
+      if (dt >= 1.0) {
+        dt = period_max;
+      }
+
+      if (!os_ctx->update_events()) {
+        is_running = false;
+      }
+      os_ctx->update();
+      os_ctx->draw_begin();
+      get_app_base()->update(dt);
+      os_ctx->draw_end();
+
+      end_counter = begin_counter;
+    }
+
+    os_ctx->sleep(period_max);
+  }
+
+  get_app_base()->stop();
+}
+} // namespace saura

+ 27 - 0
src/saura/app/app.hpp

@@ -0,0 +1,27 @@
+#ifndef SAURA_APP_HPP_
+#define SAURA_APP_HPP_
+
+#include "saura/core/os/os.hpp"
+#include "saura/core/server/server.hpp"
+
+#include <SDL3/SDL_render.h>
+#include <SDL3/SDL_video.h>
+#include <memory>
+
+namespace saura {
+class App {
+private:
+  std::unique_ptr<OS> os_ctx;
+  std::shared_ptr<ServerAPI> server_api_ctx;
+
+  bool is_running;
+
+public:
+  App();
+  ~App();
+
+  void run();
+};
+} // namespace saura
+
+#endif

+ 7 - 0
src/saura/cmd/sandbox/CMakeLists.txt

@@ -0,0 +1,7 @@
+get_filename_component(TARGET_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME)
+
+file(GLOB SOURCES "*.cpp")
+
+add_executable(${TARGET_NAME} ${SOURCES})
+
+target_link_libraries(${TARGET_NAME} PRIVATE ${PROJECT_NAME}_lib)

+ 28 - 0
src/saura/cmd/sandbox/sandbox.cpp

@@ -0,0 +1,28 @@
+#include <saura/core/app_base/app_base.hpp>
+
+#include <spdlog/spdlog.h>
+
+#include <imgui.h>
+#include <imgui_internal.h>
+
+namespace saura {
+struct SandboxApp : AppBase {
+  void start() override { spdlog::info("Sandbox start!"); }
+
+  void update(double dt) override {
+      ImGui::SetNextWindowPos({0.0f, 0.0f});
+      ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize);
+      ImGui::Begin("Demo", 0, ImGuiWindowFlags_NoDecoration);
+
+      ImGui::Text("%s", this->get_title().c_str());
+
+      ImGui::End();
+  }
+
+  void stop() override { spdlog::info("Sandbox stop!"); }
+
+  const std::string get_title() const override { return "Sandbox (Dev)"; }
+};
+
+AppBase *g_app_base_instance = new SandboxApp();
+} // namespace saura

+ 27 - 0
src/saura/core/app_base/app_base.hpp

@@ -0,0 +1,27 @@
+#pragma once
+
+#include <spdlog/spdlog.h>
+
+namespace saura {
+struct AppBase;
+
+// Эта глобальная переменная будет переопределена линковщиком в каждом
+// cmd-проекте
+extern AppBase *g_app_base_instance;
+
+struct AppBase {
+  virtual ~AppBase() = default;
+  virtual void start() {}
+  virtual void update(double dt) {}
+  virtual void stop() {}
+  virtual const std::string get_title() const { return g_app_base_instance->get_title(); }
+};
+
+inline AppBase *get_app_base() {
+  if (!g_app_base_instance) {
+    spdlog::error("g_app_instance не определён!");
+    std::terminate();
+  }
+  return g_app_base_instance;
+}
+} // namespace saura

+ 178 - 0
src/saura/core/os/os.cpp

@@ -0,0 +1,178 @@
+#include "os.hpp"
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_oldnames.h>
+#include <SDL3/SDL_video.h>
+
+#include <imgui.h>
+#include <imgui_impl_sdl3.h>
+#include <imgui_impl_sdlrenderer3.h>
+
+#include <filesystem>
+#include <locale>
+#include <string>
+
+namespace saura {
+OS::OS() {
+  root_window_ctx = std::make_unique<Window_Context>();
+  root_window_ctx->title = "(Saura Studios) [Dev]";
+  root_window_ctx->w = 1280;
+  root_window_ctx->h = 720;
+
+  std::locale::global(std::locale("en_US.UTF-8"));
+
+  if (!SDL_Init(SDL_INIT_VIDEO)) {
+    throw std::runtime_error("Failed SDL_Init!");
+  }
+
+  SDL_WindowFlags window_flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN;
+  root_window_ctx->handle =
+      SDL_CreateWindow(root_window_ctx->title.c_str(), root_window_ctx->w,
+                       root_window_ctx->h, window_flags);
+  if (root_window_ctx->handle == nullptr) {
+    throw std::runtime_error("Failed SDL_CreateWindow!");
+  }
+
+  root_window_ctx->renderer =
+      SDL_CreateRenderer(root_window_ctx->handle, nullptr);
+  if (root_window_ctx->renderer == nullptr) {
+    throw std::runtime_error("Failed SDL_CreateRenderer!");
+  }
+  SDL_SetRenderVSync(root_window_ctx->renderer, SDL_RENDERER_VSYNC_DISABLED);
+  SDL_SetWindowPosition(root_window_ctx->handle, SDL_WINDOWPOS_CENTERED,
+                        SDL_WINDOWPOS_CENTERED);
+  SDL_ShowWindow(root_window_ctx->handle);
+
+  // Setup imgui
+  IMGUI_CHECKVERSION();
+  ImGui::CreateContext();
+  ImGuiIO &io = ImGui::GetIO();
+  (void)io;
+  io.ConfigFlags |=
+      ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
+  io.ConfigFlags |=
+      ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
+
+  // Setup Dear ImGui style
+  ImGui::StyleColorsDark();
+
+  // Setup Platform/Renderer backends
+  ImGui_ImplSDL3_InitForSDLRenderer(root_window_ctx->handle,
+                                    root_window_ctx->renderer);
+  ImGui_ImplSDLRenderer3_Init(root_window_ctx->renderer);
+}
+
+OS::~OS() {
+  ImGui_ImplSDLRenderer3_Shutdown();
+  ImGui_ImplSDL3_Shutdown();
+  ImGui::DestroyContext();
+
+  SDL_DestroyRenderer(root_window_ctx->renderer);
+  SDL_DestroyWindow(root_window_ctx->handle);
+  SDL_Quit();
+}
+
+void OS::draw_begin() {
+  ImGui_ImplSDLRenderer3_NewFrame();
+  ImGui_ImplSDL3_NewFrame();
+  ImGui::NewFrame();
+}
+
+void OS::draw_end() {
+  ImVec4 clear_color = {0.15f, 0.15f, 0.15f, 0.0f};
+  ImGuiIO &io = ImGui::GetIO();
+  (void)io;
+  ImGui::Render();
+  SDL_SetRenderScale(root_window_ctx->renderer, io.DisplayFramebufferScale.x,
+                     io.DisplayFramebufferScale.y);
+  SDL_SetRenderDrawColorFloat(root_window_ctx->renderer, clear_color.x,
+                              clear_color.y, clear_color.z, clear_color.w);
+  SDL_RenderClear(root_window_ctx->renderer);
+  ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(),
+                                        root_window_ctx->renderer);
+
+  SDL_RenderPresent(root_window_ctx->renderer);
+}
+
+void OS::update() {
+  int w, h = 0;
+  SDL_GetWindowSize(root_window_ctx->handle, &w, &h);
+  root_window_ctx->w = w;
+  root_window_ctx->h = h;
+}
+
+bool OS::update_events() {
+  SDL_Event event = {};
+  while (SDL_PollEvent(&event)) {
+
+    ImGui_ImplSDL3_ProcessEvent(&event);
+
+    switch (event.type) {
+    case SDL_EVENT_KEY_UP: {
+      if (event.key.key == SDLK_ESCAPE) {
+        return false;
+      }
+      break;
+    }
+    case SDL_EVENT_QUIT: {
+      return false;
+    }
+    case SDL_EVENT_WINDOW_CLOSE_REQUESTED: {
+      if (event.window.windowID == SDL_GetWindowID(root_window_ctx->handle)) {
+        return false;
+      }
+      break;
+    }
+    }
+  }
+  return true;
+}
+
+void OS::sleep(double ms) { SDL_Delay((Uint32)ms * 1000); }
+
+int OS::get_root_window_width() { return root_window_ctx->w; }
+
+int OS::get_root_window_height() { return root_window_ctx->h; }
+
+void OS::set_root_window_title(std::string title) {
+    SDL_SetWindowTitle(root_window_ctx.get()->handle, title.c_str());
+}
+
+double OS::get_performance_frequency() {
+  return (double)SDL_GetPerformanceFrequency();
+}
+
+double OS::get_performance_counter() {
+  return (double)SDL_GetPerformanceCounter();
+}
+
+std::string OS::get_safe_getenv(const std::string key) {
+  return SDL_getenv(key.c_str());
+}
+
+fs::path OS::get_home_config_path() {
+  fs::path res = {};
+
+#if defined(_WIN32)
+  auto config = get_safe_getenv("APPDATA");
+
+  if (config.empty()) {
+    throw std::runtime_error("APPDATA environment variable not found");
+  }
+
+  res = fs::path(config);
+#elif defined(__linux__)
+  auto config = get_safe_getenv("HOME");
+
+  if (config.empty()) {
+    throw std::runtime_error("HOME environment variable not found");
+  }
+
+  res = fs::path(config) / ".config";
+#endif
+
+  return res;
+}
+
+std::string OS::get_user_name() { return get_safe_getenv("USERNAME"); }
+} // namespace saura

+ 49 - 0
src/saura/core/os/os.hpp

@@ -0,0 +1,49 @@
+#ifndef SAURA_OS_HPP_
+#define SAURA_OS_HPP_
+
+#include <cstdint>
+#include <filesystem>
+
+#include <SDL3/SDL_render.h>
+#include <SDL3/SDL_video.h>
+
+namespace fs = std::filesystem;
+
+namespace saura {
+class OS {
+  struct Window_Context {
+    uint32_t w, h;
+    std::string title;
+    SDL_Window *handle;
+    SDL_Renderer *renderer;
+  };
+
+private:
+  std::unique_ptr<Window_Context> root_window_ctx;
+
+public:
+  OS();
+  ~OS();
+
+  void draw_begin();
+  void draw_end();
+
+  void update();
+  bool update_events();
+  void sleep(double ms);
+
+  int get_root_window_width();
+  int get_root_window_height();
+
+  void set_root_window_title(std::string title);
+
+  double get_performance_frequency();
+  double get_performance_counter();
+
+  static std::string get_safe_getenv(const std::string key);
+  static fs::path get_home_config_path();
+  static std::string get_user_name();
+};
+} // namespace saura
+
+#endif

+ 32 - 0
src/saura/core/server/server.cpp

@@ -0,0 +1,32 @@
+#include "server.hpp"
+#include <chrono>
+#include <cpr/api.h>
+#include <cpr/cprtypes.h>
+#include <cpr/timeout.h>
+#include <thread>
+
+namespace saura {
+ServerAPI::ServerAPI() {
+  worker = std::thread([this]() { check_is_live(); });
+}
+
+ServerAPI::~ServerAPI() {
+  if (worker.joinable())
+    worker.join();
+}
+
+void ServerAPI::check_is_live() {
+  while (true) {
+    auto res =
+        cpr::Get(cpr::Url{"http://localhost:8090/ping"}, cpr::Timeout{1000});
+    if (res.status_code == 200) {
+      is_live = true;
+    } else {
+      is_live = false;
+    }
+    std::this_thread::sleep_for(std::chrono::seconds(1));
+  }
+}
+
+bool ServerAPI::get_is_live() { return is_live; }
+} // namespace saura

+ 20 - 0
src/saura/core/server/server.hpp

@@ -0,0 +1,20 @@
+#pragma once
+
+#include <atomic>
+#include <thread>
+
+namespace saura {
+class ServerAPI {
+private:
+  std::atomic<bool> is_live;
+  std::thread worker;
+
+public:
+  ServerAPI();
+  ~ServerAPI();
+
+  void check_is_live();
+
+  bool get_is_live();
+};
+}; // namespace saura

+ 9 - 0
src/saura/main/main.cpp

@@ -0,0 +1,9 @@
+#include "saura/app/app.hpp"
+
+int main() {
+  auto *app_ctx = new (saura::App);
+  app_ctx->run();
+  // TODO: вызывает вылет!
+  // delete app_ctx;
+  return 0;
+}

+ 0 - 59
src/server/client/client_api.go

@@ -1,59 +0,0 @@
-package clientApi
-
-import (
-	"bytes"
-	"encoding/json"
-	"fmt"
-	"net/http"
-)
-
-func PostData[T any](url string, data any) error {
-	jsonData, err := json.Marshal(data)
-	if err != nil {
-		return err
-	}
-
-	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != http.StatusOK {
-		return fmt.Errorf("failed to add: %s", resp.Status)
-	}
-
-	return nil
-}
-
-func FetchData[T any](url string, data T) error {
-	resp, err := http.Get(url)
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != http.StatusOK {
-		return fmt.Errorf("failed to fetch data: status code %d", resp.StatusCode)
-	}
-
-	return json.NewDecoder(resp.Body).Decode(data)
-}
-
-func FetchOne[T any](params string) (data T, err error) {
-	url := params
-	err = FetchData(url, &data)
-	if err != nil {
-		return data, err
-	}
-	return data, nil
-}
-
-func Fetch[T any](params string) (data []T, err error) {
-	url := params
-	err = FetchData(url, &data)
-	if err != nil {
-		return nil, err
-	}
-	return data, nil
-}

+ 0 - 119
src/server/db/db.go

@@ -1,119 +0,0 @@
-package db
-
-import (
-	"database/sql"
-	"fmt"
-	"log"
-	"os"
-	"saura/src/server/db/repo"
-	"sync"
-
-	_ "github.com/mattn/go-sqlite3"
-)
-
-type DB struct {
-	db          *sql.DB
-	clientsRepo *repo.ClientRepo
-}
-
-var (
-	instance *DB
-	once     sync.Once
-)
-
-func (ctx *DB) Init() error {
-	dbURL := os.Getenv("DB_URL")
-	if dbURL == "" {
-		return fmt.Errorf("DB_URL environment variable is not set")
-	}
-
-	var err error
-	ctx.db, err = sql.Open("sqlite3", dbURL)
-
-	if err != nil {
-		return fmt.Errorf("failed to open database: %w", err)
-	}
-
-	if err := ctx.db.Ping(); err != nil {
-		ctx.db.Close()
-		return fmt.Errorf("failed to connect to database: %w", err)
-	}
-
-	ctx.clientsRepo = repo.NewClientRepo(ctx.db, dbURL)
-
-	log.Println("Database initialized successfully")
-	return nil
-}
-
-func (ctx *DB) Close() error {
-	if ctx.db != nil {
-		return ctx.db.Close()
-	}
-	return nil
-}
-
-func (ctx *DB) GetClientRepo() *repo.ClientRepo {
-	return ctx.clientsRepo
-}
-
-func (ctx *DB) FetchMarks() ([]string, error) {
-	query := `
-      SELECT id, name
-      FROM marks
-  `
-	rows, err := ctx.db.Query(query)
-	if err != nil {
-		return nil, fmt.Errorf("failed to execute query: %w", err)
-	}
-	defer rows.Close()
-
-	var marks []string
-
-	for rows.Next() {
-		var id string
-		var name string
-
-		if err := rows.Scan(&id, &name); err != nil {
-			return nil, fmt.Errorf("failed to scan row: %w", err)
-		}
-
-		marks = append(marks, name)
-	}
-
-	if err := rows.Err(); err != nil {
-		return nil, fmt.Errorf("error during iteration: %w", err)
-	}
-
-	return marks, nil
-}
-
-func (ctx *DB) FetchAdChannels() ([]string, error) {
-	query := `
-      SELECT id, name
-      FROM ad_channels
-  `
-	rows, err := ctx.db.Query(query)
-	if err != nil {
-		return nil, fmt.Errorf("failed to execute query: %w", err)
-	}
-	defer rows.Close()
-
-	var ad_channels []string
-
-	for rows.Next() {
-		var id string
-		var name string
-
-		if err := rows.Scan(&id, &name); err != nil {
-			return nil, fmt.Errorf("failed to scan row: %w", err)
-		}
-
-		ad_channels = append(ad_channels, name)
-	}
-
-	if err := rows.Err(); err != nil {
-		return nil, fmt.Errorf("error during iteration: %w", err)
-	}
-
-	return ad_channels, nil
-}

+ 0 - 291
src/server/db/repo/client.go

@@ -1,291 +0,0 @@
-package repo
-
-import (
-	"database/sql"
-	"fmt"
-	"saura/src/common"
-)
-
-const (
-	ClientTypePhysical     string = "Физ"
-	ClientTypeLegal        string = "Юр"
-	ClientTypeSupplier     string = "Поставщик"
-	ClientTypeEmployee     string = "Сотрудник"
-	ClientTypeCounterparty string = "Контрагент"
-	ClientTypeCustomer     string = "Покупатель"
-)
-
-type ClientInfo struct {
-	Id               string       `db:"id" json:"id" form:"id"`
-	Id2              string       `db:"id2" json:"id2" form:"id2"`
-	Mark             string       `db:"mark" json:"mark" form:"mark"`
-	Contractor       bool         `db:"contractor" json:"contractor" form:"contractor"`
-	FullName         string       `db:"full_name" json:"full_name" form:"full_name"`
-	Type             string       `db:"type" json:"type" form:"type"`
-	Phones           []string     `json:"phones" form:"phones"`
-	Email            string       `db:"email" json:"email" form:"email"`
-	LegalAddress     string       `db:"legal_address" json:"legal_address" form:"legal_address"`
-	PhysicalAddress  string       `db:"physical_address" json:"physical_address" form:"physical_address"`
-	RegistrationDate string       `db:"registration_date" json:"registration_date" form:"registration_date"`
-	AdChannel        string       `db:"ad_channel" json:"ad_channel" form:"ad_channel"`
-	RegData1         string       `db:"reg_data_1" json:"reg_data_1" form:"reg_data_1"`
-	RegData2         string       `db:"reg_data_2" json:"reg_data_2" form:"reg_data_2"`
-	Note             string       `db:"note" json:"note" form:"note"`
-	RequestCount     int          `db:"request_count" json:"request_count" form:"request_count"`
-	Birthday         string       `db:"birthday" json:"birthday" form:"birthday"`
-	Income           common.Money `db:"income" json:"income" form:"income"`
-}
-
-type ClientRepo struct {
-	db    *sql.DB
-	dbUrl string
-}
-
-func NewClientRepo(db *sql.DB, dbUrl string) *ClientRepo {
-	return &ClientRepo{db: db, dbUrl: dbUrl}
-}
-
-func (r *ClientRepo) InsertClient(client ClientInfo) error {
-	var err error
-	var exists bool
-
-	err = r.db.QueryRow("SELECT EXISTS(SELECT 1 FROM clients WHERE id = ? OR id2 = ?)", client.Id, client.Id2).Scan(&exists)
-	if err != nil {
-		return fmt.Errorf("failed to check if client exists: %w", err)
-	}
-	if exists {
-		return fmt.Errorf("client with id %s already exists", client.Id)
-	}
-
-	_, err = r.db.Exec(
-		`
-    INSERT INTO clients (
-      id, id2, mark, contractor, full_name, type, email,
-      legal_address, physical_address, registration_date, ad_channel,
-      reg_data_1, reg_data_2, note, request_count, birthday, income
-    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-    `,
-		client.Id, client.Id2, client.Mark, client.Contractor, client.FullName,
-		client.Type, client.Email, client.LegalAddress,
-		client.PhysicalAddress, client.RegistrationDate, client.AdChannel,
-		client.RegData1, client.RegData2, client.Note, client.RequestCount,
-		client.Birthday, client.Income,
-	)
-	if err != nil {
-		return fmt.Errorf("failed to insert client: %w", err)
-	}
-
-	return nil
-}
-
-func (r *ClientRepo) FetchClientByID(id string) (*ClientInfo, error) {
-	var err error
-
-	row := r.db.QueryRow(`
-		SELECT
-    	id, id2, mark, contractor, full_name, type, email,
-      legal_address, physical_address, registration_date, ad_channel,
-      reg_data_1, reg_data_2, note, request_count, birthday, income
-    FROM clients
-    WHERE id = ?
-    `, id)
-
-	var client ClientInfo
-	err = row.Scan(
-		&client.Id, &client.Id2, &client.Mark, &client.Contractor, &client.FullName,
-		&client.Type, &client.Email, &client.LegalAddress,
-		&client.PhysicalAddress, &client.RegistrationDate, &client.AdChannel,
-		&client.RegData1, &client.RegData2, &client.Note, &client.RequestCount,
-		&client.Birthday, &client.Income,
-	)
-
-	if err == sql.ErrNoRows {
-		return nil, fmt.Errorf("client with id %s not found", id)
-	} else if err != nil {
-		return nil, fmt.Errorf("failed to scan row: %w", err)
-	}
-
-	phones, err := r.FetchClientPhones(client.Id)
-	if err != nil {
-		return nil, fmt.Errorf("error: %w", err)
-	}
-	client.Phones = phones
-
-	return &client, nil
-}
-
-func (r *ClientRepo) InsertClientPhones(client_id string, phones []string) error {
-	for _, phone := range phones {
-		_, err := r.db.Exec("INSERT INTO client_phones VALUES(?, ?)", client_id, phone)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (r *ClientRepo) FetchClientPhones(client_id string) ([]string, error) {
-	rows, err := r.db.Query("SELECT phone FROM client_phones WHERE client_id = ?", client_id)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	var tags []string
-	for rows.Next() {
-		var tag string
-		rows.Scan(&tag)
-		tags = append(tags, tag)
-	}
-	return tags, nil
-}
-
-func (r *ClientRepo) FetchClients(count int, offset int) ([]ClientInfo, error) {
-	rows, err := r.db.Query(`
-        SELECT
-            id, id2, mark, contractor, full_name, type, email,
-            legal_address, physical_address, registration_date, ad_channel,
-            reg_data_1, reg_data_2, note, request_count, birthday, income
-        FROM clients
-        LIMIT ? OFFSET ?
-    `, count, offset)
-	if err != nil {
-		return nil, fmt.Errorf("failed to execute query: %w", err)
-	}
-	defer rows.Close()
-
-	var clients []ClientInfo
-	for rows.Next() {
-		var client ClientInfo
-		err := rows.Scan(
-			&client.Id,
-			&client.Id2,
-			&client.Mark,
-			&client.Contractor,
-			&client.FullName,
-			&client.Type,
-			&client.Email,
-			&client.LegalAddress,
-			&client.PhysicalAddress,
-			&client.RegistrationDate, // time.Time или sql.NullTime
-			&client.AdChannel,
-			&client.RegData1,
-			&client.RegData2,
-			&client.Note,         // sql.NullString
-			&client.RequestCount, // int или sql.NullInt32
-			&client.Birthday,     // time.Time или sql.NullTime
-			&client.Income,       // float64 или sql.NullFloat64
-		)
-		if err != nil {
-			return nil, fmt.Errorf("failed to scan row: %w", err)
-		}
-
-		phones, err := r.FetchClientPhones(client.Id)
-		if err != nil {
-			return nil, fmt.Errorf("rows error: %w", err)
-		}
-		client.Phones = phones
-
-		clients = append(clients, client)
-	}
-
-	// Проверка ошибок после итерации
-	if err := rows.Err(); err != nil {
-		return nil, fmt.Errorf("rows error: %w", err)
-	}
-
-	return clients, nil
-}
-
-func (r *ClientRepo) FetchClientsCount() (int, error) {
-	var count int
-	query := "SELECT COUNT(*) FROM clients"
-	err := r.db.QueryRow(query).Scan(&count)
-	if err != nil {
-		return 0, fmt.Errorf("failed to fetch count: %w", err)
-	}
-	return count, nil
-}
-
-func (r *ClientRepo) DeleteClient(id string) error {
-	query := "DELETE FROM clients WHERE id = ?"
-	result, err := r.db.Exec(query, id)
-	if err != nil {
-		return fmt.Errorf("failed to delete client: %w", err)
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return fmt.Errorf("failed to get rows affected: %w", err)
-	}
-
-	if rowsAffected == 0 {
-		return fmt.Errorf("client with id %s not found", id)
-	}
-
-	return nil
-}
-
-func (r *ClientRepo) UpdateClient(id string, client ClientInfo) error {
-	// Проверим, существует ли клиент
-	var exists string
-	err := r.db.QueryRow("SELECT id FROM clients WHERE id = ?", id).Scan(&exists)
-	if err == sql.ErrNoRows {
-		return fmt.Errorf("client with id %s not found", id)
-	} else if err != nil {
-		return fmt.Errorf("failed to check client existence: %w", err)
-	}
-
-	query := `
-        UPDATE clients SET
-            id = ?,
-            id2 = ?,
-            mark = ?,
-            contractor = ?,
-            full_name = ?,
-            type = ?,
-            email = ?,
-            legal_address = ?,
-            physical_address = ?,
-            registration_date = ?,
-            ad_channel = ?,
-            reg_data_1 = ?,
-            reg_data_2 = ?,
-            note = ?,
-            request_count = ?,
-            birthday = ?,
-            income = ?
-        WHERE id = ?
-    `
-
-	contractorInt := 0
-	if client.Contractor {
-		contractorInt = 1
-	}
-
-	_, err = r.db.Exec(query,
-		id,
-		client.Id2,
-		client.Mark,
-		contractorInt,
-		client.FullName,
-		client.Type,
-		client.Email,
-		client.LegalAddress,
-		client.PhysicalAddress,
-		client.RegistrationDate,
-		client.AdChannel,
-		client.RegData1,
-		client.RegData2,
-		client.Note,
-		client.RequestCount,
-		client.Birthday,
-		client.Income,
-		id,
-	)
-	if err != nil {
-		return fmt.Errorf("failed to update client: %w", err)
-	}
-
-	return nil
-}

+ 0 - 75
src/server/server.go

@@ -1,75 +0,0 @@
-package server
-
-import (
-	"net/http"
-	"strconv"
-	"strings"
-	"time"
-
-	"saura/src/app"
-	"saura/src/server/db"
-
-	"github.com/labstack/echo/v4"
-	"github.com/labstack/echo/v4/middleware"
-
-	_ "github.com/joho/godotenv/autoload"
-)
-
-type Server struct {
-	db *db.DB
-}
-
-func Init(ctx *Server, appCtx *app.App) *http.Server {
-	ctx.db = new(db.DB)
-	ctx.db.Init()
-
-	http_server := &http.Server{
-		Addr:         appCtx.Port,
-		Handler:      RegisterHandlers(ctx.db),
-		IdleTimeout:  time.Minute,
-		ReadTimeout:  10 * time.Second,
-		WriteTimeout: 30 * time.Second,
-	}
-
-	return http_server
-}
-
-func RegisterHandlers(db *db.DB) http.Handler {
-	e := echo.New()
-	e.Use(middleware.RequestLogger())
-	e.Use(middleware.Recover())
-
-	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
-		AllowOrigins:     []string{"https://*", "http://*"},
-		AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
-		AllowHeaders:     []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
-		AllowCredentials: true,
-		MaxAge:           300,
-	}))
-
-	apiGroup := e.Group("", func(next echo.HandlerFunc) echo.HandlerFunc {
-		return func(c echo.Context) error {
-			if strings.HasPrefix(c.Request().Host, "api.") {
-				return next(c)
-			}
-			return echo.ErrNotFound
-		}
-	})
-
-	e.GET("/", func(c echo.Context) error {
-		return c.String(http.StatusOK, "is live")
-	})
-
-	apiGroup.GET("/v1/clients", func(c echo.Context) error {
-		limit, _ := strconv.Atoi(c.QueryParam("limit"))
-		offset, _ := strconv.Atoi(c.QueryParam("offset"))
-
-		clients, err := db.GetClientRepo().FetchClients(limit, offset)
-		if err != nil {
-			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
-		}
-		return c.JSON(http.StatusOK, clients)
-	})
-
-	return e
-}

+ 15 - 0
vcpkg.json

@@ -0,0 +1,15 @@
+{
+  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
+  "name": "bookmarks",
+  "version": "0.1.0",
+  "dependencies": [
+    {
+      "name": "imgui",
+      "features": ["sdl3-binding", "sdl3-renderer-binding"]
+    },
+    "spdlog",
+    "sdl3",
+    "glm",
+    "cpr"
+  ]
+}