chore: restructure project into backend and frontend folders

- Move all frontend code to frontend/ directory
- Add backend/ with Go project structure (cmd, internal, pkg)
- Add docker-compose.yml for orchestration
This commit is contained in:
Egor Pozharov
2026-04-14 13:14:28 +06:00
parent 11e12f964d
commit 4e0899d3ce
54 changed files with 779 additions and 0 deletions

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS deliveries;

View File

@@ -0,0 +1,14 @@
CREATE TABLE deliveries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
date date NOT NULL, -- хранить как date, отдавать как DD-MM-YYYY
pickup_location varchar(20) NOT NULL, -- warehouse|symbat|nursaya|galaktika
product_name text NOT NULL,
address text NOT NULL,
phone varchar(30) NOT NULL,
additional_phone varchar(30),
has_elevator boolean NOT NULL DEFAULT false,
comment text,
status varchar(20) NOT NULL DEFAULT 'new', -- new|delivered
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);

View File

@@ -0,0 +1 @@
DROP TABLE IF EXISTS users;

View File

@@ -0,0 +1,6 @@
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -0,0 +1,16 @@
-- name: GetDeliveriesByDate :many
SELECT * FROM deliveries WHERE date = $1;
-- name: CreateDelivery :one
INSERT INTO deliveries (date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: GetDeliveryByID :one
SELECT * FROM deliveries WHERE id = $1;
-- name: DeleteDelivery :exec
DELETE FROM deliveries WHERE id = $1;
-- name: UpdateDelivery :exec
UPDATE deliveries SET date = $1, pickup_location = $2, product_name = $3, address = $4, phone = $5, additional_phone = $6, has_elevator = $7, comment = $8, updated_at = NOW() WHERE id = $9;

View File

@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Delivery struct {
ID pgtype.UUID `db:"id" json:"id"`
Date pgtype.Date `db:"date" json:"date"`
PickupLocation string `db:"pickup_location" json:"pickup_location"`
ProductName string `db:"product_name" json:"product_name"`
Address string `db:"address" json:"address"`
Phone string `db:"phone" json:"phone"`
AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"`
HasElevator bool `db:"has_elevator" json:"has_elevator"`
Comment pgtype.Text `db:"comment" json:"comment"`
Status string `db:"status" json:"status"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type User struct {
ID pgtype.UUID `db:"id" json:"id"`
Username string `db:"username" json:"username"`
PasswordHash string `db:"password_hash" json:"password_hash"`
CreatedAt pgtype.Timestamp `db:"created_at" json:"created_at"`
}

View File

@@ -0,0 +1,21 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type Querier interface {
CreateDelivery(ctx context.Context, arg CreateDeliveryParams) (Delivery, error)
DeleteDelivery(ctx context.Context, id pgtype.UUID) error
GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([]Delivery, error)
GetDeliveryByID(ctx context.Context, id pgtype.UUID) (Delivery, error)
UpdateDelivery(ctx context.Context, arg UpdateDeliveryParams) error
}
var _ Querier = (*Queries)(nil)

View File

@@ -0,0 +1,159 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: query.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createDelivery = `-- name: CreateDelivery :one
INSERT INTO deliveries (date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at
`
type CreateDeliveryParams struct {
Date pgtype.Date `db:"date" json:"date"`
PickupLocation string `db:"pickup_location" json:"pickup_location"`
ProductName string `db:"product_name" json:"product_name"`
Address string `db:"address" json:"address"`
Phone string `db:"phone" json:"phone"`
AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"`
HasElevator bool `db:"has_elevator" json:"has_elevator"`
Comment pgtype.Text `db:"comment" json:"comment"`
}
func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams) (Delivery, error) {
row := q.db.QueryRow(ctx, createDelivery,
arg.Date,
arg.PickupLocation,
arg.ProductName,
arg.Address,
arg.Phone,
arg.AdditionalPhone,
arg.HasElevator,
arg.Comment,
)
var i Delivery
err := row.Scan(
&i.ID,
&i.Date,
&i.PickupLocation,
&i.ProductName,
&i.Address,
&i.Phone,
&i.AdditionalPhone,
&i.HasElevator,
&i.Comment,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteDelivery = `-- name: DeleteDelivery :exec
DELETE FROM deliveries WHERE id = $1
`
func (q *Queries) DeleteDelivery(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteDelivery, id)
return err
}
const getDeliveriesByDate = `-- name: GetDeliveriesByDate :many
SELECT id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at FROM deliveries WHERE date = $1
`
func (q *Queries) GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([]Delivery, error) {
rows, err := q.db.Query(ctx, getDeliveriesByDate, date)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Delivery{}
for rows.Next() {
var i Delivery
if err := rows.Scan(
&i.ID,
&i.Date,
&i.PickupLocation,
&i.ProductName,
&i.Address,
&i.Phone,
&i.AdditionalPhone,
&i.HasElevator,
&i.Comment,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getDeliveryByID = `-- name: GetDeliveryByID :one
SELECT id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at FROM deliveries WHERE id = $1
`
func (q *Queries) GetDeliveryByID(ctx context.Context, id pgtype.UUID) (Delivery, error) {
row := q.db.QueryRow(ctx, getDeliveryByID, id)
var i Delivery
err := row.Scan(
&i.ID,
&i.Date,
&i.PickupLocation,
&i.ProductName,
&i.Address,
&i.Phone,
&i.AdditionalPhone,
&i.HasElevator,
&i.Comment,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const updateDelivery = `-- name: UpdateDelivery :exec
UPDATE deliveries SET date = $1, pickup_location = $2, product_name = $3, address = $4, phone = $5, additional_phone = $6, has_elevator = $7, comment = $8, updated_at = NOW() WHERE id = $9
`
type UpdateDeliveryParams struct {
Date pgtype.Date `db:"date" json:"date"`
PickupLocation string `db:"pickup_location" json:"pickup_location"`
ProductName string `db:"product_name" json:"product_name"`
Address string `db:"address" json:"address"`
Phone string `db:"phone" json:"phone"`
AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"`
HasElevator bool `db:"has_elevator" json:"has_elevator"`
Comment pgtype.Text `db:"comment" json:"comment"`
ID pgtype.UUID `db:"id" json:"id"`
}
func (q *Queries) UpdateDelivery(ctx context.Context, arg UpdateDeliveryParams) error {
_, err := q.db.Exec(ctx, updateDelivery,
arg.Date,
arg.PickupLocation,
arg.ProductName,
arg.Address,
arg.Phone,
arg.AdditionalPhone,
arg.HasElevator,
arg.Comment,
arg.ID,
)
return err
}

View File

@@ -0,0 +1,181 @@
package delivery
import (
"net/http"
"time"
sqlc "github.com/chedius/delivery-tracker/internal/db/sqlc"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
type Handler struct {
queries *sqlc.Queries
}
// DeliveryRequest represents the request body for creating or updating a delivery
type DeliveryRequest struct {
Date string `json:"date" binding:"required"` // DD-MM-YYYY
PickupLocation string `json:"pickup_location" binding:"required,oneof=warehouse symbat nursaya galaktika"`
ProductName string `json:"product_name" binding:"required"`
Address string `json:"address" binding:"required"`
Phone string `json:"phone" binding:"required"`
AdditionalPhone string `json:"additional_phone"`
HasElevator bool `json:"has_elevator"`
Comment string `json:"comment"`
}
func NewHandler(queries *sqlc.Queries) *Handler {
return &Handler{queries: queries}
}
// GET /api/deliveries/:id
func (h *Handler) GetDeliveryByID(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"})
return
}
parsedID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid UUID format", "details": err.Error()})
return
}
delivery, err := h.queries.GetDeliveryByID(c.Request.Context(), pgtype.UUID{Bytes: parsedID, Valid: true})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get delivery", "details": err.Error(), "id": id, "bytes": [16]byte([]byte(id))})
return
}
c.JSON(http.StatusOK, gin.H{"delivery": delivery})
}
// GET /api/deliveries?date=DD-MM-YYYY
func (h *Handler) GetDeliveries(c *gin.Context) {
t, err := parseDate(c.Query("date"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format", "details": err.Error()})
return
}
date := pgtype.Date{Time: t, Valid: true}
deliveries, err := h.queries.GetDeliveriesByDate(c.Request.Context(), date)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get deliveries", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"deliveries": deliveries})
}
// POST /api/deliveries
func (h *Handler) CreateDelivery(c *gin.Context) {
var req DeliveryRequest = DeliveryRequest{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
// Parse date from DD-MM-YYYY
t, err := parseDate(req.Date)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format", "details": err.Error()})
return
}
params := sqlc.CreateDeliveryParams{
Date: pgtype.Date{Time: t, Valid: true},
PickupLocation: req.PickupLocation,
ProductName: req.ProductName,
Address: req.Address,
Phone: req.Phone,
AdditionalPhone: pgtype.Text{String: req.AdditionalPhone, Valid: req.AdditionalPhone != ""},
HasElevator: req.HasElevator,
Comment: pgtype.Text{String: req.Comment, Valid: true},
}
res, err := h.queries.CreateDelivery(c.Request.Context(), params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create delivery", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Delivery created", "id": res.ID.String()})
}
// PATCH /api/deliveries/:id
func (h *Handler) UpdateDelivery(c *gin.Context) {
var req DeliveryRequest = DeliveryRequest{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"})
return
}
parsedID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid UUID format", "details": err.Error()})
return
}
t, err := parseDate(req.Date)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid date format", "details": err.Error()})
return
}
if err := h.queries.UpdateDelivery(c.Request.Context(), sqlc.UpdateDeliveryParams{
ID: pgtype.UUID{Bytes: parsedID, Valid: true},
Date: pgtype.Date{Time: t, Valid: true},
PickupLocation: req.PickupLocation,
ProductName: req.ProductName,
Address: req.Address,
Phone: req.Phone,
AdditionalPhone: pgtype.Text{String: req.AdditionalPhone, Valid: req.AdditionalPhone != ""},
HasElevator: req.HasElevator,
Comment: pgtype.Text{String: req.Comment, Valid: true},
}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update delivery", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Delivery updated"})
}
// DELETE /api/deliveries/:id
func (h *Handler) DeleteDelivery(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID is required"})
return
}
parsedID, err := uuid.Parse(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid UUID format", "details": err.Error()})
return
}
if err := h.queries.DeleteDelivery(c.Request.Context(), pgtype.UUID{Bytes: parsedID, Valid: true}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete delivery", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Delivery deleted"})
}
func parseDate(dateStr string) (time.Time, error) {
t, err := time.Parse("02-01-2006", dateStr)
if err != nil {
return time.Time{}, err
}
return t, nil
}