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:
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS deliveries;
|
||||
@@ -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()
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -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
|
||||
);
|
||||
16
backend/internal/db/queries/query.sql
Normal file
16
backend/internal/db/queries/query.sql
Normal 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;
|
||||
32
backend/internal/db/sqlc/db.go
Normal file
32
backend/internal/db/sqlc/db.go
Normal 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,
|
||||
}
|
||||
}
|
||||
31
backend/internal/db/sqlc/models.go
Normal file
31
backend/internal/db/sqlc/models.go
Normal 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"`
|
||||
}
|
||||
21
backend/internal/db/sqlc/querier.go
Normal file
21
backend/internal/db/sqlc/querier.go
Normal 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)
|
||||
159
backend/internal/db/sqlc/query.sql.go
Normal file
159
backend/internal/db/sqlc/query.sql.go
Normal 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
|
||||
}
|
||||
181
backend/internal/delivery/handler.go
Normal file
181
backend/internal/delivery/handler.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user