From 906563c7c3ecc9573d9145867f17de74a463575b Mon Sep 17 00:00:00 2001 From: Egor Pozharov Date: Wed, 17 Jun 2026 13:57:54 +0500 Subject: [PATCH] add whatsapp contact button and prepayment/full payment badge --- .../000006_add_payment_status.down.sql | 1 + .../000006_add_payment_status.up.sql | 2 + backend/internal/db/queries/query.sql | 11 ++-- backend/internal/db/sqlc/models.go | 1 + backend/internal/db/sqlc/query.sql.go | 24 ++++--- backend/internal/delivery/handler.go | 11 ++++ frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/src/api/deliveries.ts | 6 +- .../src/components/delivery/DeliveryCard.tsx | 63 ++++++++++++++----- .../src/components/delivery/DeliveryForm.tsx | 17 ++++- .../src/components/delivery/DeliveryList.tsx | 1 + .../src/components/delivery/DeliveryRow.tsx | 35 ++++++++--- .../src/components/delivery/PaymentBadge.tsx | 25 ++++++++ frontend/src/components/ui/WhatsAppIcon.tsx | 17 +++++ frontend/src/types/index.ts | 7 +++ frontend/src/utils/phone.ts | 5 ++ 17 files changed, 191 insertions(+), 41 deletions(-) create mode 100644 backend/internal/db/migrations/000006_add_payment_status.down.sql create mode 100644 backend/internal/db/migrations/000006_add_payment_status.up.sql create mode 100644 frontend/src/components/delivery/PaymentBadge.tsx create mode 100644 frontend/src/components/ui/WhatsAppIcon.tsx create mode 100644 frontend/src/utils/phone.ts diff --git a/backend/internal/db/migrations/000006_add_payment_status.down.sql b/backend/internal/db/migrations/000006_add_payment_status.down.sql new file mode 100644 index 0000000..a553340 --- /dev/null +++ b/backend/internal/db/migrations/000006_add_payment_status.down.sql @@ -0,0 +1 @@ +ALTER TABLE deliveries DROP COLUMN payment_status; diff --git a/backend/internal/db/migrations/000006_add_payment_status.up.sql b/backend/internal/db/migrations/000006_add_payment_status.up.sql new file mode 100644 index 0000000..cf1e188 --- /dev/null +++ b/backend/internal/db/migrations/000006_add_payment_status.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE deliveries +ADD COLUMN payment_status varchar(20) NOT NULL DEFAULT 'prepaid'; diff --git a/backend/internal/db/queries/query.sql b/backend/internal/db/queries/query.sql index 1335435..6d6ac5f 100644 --- a/backend/internal/db/queries/query.sql +++ b/backend/internal/db/queries/query.sql @@ -32,9 +32,9 @@ SELECT * FROM deliveries WHERE date = $1; INSERT INTO deliveries ( 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 + phone, additional_phone, has_elevator, payment_status, service_info, comment ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING *; -- name: GetDeliveryByID :one @@ -62,10 +62,11 @@ UPDATE deliveries SET phone = $15, additional_phone = $16, has_elevator = $17, - service_info = $18, - comment = $19, + payment_status = $18, + service_info = $19, + comment = $20, updated_at = NOW() -WHERE id = $20; +WHERE id = $21; -- 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 c1816f2..c4738b5 100644 --- a/backend/internal/db/sqlc/models.go +++ b/backend/internal/db/sqlc/models.go @@ -32,6 +32,7 @@ type Delivery struct { 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"` + PaymentStatus string `db:"payment_status" json:"payment_status"` } type User struct { diff --git a/backend/internal/db/sqlc/query.sql.go b/backend/internal/db/sqlc/query.sql.go index e4b1c39..2f8bfe9 100644 --- a/backend/internal/db/sqlc/query.sql.go +++ b/backend/internal/db/sqlc/query.sql.go @@ -15,10 +15,10 @@ const createDelivery = `-- name: CreateDelivery :one INSERT INTO deliveries ( 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 + phone, additional_phone, has_elevator, payment_status, service_info, comment ) -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 +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) +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, payment_status ` type CreateDeliveryParams struct { @@ -39,6 +39,7 @@ type CreateDeliveryParams struct { Phone string `db:"phone" json:"phone"` AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"` HasElevator bool `db:"has_elevator" json:"has_elevator"` + PaymentStatus string `db:"payment_status" json:"payment_status"` ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` Comment pgtype.Text `db:"comment" json:"comment"` } @@ -62,6 +63,7 @@ func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams) arg.Phone, arg.AdditionalPhone, arg.HasElevator, + arg.PaymentStatus, arg.ServiceInfo, arg.Comment, ) @@ -90,6 +92,7 @@ func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams) &i.Floor, &i.WarehouseRequestSource, &i.WarehouseRequestSource2, + &i.PaymentStatus, ) return i, err } @@ -129,7 +132,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, warehouse_request_source, warehouse_request_source_2 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, payment_status FROM deliveries WHERE date = $1 ` func (q *Queries) GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([]Delivery, error) { @@ -165,6 +168,7 @@ func (q *Queries) GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([] &i.Floor, &i.WarehouseRequestSource, &i.WarehouseRequestSource2, + &i.PaymentStatus, ); err != nil { return nil, err } @@ -177,7 +181,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, warehouse_request_source, warehouse_request_source_2 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, payment_status FROM deliveries WHERE id = $1 ` func (q *Queries) GetDeliveryByID(ctx context.Context, id pgtype.UUID) (Delivery, error) { @@ -207,6 +211,7 @@ func (q *Queries) GetDeliveryByID(ctx context.Context, id pgtype.UUID) (Delivery &i.Floor, &i.WarehouseRequestSource, &i.WarehouseRequestSource2, + &i.PaymentStatus, ) return i, err } @@ -317,10 +322,11 @@ UPDATE deliveries SET phone = $15, additional_phone = $16, has_elevator = $17, - service_info = $18, - comment = $19, + payment_status = $18, + service_info = $19, + comment = $20, updated_at = NOW() -WHERE id = $20 +WHERE id = $21 ` type UpdateDeliveryParams struct { @@ -341,6 +347,7 @@ type UpdateDeliveryParams struct { Phone string `db:"phone" json:"phone"` AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"` HasElevator bool `db:"has_elevator" json:"has_elevator"` + PaymentStatus string `db:"payment_status" json:"payment_status"` ServiceInfo pgtype.Text `db:"service_info" json:"service_info"` Comment pgtype.Text `db:"comment" json:"comment"` ID pgtype.UUID `db:"id" json:"id"` @@ -365,6 +372,7 @@ func (q *Queries) UpdateDelivery(ctx context.Context, arg UpdateDeliveryParams) arg.Phone, arg.AdditionalPhone, arg.HasElevator, + arg.PaymentStatus, arg.ServiceInfo, arg.Comment, arg.ID, diff --git a/backend/internal/delivery/handler.go b/backend/internal/delivery/handler.go index 8694eab..b3808d0 100644 --- a/backend/internal/delivery/handler.go +++ b/backend/internal/delivery/handler.go @@ -37,6 +37,7 @@ type DeliveryRequest struct { Phone string `json:"phone" binding:"required"` AdditionalPhone *string `json:"additional_phone"` HasElevator bool `json:"has_elevator"` + PaymentStatus string `json:"payment_status" binding:"omitempty,oneof=prepaid paid"` ServiceInfo *string `json:"service_info"` Comment string `json:"comment"` } @@ -107,6 +108,7 @@ func (h *Handler) CreateDelivery(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + normalizePaymentStatus(&req) // Parse date from DD-MM-YYYY t, err := parseDate(req.Date) @@ -133,6 +135,7 @@ func (h *Handler) CreateDelivery(c *gin.Context) { Phone: req.Phone, AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, HasElevator: req.HasElevator, + PaymentStatus: req.PaymentStatus, ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, Comment: pgtype.Text{String: req.Comment, Valid: true}, } @@ -158,6 +161,7 @@ func (h *Handler) UpdateDelivery(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + normalizePaymentStatus(&req) id := c.Param("id") @@ -196,6 +200,7 @@ func (h *Handler) UpdateDelivery(c *gin.Context) { Phone: req.Phone, AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil}, HasElevator: req.HasElevator, + PaymentStatus: req.PaymentStatus, ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil}, Comment: pgtype.Text{String: req.Comment, Valid: true}, }); err != nil { @@ -308,3 +313,9 @@ func normalizeWarehouseRequestSources(req *DeliveryRequest) error { } return nil } + +func normalizePaymentStatus(req *DeliveryRequest) { + if req.PaymentStatus == "" { + req.PaymentStatus = "prepaid" + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 81b17c9..64d2caf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "delivery-tracker", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "delivery-tracker", - "version": "0.0.5", + "version": "0.0.6", "dependencies": { "@tailwindcss/vite": "^4.2.2", "date-fns": "^4.1.0", diff --git a/frontend/package.json b/frontend/package.json index 5598b5f..3ac3624 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "delivery-tracker", "private": true, - "version": "0.0.5", + "version": "0.0.6", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/api/deliveries.ts b/frontend/src/api/deliveries.ts index bc5b606..e4a07a4 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, DeliveryRequestSource, PickupLocation, DeliveryStatus } from '../types'; +import type { Delivery, DeliveryRequestSource, PickupLocation, DeliveryStatus, PaymentStatus } from '../types'; // Types matching backend responses export interface BackendDelivery { @@ -22,6 +22,7 @@ export interface BackendDelivery { phone: string; additional_phone: string | null; has_elevator: boolean; + payment_status: PaymentStatus; service_info: string | null; comment: string; status: DeliveryStatus; @@ -77,6 +78,7 @@ export function mapBackendToFrontend(backend: BackendDelivery): Delivery { phone: backend.phone, additionalPhone: backend.additional_phone || undefined, hasElevator: backend.has_elevator, + paymentStatus: backend.payment_status || 'prepaid', serviceInfo: backend.service_info || undefined, comment: backend.comment, status: backend.status, @@ -133,6 +135,7 @@ export const deliveriesApi = { phone: data.phone, additional_phone: data.additionalPhone || null, has_elevator: data.hasElevator, + payment_status: data.paymentStatus, service_info: data.serviceInfo || null, comment: data.comment, }; @@ -163,6 +166,7 @@ export const deliveriesApi = { phone: data.phone, additional_phone: data.additionalPhone || null, has_elevator: data.hasElevator, + payment_status: data.paymentStatus, service_info: data.serviceInfo || null, comment: data.comment, }; diff --git a/frontend/src/components/delivery/DeliveryCard.tsx b/frontend/src/components/delivery/DeliveryCard.tsx index 471d92b..66fc4fd 100644 --- a/frontend/src/components/delivery/DeliveryCard.tsx +++ b/frontend/src/components/delivery/DeliveryCard.tsx @@ -3,7 +3,10 @@ import { MapPin, Phone, Store, Calendar, MessageSquare, CheckCircle2, Circle, Ch import type { Delivery } from '../../types'; import { formatPickupLocation } from '../../types'; import { StatusBadge } from './StatusBadge'; +import { PaymentBadge } from './PaymentBadge'; import { Card } from '../ui/Card'; +import { WhatsAppIcon } from '../ui/WhatsAppIcon'; +import { getWhatsAppUrl } from '../../utils/phone'; const CITY = 'kokshetau'; const createdAtFormatter = new Intl.DateTimeFormat('ru-RU', { @@ -31,6 +34,10 @@ export const DeliveryCard = memo(({ delivery, onStatusChange, onEdit, onDelete } window.location.href = `tel:${delivery.phone}`; }; + const handleWhatsAppClick = (phone: string) => { + window.location.href = getWhatsAppUrl(phone); + }; + return (
@@ -112,26 +119,46 @@ export const DeliveryCard = memo(({ delivery, onStatusChange, onEdit, onDelete } {delivery.customerName}
- - - {delivery.additionalPhone && ( +
+ +
+ + {delivery.additionalPhone && ( +
+ + +
)}
@@ -141,6 +168,10 @@ export const DeliveryCard = memo(({ delivery, onStatusChange, onEdit, onDelete }
+
+ +
+ {delivery.serviceInfo && (
diff --git a/frontend/src/components/delivery/DeliveryForm.tsx b/frontend/src/components/delivery/DeliveryForm.tsx index e2a88de..025a7c8 100644 --- a/frontend/src/components/delivery/DeliveryForm.tsx +++ b/frontend/src/components/delivery/DeliveryForm.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Button, Input, Select, Modal } from '../ui'; import { deliveryRequestSourceOptions, pickupOptions } from '../../constants/pickup'; import { formatDateForInput, parseDateFromInput, getTodayFrontend } from '../../utils/date'; -import type { Delivery, DeliveryRequestSource, PickupLocation, DeliveryStatus } from '../../types'; +import type { Delivery, DeliveryRequestSource, PickupLocation, DeliveryStatus, PaymentStatus } from '../../types'; interface DeliveryFormProps { isOpen: boolean; @@ -22,6 +22,10 @@ const requestSourceOptions = [ { value: '', label: 'Выберите источник заявки' }, ...deliveryRequestSourceOptions, ]; +const paymentStatusOptions = [ + { value: 'prepaid', label: 'Предоплата' }, + { value: 'paid', label: 'Полная оплата' }, +]; const buildAddressString = ( street: string, @@ -56,6 +60,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa phone: '', additionalPhone: '', hasElevator: false, + paymentStatus: 'prepaid' as PaymentStatus, serviceInfo: '', comment: '', status: 'new' as DeliveryStatus, @@ -82,6 +87,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa phone: initialData.phone, additionalPhone: initialData.additionalPhone || '', hasElevator: initialData.hasElevator, + paymentStatus: initialData.paymentStatus || 'prepaid', serviceInfo: initialData.serviceInfo || '', comment: initialData.comment, status: initialData.status, @@ -147,6 +153,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa phone: '', additionalPhone: '', hasElevator: false, + paymentStatus: 'prepaid', serviceInfo: '', comment: '', status: 'new', @@ -383,6 +390,14 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
+ Клиент Адрес Телефон + Оплата Комментарий Действия diff --git a/frontend/src/components/delivery/DeliveryRow.tsx b/frontend/src/components/delivery/DeliveryRow.tsx index e11c5bb..2a4e0e9 100644 --- a/frontend/src/components/delivery/DeliveryRow.tsx +++ b/frontend/src/components/delivery/DeliveryRow.tsx @@ -3,6 +3,9 @@ import { MapPin, Phone } from 'lucide-react'; import type { Delivery } from '../../types'; import { formatPickupLocation } from '../../types'; import { StatusBadge } from './StatusBadge'; +import { PaymentBadge } from './PaymentBadge'; +import { WhatsAppIcon } from '../ui/WhatsAppIcon'; +import { getWhatsAppUrl } from '../../utils/phone'; const CITY = 'kokshetau'; @@ -25,6 +28,11 @@ export const DeliveryRow = memo(({ delivery, onStatusChange, onEdit, onDelete }: window.location.href = `tel:${delivery.phone}`; }; + const handleWhatsAppClick = (e: React.MouseEvent) => { + e.stopPropagation(); + window.location.href = getWhatsAppUrl(delivery.phone); + }; + return ( @@ -57,13 +65,26 @@ export const DeliveryRow = memo(({ delivery, onStatusChange, onEdit, onDelete }: - +
+ + +
+ + + {delivery.comment || '-'} diff --git a/frontend/src/components/delivery/PaymentBadge.tsx b/frontend/src/components/delivery/PaymentBadge.tsx new file mode 100644 index 0000000..fcf53a3 --- /dev/null +++ b/frontend/src/components/delivery/PaymentBadge.tsx @@ -0,0 +1,25 @@ +import { CircleDollarSign } from 'lucide-react'; +import type { PaymentStatus } from '../../types'; +import { paymentStatusLabels } from '../../types'; + +interface PaymentBadgeProps { + status: PaymentStatus; + size?: 'sm' | 'md'; +} + +const paymentBadgeClasses: Record = { + paid: 'bg-[#dcfce7] text-[#166534] border-[#86efac]', + prepaid: 'bg-[#fff7ed] text-[#c2410c] border-[#fed7aa]', +}; + +export const PaymentBadge = ({ status, size = 'md' }: PaymentBadgeProps) => { + const iconSize = size === 'sm' ? 13 : 16; + const sizeClass = size === 'sm' ? 'px-2 py-1 text-xs' : 'px-2.5 py-1.5 text-sm'; + + return ( + + + {paymentStatusLabels[status]} + + ); +}; diff --git a/frontend/src/components/ui/WhatsAppIcon.tsx b/frontend/src/components/ui/WhatsAppIcon.tsx new file mode 100644 index 0000000..880275c --- /dev/null +++ b/frontend/src/components/ui/WhatsAppIcon.tsx @@ -0,0 +1,17 @@ +import type { SVGProps } from 'react'; + +export const WhatsAppIcon = (props: SVGProps) => ( + +); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 4dfd3f2..7a885d6 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,6 +1,7 @@ export type PickupLocation = 'warehouse' | 'symbat' | 'nursaya' | 'galaktika'; export type DeliveryRequestSource = 'symbat' | 'nursaya' | 'galaktika'; export type DeliveryStatus = 'new' | 'delivered'; +export type PaymentStatus = 'prepaid' | 'paid'; export interface Delivery { id: string; @@ -22,6 +23,7 @@ export interface Delivery { floor?: string; serviceInfo?: string; hasElevator: boolean; + paymentStatus: PaymentStatus; comment: string; status: DeliveryStatus; createdAt: number; @@ -56,6 +58,11 @@ export const statusLabels: Record = { delivered: 'Доставлено', }; +export const paymentStatusLabels: Record = { + prepaid: 'Предоплата', + paid: 'Полная оплата', +}; + export interface LoginRequest { username: string; password: string; diff --git a/frontend/src/utils/phone.ts b/frontend/src/utils/phone.ts new file mode 100644 index 0000000..7d46192 --- /dev/null +++ b/frontend/src/utils/phone.ts @@ -0,0 +1,5 @@ +export const getWhatsAppUrl = (phone: string) => { + const digits = phone.replace(/\D/g, ''); + + return `https://wa.me/${digits}`; +};