From c39bde0b10440cc5cd8e1aa96a794c416d6a0ac1 Mon Sep 17 00:00:00 2001 From: Egor Pozharov Date: Mon, 4 May 2026 17:32:03 +0600 Subject: [PATCH] add warehouse_request_source and warehouse_request_source_2 fields to deliveries table with validation and normalization logic --- ...0005_add_warehouse_request_source.down.sql | 2 + ...000005_add_warehouse_request_source.up.sql | 2 + backend/internal/db/queries/query.sql | 36 ++--- backend/internal/db/sqlc/models.go | 44 +++--- backend/internal/db/sqlc/query.sql.go | 126 +++++++++------- backend/internal/delivery/handler.go | 135 +++++++++++------- frontend/src/api/deliveries.ts | 10 +- .../src/components/delivery/DeliveryCard.tsx | 6 +- .../src/components/delivery/DeliveryForm.tsx | 71 +++++++-- .../src/components/delivery/DeliveryRow.tsx | 6 +- frontend/src/constants/pickup.ts | 10 +- frontend/src/pages/Dashboard.tsx | 4 +- frontend/src/types/index.ts | 18 +++ 13 files changed, 305 insertions(+), 165 deletions(-) create mode 100644 backend/internal/db/migrations/000005_add_warehouse_request_source.down.sql create mode 100644 backend/internal/db/migrations/000005_add_warehouse_request_source.up.sql diff --git a/backend/internal/db/migrations/000005_add_warehouse_request_source.down.sql b/backend/internal/db/migrations/000005_add_warehouse_request_source.down.sql new file mode 100644 index 0000000..cccb5ee --- /dev/null +++ b/backend/internal/db/migrations/000005_add_warehouse_request_source.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE deliveries DROP COLUMN warehouse_request_source_2; +ALTER TABLE deliveries DROP COLUMN warehouse_request_source; diff --git a/backend/internal/db/migrations/000005_add_warehouse_request_source.up.sql b/backend/internal/db/migrations/000005_add_warehouse_request_source.up.sql new file mode 100644 index 0000000..abc84ad --- /dev/null +++ b/backend/internal/db/migrations/000005_add_warehouse_request_source.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE deliveries ADD COLUMN warehouse_request_source varchar(20); +ALTER TABLE deliveries ADD COLUMN warehouse_request_source_2 varchar(20); diff --git a/backend/internal/db/queries/query.sql b/backend/internal/db/queries/query.sql index 7754678..1335435 100644 --- a/backend/internal/db/queries/query.sql +++ b/backend/internal/db/queries/query.sql @@ -30,11 +30,11 @@ SELECT * FROM deliveries WHERE date = $1; -- name: CreateDelivery :one INSERT INTO deliveries ( - date, pickup_location, pickup_location_2, product_name, product_name_2, + date, pickup_location, pickup_location_2, warehouse_request_source, warehouse_request_source_2, product_name, product_name_2, customer_name, address, street, house, apartment, entrance, floor, phone, additional_phone, has_elevator, service_info, comment ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING *; -- name: GetDeliveryByID :one @@ -48,22 +48,24 @@ UPDATE deliveries SET date = $1, pickup_location = $2, pickup_location_2 = $3, - product_name = $4, - product_name_2 = $5, - customer_name = $6, - address = $7, - street = $8, - house = $9, - apartment = $10, - entrance = $11, - floor = $12, - phone = $13, - additional_phone = $14, - has_elevator = $15, - service_info = $16, - comment = $17, + warehouse_request_source = $4, + warehouse_request_source_2 = $5, + product_name = $6, + product_name_2 = $7, + customer_name = $8, + address = $9, + street = $10, + house = $11, + apartment = $12, + entrance = $13, + floor = $14, + phone = $15, + additional_phone = $16, + has_elevator = $17, + service_info = $18, + comment = $19, updated_at = NOW() -WHERE id = $18; +WHERE id = $20; -- name: GetDeliveryCount :many SELECT COUNT(*) as count, date FROM deliveries diff --git a/backend/internal/db/sqlc/models.go b/backend/internal/db/sqlc/models.go index aed54ee..c1816f2 100644 --- a/backend/internal/db/sqlc/models.go +++ b/backend/internal/db/sqlc/models.go @@ -9,27 +9,29 @@ import ( ) 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"` - CustomerName string `db:"customer_name" json:"customer_name"` - ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` - PickupLocation2 pgtype.Text `db:"pickup_location_2" json:"pickup_location_2"` - ProductName2 pgtype.Text `db:"product_name_2" json:"product_name_2"` - Street string `db:"street" json:"street"` - House string `db:"house" json:"house"` - Apartment pgtype.Text `db:"apartment" json:"apartment"` - Entrance pgtype.Text `db:"entrance" json:"entrance"` - Floor pgtype.Text `db:"floor" json:"floor"` + 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"` + CustomerName string `db:"customer_name" json:"customer_name"` + ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` + PickupLocation2 pgtype.Text `db:"pickup_location_2" json:"pickup_location_2"` + ProductName2 pgtype.Text `db:"product_name_2" json:"product_name_2"` + Street string `db:"street" json:"street"` + House string `db:"house" json:"house"` + Apartment pgtype.Text `db:"apartment" json:"apartment"` + Entrance pgtype.Text `db:"entrance" json:"entrance"` + Floor pgtype.Text `db:"floor" json:"floor"` + WarehouseRequestSource pgtype.Text `db:"warehouse_request_source" json:"warehouse_request_source"` + WarehouseRequestSource2 pgtype.Text `db:"warehouse_request_source_2" json:"warehouse_request_source_2"` } type User struct { diff --git a/backend/internal/db/sqlc/query.sql.go b/backend/internal/db/sqlc/query.sql.go index 72d8ae4..e4b1c39 100644 --- a/backend/internal/db/sqlc/query.sql.go +++ b/backend/internal/db/sqlc/query.sql.go @@ -13,32 +13,34 @@ import ( const createDelivery = `-- name: CreateDelivery :one INSERT INTO deliveries ( - date, pickup_location, pickup_location_2, product_name, product_name_2, + date, pickup_location, pickup_location_2, warehouse_request_source, warehouse_request_source_2, product_name, product_name_2, customer_name, address, street, house, apartment, entrance, floor, phone, additional_phone, has_elevator, service_info, comment ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) -RETURNING id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at, customer_name, service_info, pickup_location_2, product_name_2, street, house, apartment, entrance, floor +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) +RETURNING id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at, customer_name, service_info, pickup_location_2, product_name_2, street, house, apartment, entrance, floor, warehouse_request_source, warehouse_request_source_2 ` type CreateDeliveryParams struct { - Date pgtype.Date `db:"date" json:"date"` - PickupLocation string `db:"pickup_location" json:"pickup_location"` - PickupLocation2 pgtype.Text `db:"pickup_location_2" json:"pickup_location_2"` - ProductName string `db:"product_name" json:"product_name"` - ProductName2 pgtype.Text `db:"product_name_2" json:"product_name_2"` - CustomerName string `db:"customer_name" json:"customer_name"` - Address string `db:"address" json:"address"` - Street string `db:"street" json:"street"` - House string `db:"house" json:"house"` - Apartment pgtype.Text `db:"apartment" json:"apartment"` - Entrance pgtype.Text `db:"entrance" json:"entrance"` - Floor pgtype.Text `db:"floor" json:"floor"` - Phone string `db:"phone" json:"phone"` - AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"` - HasElevator bool `db:"has_elevator" json:"has_elevator"` - ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` - Comment pgtype.Text `db:"comment" json:"comment"` + Date pgtype.Date `db:"date" json:"date"` + PickupLocation string `db:"pickup_location" json:"pickup_location"` + PickupLocation2 pgtype.Text `db:"pickup_location_2" json:"pickup_location_2"` + WarehouseRequestSource pgtype.Text `db:"warehouse_request_source" json:"warehouse_request_source"` + WarehouseRequestSource2 pgtype.Text `db:"warehouse_request_source_2" json:"warehouse_request_source_2"` + ProductName string `db:"product_name" json:"product_name"` + ProductName2 pgtype.Text `db:"product_name_2" json:"product_name_2"` + CustomerName string `db:"customer_name" json:"customer_name"` + Address string `db:"address" json:"address"` + Street string `db:"street" json:"street"` + House string `db:"house" json:"house"` + Apartment pgtype.Text `db:"apartment" json:"apartment"` + Entrance pgtype.Text `db:"entrance" json:"entrance"` + Floor pgtype.Text `db:"floor" json:"floor"` + Phone string `db:"phone" json:"phone"` + AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"` + HasElevator bool `db:"has_elevator" json:"has_elevator"` + ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` + Comment pgtype.Text `db:"comment" json:"comment"` } func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams) (Delivery, error) { @@ -46,6 +48,8 @@ func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams) arg.Date, arg.PickupLocation, arg.PickupLocation2, + arg.WarehouseRequestSource, + arg.WarehouseRequestSource2, arg.ProductName, arg.ProductName2, arg.CustomerName, @@ -84,6 +88,8 @@ func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams) &i.Apartment, &i.Entrance, &i.Floor, + &i.WarehouseRequestSource, + &i.WarehouseRequestSource2, ) return i, err } @@ -123,7 +129,7 @@ func (q *Queries) DeleteDelivery(ctx context.Context, id pgtype.UUID) error { } const getDeliveriesByDate = `-- name: GetDeliveriesByDate :many -SELECT id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at, customer_name, service_info, pickup_location_2, product_name_2, street, house, apartment, entrance, floor FROM deliveries WHERE date = $1 +SELECT id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at, customer_name, service_info, pickup_location_2, product_name_2, street, house, apartment, entrance, floor, warehouse_request_source, warehouse_request_source_2 FROM deliveries WHERE date = $1 ` func (q *Queries) GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([]Delivery, error) { @@ -157,6 +163,8 @@ func (q *Queries) GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([] &i.Apartment, &i.Entrance, &i.Floor, + &i.WarehouseRequestSource, + &i.WarehouseRequestSource2, ); err != nil { return nil, err } @@ -169,7 +177,7 @@ func (q *Queries) GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([] } const getDeliveryByID = `-- name: GetDeliveryByID :one -SELECT id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at, customer_name, service_info, pickup_location_2, product_name_2, street, house, apartment, entrance, floor FROM deliveries WHERE id = $1 +SELECT id, date, pickup_location, product_name, address, phone, additional_phone, has_elevator, comment, status, created_at, updated_at, customer_name, service_info, pickup_location_2, product_name_2, street, house, apartment, entrance, floor, warehouse_request_source, warehouse_request_source_2 FROM deliveries WHERE id = $1 ` func (q *Queries) GetDeliveryByID(ctx context.Context, id pgtype.UUID) (Delivery, error) { @@ -197,6 +205,8 @@ func (q *Queries) GetDeliveryByID(ctx context.Context, id pgtype.UUID) (Delivery &i.Apartment, &i.Entrance, &i.Floor, + &i.WarehouseRequestSource, + &i.WarehouseRequestSource2, ) return i, err } @@ -293,43 +303,47 @@ UPDATE deliveries SET date = $1, pickup_location = $2, pickup_location_2 = $3, - product_name = $4, - product_name_2 = $5, - customer_name = $6, - address = $7, - street = $8, - house = $9, - apartment = $10, - entrance = $11, - floor = $12, - phone = $13, - additional_phone = $14, - has_elevator = $15, - service_info = $16, - comment = $17, + warehouse_request_source = $4, + warehouse_request_source_2 = $5, + product_name = $6, + product_name_2 = $7, + customer_name = $8, + address = $9, + street = $10, + house = $11, + apartment = $12, + entrance = $13, + floor = $14, + phone = $15, + additional_phone = $16, + has_elevator = $17, + service_info = $18, + comment = $19, updated_at = NOW() -WHERE id = $18 +WHERE id = $20 ` type UpdateDeliveryParams struct { - Date pgtype.Date `db:"date" json:"date"` - PickupLocation string `db:"pickup_location" json:"pickup_location"` - PickupLocation2 pgtype.Text `db:"pickup_location_2" json:"pickup_location_2"` - ProductName string `db:"product_name" json:"product_name"` - ProductName2 pgtype.Text `db:"product_name_2" json:"product_name_2"` - CustomerName string `db:"customer_name" json:"customer_name"` - Address string `db:"address" json:"address"` - Street string `db:"street" json:"street"` - House string `db:"house" json:"house"` - Apartment pgtype.Text `db:"apartment" json:"apartment"` - Entrance pgtype.Text `db:"entrance" json:"entrance"` - Floor pgtype.Text `db:"floor" json:"floor"` - Phone string `db:"phone" json:"phone"` - AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"` - HasElevator bool `db:"has_elevator" json:"has_elevator"` - ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` - Comment pgtype.Text `db:"comment" json:"comment"` - ID pgtype.UUID `db:"id" json:"id"` + Date pgtype.Date `db:"date" json:"date"` + PickupLocation string `db:"pickup_location" json:"pickup_location"` + PickupLocation2 pgtype.Text `db:"pickup_location_2" json:"pickup_location_2"` + WarehouseRequestSource pgtype.Text `db:"warehouse_request_source" json:"warehouse_request_source"` + WarehouseRequestSource2 pgtype.Text `db:"warehouse_request_source_2" json:"warehouse_request_source_2"` + ProductName string `db:"product_name" json:"product_name"` + ProductName2 pgtype.Text `db:"product_name_2" json:"product_name_2"` + CustomerName string `db:"customer_name" json:"customer_name"` + Address string `db:"address" json:"address"` + Street string `db:"street" json:"street"` + House string `db:"house" json:"house"` + Apartment pgtype.Text `db:"apartment" json:"apartment"` + Entrance pgtype.Text `db:"entrance" json:"entrance"` + Floor pgtype.Text `db:"floor" json:"floor"` + Phone string `db:"phone" json:"phone"` + AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"` + HasElevator bool `db:"has_elevator" json:"has_elevator"` + ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` + Comment pgtype.Text `db:"comment" json:"comment"` + ID pgtype.UUID `db:"id" json:"id"` } func (q *Queries) UpdateDelivery(ctx context.Context, arg UpdateDeliveryParams) error { @@ -337,6 +351,8 @@ func (q *Queries) UpdateDelivery(ctx context.Context, arg UpdateDeliveryParams) arg.Date, arg.PickupLocation, arg.PickupLocation2, + arg.WarehouseRequestSource, + arg.WarehouseRequestSource2, arg.ProductName, arg.ProductName2, arg.CustomerName, diff --git a/backend/internal/delivery/handler.go b/backend/internal/delivery/handler.go index cf5ce30..f0a0a75 100644 --- a/backend/internal/delivery/handler.go +++ b/backend/internal/delivery/handler.go @@ -1,6 +1,7 @@ package delivery import ( + "errors" "net/http" "time" @@ -16,23 +17,25 @@ type Handler struct { // 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"` - PickupLocation2 *string `json:"pickup_location_2" binding:"omitempty,oneof=warehouse symbat nursaya galaktika"` - ProductName string `json:"product_name" binding:"required"` - ProductName2 *string `json:"product_name_2"` - CustomerName string `json:"customer_name" binding:"required"` - Address string `json:"address" binding:"required"` - Street string `json:"street" binding:"required"` - House string `json:"house" binding:"required"` - Apartment *string `json:"apartment"` - Entrance *string `json:"entrance"` - Floor *string `json:"floor"` - Phone string `json:"phone" binding:"required"` - AdditionalPhone *string `json:"additional_phone"` - HasElevator bool `json:"has_elevator"` - ServiceInfo *string `json:"service_info"` - Comment string `json:"comment"` + Date string `json:"date" binding:"required"` // DD-MM-YYYY + PickupLocation string `json:"pickup_location" binding:"required,oneof=warehouse symbat nursaya galaktika"` + PickupLocation2 *string `json:"pickup_location_2" binding:"omitempty,oneof=warehouse symbat nursaya galaktika"` + WarehouseRequestSource *string `json:"warehouse_request_source" binding:"omitempty,oneof=symbat nursaya galaktika"` + WarehouseRequestSource2 *string `json:"warehouse_request_source_2" binding:"omitempty,oneof=symbat nursaya galaktika"` + ProductName string `json:"product_name" binding:"required"` + ProductName2 *string `json:"product_name_2"` + CustomerName string `json:"customer_name" binding:"required"` + Address string `json:"address" binding:"required"` + Street string `json:"street" binding:"required"` + House string `json:"house" binding:"required"` + Apartment *string `json:"apartment"` + Entrance *string `json:"entrance"` + Floor *string `json:"floor"` + Phone string `json:"phone" binding:"required"` + AdditionalPhone *string `json:"additional_phone"` + HasElevator bool `json:"has_elevator"` + ServiceInfo *string `json:"service_info"` + Comment string `json:"comment"` } func NewHandler(queries *sqlc.Queries) *Handler { @@ -97,6 +100,10 @@ func (h *Handler) CreateDelivery(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } + if err := normalizeWarehouseRequestSources(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } // Parse date from DD-MM-YYYY t, err := parseDate(req.Date) @@ -106,23 +113,25 @@ func (h *Handler) CreateDelivery(c *gin.Context) { } params := sqlc.CreateDeliveryParams{ - Date: pgtype.Date{Time: t, Valid: true}, - PickupLocation: req.PickupLocation, - PickupLocation2: pgtype.Text{String: derefString(req.PickupLocation2), Valid: req.PickupLocation2 != nil}, - ProductName: req.ProductName, - ProductName2: pgtype.Text{String: derefString(req.ProductName2), Valid: req.ProductName2 != nil}, - CustomerName: req.CustomerName, - Address: req.Address, - Street: req.Street, - House: req.House, - Apartment: pgtype.Text{String: derefString(req.Apartment), Valid: req.Apartment != nil}, - Entrance: pgtype.Text{String: derefString(req.Entrance), Valid: req.Entrance != nil}, - Floor: pgtype.Text{String: derefString(req.Floor), Valid: req.Floor != nil}, - Phone: req.Phone, - AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, - HasElevator: req.HasElevator, - ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, - Comment: pgtype.Text{String: req.Comment, Valid: true}, + Date: pgtype.Date{Time: t, Valid: true}, + PickupLocation: req.PickupLocation, + PickupLocation2: pgtype.Text{String: derefString(req.PickupLocation2), Valid: req.PickupLocation2 != nil}, + WarehouseRequestSource: pgtype.Text{String: derefString(req.WarehouseRequestSource), Valid: req.WarehouseRequestSource != nil}, + WarehouseRequestSource2: pgtype.Text{String: derefString(req.WarehouseRequestSource2), Valid: req.WarehouseRequestSource2 != nil}, + ProductName: req.ProductName, + ProductName2: pgtype.Text{String: derefString(req.ProductName2), Valid: req.ProductName2 != nil}, + CustomerName: req.CustomerName, + Address: req.Address, + Street: req.Street, + House: req.House, + Apartment: pgtype.Text{String: derefString(req.Apartment), Valid: req.Apartment != nil}, + Entrance: pgtype.Text{String: derefString(req.Entrance), Valid: req.Entrance != nil}, + Floor: pgtype.Text{String: derefString(req.Floor), Valid: req.Floor != nil}, + Phone: req.Phone, + AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, + HasElevator: req.HasElevator, + ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, + Comment: pgtype.Text{String: req.Comment, Valid: true}, } res, err := h.queries.CreateDelivery(c.Request.Context(), params) if err != nil { @@ -141,6 +150,10 @@ func (h *Handler) UpdateDelivery(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } + if err := normalizeWarehouseRequestSources(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } id := c.Param("id") @@ -161,24 +174,26 @@ func (h *Handler) UpdateDelivery(c *gin.Context) { } 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, - PickupLocation2: pgtype.Text{String: derefString(req.PickupLocation2), Valid: req.PickupLocation2 != nil}, - ProductName: req.ProductName, - ProductName2: pgtype.Text{String: derefString(req.ProductName2), Valid: req.ProductName2 != nil}, - CustomerName: req.CustomerName, - Address: req.Address, - Street: req.Street, - House: req.House, - Apartment: pgtype.Text{String: derefString(req.Apartment), Valid: req.Apartment != nil}, - Entrance: pgtype.Text{String: derefString(req.Entrance), Valid: req.Entrance != nil}, - Floor: pgtype.Text{String: derefString(req.Floor), Valid: req.Floor != nil}, - Phone: req.Phone, - AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, - HasElevator: req.HasElevator, - ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, - Comment: pgtype.Text{String: req.Comment, Valid: true}, + ID: pgtype.UUID{Bytes: parsedID, Valid: true}, + Date: pgtype.Date{Time: t, Valid: true}, + PickupLocation: req.PickupLocation, + PickupLocation2: pgtype.Text{String: derefString(req.PickupLocation2), Valid: req.PickupLocation2 != nil}, + WarehouseRequestSource: pgtype.Text{String: derefString(req.WarehouseRequestSource), Valid: req.WarehouseRequestSource != nil}, + WarehouseRequestSource2: pgtype.Text{String: derefString(req.WarehouseRequestSource2), Valid: req.WarehouseRequestSource2 != nil}, + ProductName: req.ProductName, + ProductName2: pgtype.Text{String: derefString(req.ProductName2), Valid: req.ProductName2 != nil}, + CustomerName: req.CustomerName, + Address: req.Address, + Street: req.Street, + House: req.House, + Apartment: pgtype.Text{String: derefString(req.Apartment), Valid: req.Apartment != nil}, + Entrance: pgtype.Text{String: derefString(req.Entrance), Valid: req.Entrance != nil}, + Floor: pgtype.Text{String: derefString(req.Floor), Valid: req.Floor != nil}, + Phone: req.Phone, + AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, + HasElevator: req.HasElevator, + ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, + 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 @@ -266,3 +281,19 @@ func derefString(s *string) string { } return *s } + +func normalizeWarehouseRequestSources(req *DeliveryRequest) error { + if req.PickupLocation == "warehouse" && req.WarehouseRequestSource == nil { + return errors.New("warehouse_request_source is required when pickup_location is warehouse") + } + if req.PickupLocation != "warehouse" { + req.WarehouseRequestSource = nil + } + if req.PickupLocation2 != nil && *req.PickupLocation2 == "warehouse" && req.WarehouseRequestSource2 == nil { + return errors.New("warehouse_request_source_2 is required when pickup_location_2 is warehouse") + } + if req.PickupLocation2 == nil || *req.PickupLocation2 != "warehouse" { + req.WarehouseRequestSource2 = nil + } + return nil +} diff --git a/frontend/src/api/deliveries.ts b/frontend/src/api/deliveries.ts index ac4bc41..0808707 100644 --- a/frontend/src/api/deliveries.ts +++ b/frontend/src/api/deliveries.ts @@ -1,6 +1,6 @@ import { api } from './client'; import { backendDateToFrontend } from '../utils/date'; -import type { Delivery, PickupLocation, DeliveryStatus } from '../types'; +import type { Delivery, DeliveryRequestSource, PickupLocation, DeliveryStatus } from '../types'; // Types matching backend responses interface BackendDelivery { @@ -8,6 +8,8 @@ interface BackendDelivery { date: string; // YYYY-MM-DD from pgtype.Date pickup_location: PickupLocation; pickup_location_2: PickupLocation | null; + warehouse_request_source: DeliveryRequestSource | null; + warehouse_request_source_2: DeliveryRequestSource | null; product_name: string; product_name_2: string | null; customer_name: string; @@ -61,6 +63,8 @@ function mapBackendToFrontend(backend: BackendDelivery): Delivery { date: backendDateToFrontend(backend.date), pickupLocation: backend.pickup_location, pickupLocation2: backend.pickup_location_2 || undefined, + warehouseRequestSource: backend.warehouse_request_source || undefined, + warehouseRequestSource2: backend.warehouse_request_source_2 || undefined, productName: backend.product_name, productName2: backend.product_name_2 || undefined, customerName: backend.customer_name, @@ -115,6 +119,8 @@ export const deliveriesApi = { date: data.date, pickup_location: data.pickupLocation, pickup_location_2: data.pickupLocation2 || null, + warehouse_request_source: data.pickupLocation === 'warehouse' ? data.warehouseRequestSource || null : null, + warehouse_request_source_2: data.pickupLocation2 === 'warehouse' ? data.warehouseRequestSource2 || null : null, product_name: data.productName, product_name_2: data.productName2 || null, customer_name: data.customerName, @@ -143,6 +149,8 @@ export const deliveriesApi = { date: data.date, pickup_location: data.pickupLocation, pickup_location_2: data.pickupLocation2 || null, + warehouse_request_source: data.pickupLocation === 'warehouse' ? data.warehouseRequestSource || null : null, + warehouse_request_source_2: data.pickupLocation2 === 'warehouse' ? data.warehouseRequestSource2 || null : null, product_name: data.productName, product_name_2: data.productName2 || null, customer_name: data.customerName, diff --git a/frontend/src/components/delivery/DeliveryCard.tsx b/frontend/src/components/delivery/DeliveryCard.tsx index 94522df..1715e3c 100644 --- a/frontend/src/components/delivery/DeliveryCard.tsx +++ b/frontend/src/components/delivery/DeliveryCard.tsx @@ -1,7 +1,7 @@ import { memo } from 'react'; import { MapPin, Phone, Store, Calendar, MessageSquare, CheckCircle2, Circle, CheckSquare, User, Wrench } from 'lucide-react'; import type { Delivery } from '../../types'; -import { pickupLocationLabels } from '../../types'; +import { formatPickupLocation } from '../../types'; import { StatusBadge } from './StatusBadge'; import { Card } from '../ui/Card'; @@ -63,13 +63,13 @@ export const DeliveryCard = memo(({ delivery, onStatusChange, onEdit, onDelete }
- {pickupLocationLabels[delivery.pickupLocation]} + {formatPickupLocation(delivery.pickupLocation, delivery.warehouseRequestSource)} {delivery.productName}
{delivery.pickupLocation2 && (
- {pickupLocationLabels[delivery.pickupLocation2]} + {formatPickupLocation(delivery.pickupLocation2, delivery.warehouseRequestSource2)} {delivery.productName2 || '—'}
diff --git a/frontend/src/components/delivery/DeliveryForm.tsx b/frontend/src/components/delivery/DeliveryForm.tsx index 50db571..e2a88de 100644 --- a/frontend/src/components/delivery/DeliveryForm.tsx +++ b/frontend/src/components/delivery/DeliveryForm.tsx @@ -1,8 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; import { Button, Input, Select, Modal } from '../ui'; -import { pickupOptions } from '../../constants/pickup'; +import { deliveryRequestSourceOptions, pickupOptions } from '../../constants/pickup'; import { formatDateForInput, parseDateFromInput, getTodayFrontend } from '../../utils/date'; -import type { Delivery, PickupLocation, DeliveryStatus } from '../../types'; +import type { Delivery, DeliveryRequestSource, PickupLocation, DeliveryStatus } from '../../types'; interface DeliveryFormProps { isOpen: boolean; @@ -18,6 +18,10 @@ const PHONE_REGEX = /^\+7\s?\(?\d{3}\)?\s?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/; // City is not shown in UI but is included in the saved address (used for 2GIS search). const CITY_LABEL = 'Кокшетау'; +const requestSourceOptions = [ + { value: '', label: 'Выберите источник заявки' }, + ...deliveryRequestSourceOptions, +]; const buildAddressString = ( street: string, @@ -38,6 +42,8 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa date: defaultDate || getTodayFrontend(), pickupLocation: 'warehouse' as PickupLocation, pickupLocation2: null as PickupLocation | null, + warehouseRequestSource: null as DeliveryRequestSource | null, + warehouseRequestSource2: null as DeliveryRequestSource | null, productName: '', productName2: '', customerName: '', @@ -62,6 +68,8 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa date: initialData.date, pickupLocation: initialData.pickupLocation, pickupLocation2: initialData.pickupLocation2 || null, + warehouseRequestSource: initialData.warehouseRequestSource || null, + warehouseRequestSource2: initialData.warehouseRequestSource2 || null, productName: initialData.productName, productName2: initialData.productName2 || '', customerName: initialData.customerName, @@ -91,7 +99,25 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa const isPhoneValid = !formData.phone || validatePhone(formData.phone); const isAdditionalPhoneValid = !formData.additionalPhone || validatePhone(formData.additionalPhone); - const isFormValid = formData.productName && formData.phone && isPhoneValid && formData.customerName && formData.street && formData.house; + const isWarehouseRequestSourceValid = formData.pickupLocation !== 'warehouse' || !!formData.warehouseRequestSource; + const isWarehouseRequestSource2Valid = !showSecondPickup || formData.pickupLocation2 !== 'warehouse' || !!formData.warehouseRequestSource2; + const isFormValid = formData.productName && formData.phone && isPhoneValid && formData.customerName && formData.street && formData.house && isWarehouseRequestSourceValid && isWarehouseRequestSource2Valid; + + const handlePickupLocationChange = (pickupLocation: PickupLocation) => { + setFormData({ + ...formData, + pickupLocation, + warehouseRequestSource: pickupLocation === 'warehouse' ? formData.warehouseRequestSource : null, + }); + }; + + const handlePickupLocation2Change = (pickupLocation2: PickupLocation) => { + setFormData({ + ...formData, + pickupLocation2, + warehouseRequestSource2: pickupLocation2 === 'warehouse' ? formData.warehouseRequestSource2 : null, + }); + }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -107,6 +133,8 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa date: defaultDate || getTodayFrontend(), pickupLocation: 'warehouse', pickupLocation2: null, + warehouseRequestSource: null, + warehouseRequestSource2: null, productName: '', productName2: '', customerName: '', @@ -165,10 +193,21 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa setFormData({ ...formData, warehouseRequestSource: e.target.value ? e.target.value as DeliveryRequestSource : null })} + options={requestSourceOptions} + required + error={!isWarehouseRequestSourceValid ? 'Выберите, от кого исходит заявка' : undefined} + /> + )} + { - setShowSecondPickup(e.target.checked); - if (!e.target.checked) { - setFormData({ ...formData, pickupLocation2: null, productName2: '' }); - } + const checked = e.target.checked; + setShowSecondPickup(checked); + setFormData({ + ...formData, + pickupLocation2: checked ? formData.pickupLocation2 || 'warehouse' : null, + warehouseRequestSource2: checked ? formData.warehouseRequestSource2 : null, + productName2: checked ? formData.productName2 : '', + }); }} className="w-4 h-4 text-[#1B263B] border-[#c5c6cd] rounded focus:ring-[#1B263B]" /> @@ -305,9 +348,19 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa setFormData({ ...formData, warehouseRequestSource2: e.target.value ? e.target.value as DeliveryRequestSource : null })} + options={requestSourceOptions} + required + error={!isWarehouseRequestSource2Valid ? 'Выберите, от кого исходит заявка' : undefined} + /> + )} {delivery.date} {delivery.pickupLocation2 - ? `${pickupLocationLabels[delivery.pickupLocation]} + ${pickupLocationLabels[delivery.pickupLocation2]}` - : pickupLocationLabels[delivery.pickupLocation]} + ? `${formatPickupLocation(delivery.pickupLocation, delivery.warehouseRequestSource)} + ${formatPickupLocation(delivery.pickupLocation2, delivery.warehouseRequestSource2)}` + : formatPickupLocation(delivery.pickupLocation, delivery.warehouseRequestSource)} {delivery.productName} diff --git a/frontend/src/constants/pickup.ts b/frontend/src/constants/pickup.ts index 2226b12..a95d17a 100644 --- a/frontend/src/constants/pickup.ts +++ b/frontend/src/constants/pickup.ts @@ -1,5 +1,5 @@ -import type { PickupLocation } from '../types'; -import { pickupLocationLabels } from '../types'; +import type { DeliveryRequestSource, PickupLocation } from '../types'; +import { deliveryRequestSourceLabels, pickupLocationLabels } from '../types'; export const pickupOptions: { value: PickupLocation; label: string }[] = [ { value: 'warehouse', label: pickupLocationLabels.warehouse }, @@ -12,3 +12,9 @@ export const pickupFilterOptions: { value: PickupLocation | 'all'; label: string { value: 'all', label: 'Все места загрузки' }, ...pickupOptions, ]; + +export const deliveryRequestSourceOptions: { value: DeliveryRequestSource; label: string }[] = [ + { value: 'symbat', label: deliveryRequestSourceLabels.symbat }, + { value: 'nursaya', label: deliveryRequestSourceLabels.nursaya }, + { value: 'galaktika', label: deliveryRequestSourceLabels.galaktika }, +]; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index bbf7e41..fff6bf3 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -4,7 +4,7 @@ import { format, startOfMonth, endOfMonth, eachDayOfInterval, isToday, getDay } import { ru } from 'date-fns/locale'; import { useDeliveryStore } from '../stores/deliveryStore'; import type { Delivery } from '../types'; -import { pickupLocationLabels } from '../types'; +import { formatPickupLocation } from '../types'; import { Button } from '../components/ui/Button'; import { Card } from '../components/ui/Card'; @@ -78,7 +78,7 @@ const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => { ${dayDeliveries.map((d: Delivery) => ` - ${d.pickupLocation2 ? pickupLocationLabels[d.pickupLocation] + ' + ' + pickupLocationLabels[d.pickupLocation2] : pickupLocationLabels[d.pickupLocation]} + ${d.pickupLocation2 ? formatPickupLocation(d.pickupLocation, d.warehouseRequestSource) + ' + ' + formatPickupLocation(d.pickupLocation2, d.warehouseRequestSource2) : formatPickupLocation(d.pickupLocation, d.warehouseRequestSource)} ${d.productName}${d.productName2 ? '
+ ' + d.productName2 + '' : ''} ${d.customerName} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 083fcda..4dfd3f2 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,4 +1,5 @@ export type PickupLocation = 'warehouse' | 'symbat' | 'nursaya' | 'galaktika'; +export type DeliveryRequestSource = 'symbat' | 'nursaya' | 'galaktika'; export type DeliveryStatus = 'new' | 'delivered'; export interface Delivery { @@ -6,6 +7,8 @@ export interface Delivery { date: string; // DD-MM-YYYY pickupLocation: PickupLocation; pickupLocation2?: PickupLocation | null; + warehouseRequestSource?: DeliveryRequestSource | null; + warehouseRequestSource2?: DeliveryRequestSource | null; productName: string; productName2?: string | null; customerName: string; @@ -32,6 +35,21 @@ export const pickupLocationLabels: Record = { galaktika: 'Галактика', }; +export const deliveryRequestSourceLabels: Record = { + symbat: 'Сымбат', + nursaya: 'Нурсая', + galaktika: 'Галактика', +}; + +export const formatPickupLocation = ( + pickupLocation: PickupLocation, + warehouseRequestSource?: DeliveryRequestSource | null, +): string => { + if (pickupLocation !== 'warehouse' || !warehouseRequestSource) { + return pickupLocationLabels[pickupLocation]; + } + return `${pickupLocationLabels[pickupLocation]} · от ${deliveryRequestSourceLabels[warehouseRequestSource]}`; +}; export const statusLabels: Record = { new: 'Новое',