add whatsapp contact button and prepayment/full payment badge
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE deliveries DROP COLUMN payment_status;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE deliveries
|
||||||
|
ADD COLUMN payment_status varchar(20) NOT NULL DEFAULT 'prepaid';
|
||||||
@@ -32,9 +32,9 @@ SELECT * FROM deliveries WHERE date = $1;
|
|||||||
INSERT INTO deliveries (
|
INSERT INTO deliveries (
|
||||||
date, pickup_location, pickup_location_2, warehouse_request_source, warehouse_request_source_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,
|
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 *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetDeliveryByID :one
|
-- name: GetDeliveryByID :one
|
||||||
@@ -62,10 +62,11 @@ UPDATE deliveries SET
|
|||||||
phone = $15,
|
phone = $15,
|
||||||
additional_phone = $16,
|
additional_phone = $16,
|
||||||
has_elevator = $17,
|
has_elevator = $17,
|
||||||
service_info = $18,
|
payment_status = $18,
|
||||||
comment = $19,
|
service_info = $19,
|
||||||
|
comment = $20,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $20;
|
WHERE id = $21;
|
||||||
|
|
||||||
-- name: GetDeliveryCount :many
|
-- name: GetDeliveryCount :many
|
||||||
SELECT COUNT(*) as count, date FROM deliveries
|
SELECT COUNT(*) as count, date FROM deliveries
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type Delivery struct {
|
|||||||
Floor pgtype.Text `db:"floor" json:"floor"`
|
Floor pgtype.Text `db:"floor" json:"floor"`
|
||||||
WarehouseRequestSource pgtype.Text `db:"warehouse_request_source" json:"warehouse_request_source"`
|
WarehouseRequestSource pgtype.Text `db:"warehouse_request_source" json:"warehouse_request_source"`
|
||||||
WarehouseRequestSource2 pgtype.Text `db:"warehouse_request_source_2" json:"warehouse_request_source_2"`
|
WarehouseRequestSource2 pgtype.Text `db:"warehouse_request_source_2" json:"warehouse_request_source_2"`
|
||||||
|
PaymentStatus string `db:"payment_status" json:"payment_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ const createDelivery = `-- name: CreateDelivery :one
|
|||||||
INSERT INTO deliveries (
|
INSERT INTO deliveries (
|
||||||
date, pickup_location, pickup_location_2, warehouse_request_source, warehouse_request_source_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,
|
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 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
|
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 {
|
type CreateDeliveryParams struct {
|
||||||
@@ -39,6 +39,7 @@ type CreateDeliveryParams struct {
|
|||||||
Phone string `db:"phone" json:"phone"`
|
Phone string `db:"phone" json:"phone"`
|
||||||
AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"`
|
AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"`
|
||||||
HasElevator bool `db:"has_elevator" json:"has_elevator"`
|
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"`
|
ServiceInfo pgtype.Text `db:"service_info" json:"service_info"`
|
||||||
Comment pgtype.Text `db:"comment" json:"comment"`
|
Comment pgtype.Text `db:"comment" json:"comment"`
|
||||||
}
|
}
|
||||||
@@ -62,6 +63,7 @@ func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams)
|
|||||||
arg.Phone,
|
arg.Phone,
|
||||||
arg.AdditionalPhone,
|
arg.AdditionalPhone,
|
||||||
arg.HasElevator,
|
arg.HasElevator,
|
||||||
|
arg.PaymentStatus,
|
||||||
arg.ServiceInfo,
|
arg.ServiceInfo,
|
||||||
arg.Comment,
|
arg.Comment,
|
||||||
)
|
)
|
||||||
@@ -90,6 +92,7 @@ func (q *Queries) CreateDelivery(ctx context.Context, arg CreateDeliveryParams)
|
|||||||
&i.Floor,
|
&i.Floor,
|
||||||
&i.WarehouseRequestSource,
|
&i.WarehouseRequestSource,
|
||||||
&i.WarehouseRequestSource2,
|
&i.WarehouseRequestSource2,
|
||||||
|
&i.PaymentStatus,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -129,7 +132,7 @@ func (q *Queries) DeleteDelivery(ctx context.Context, id pgtype.UUID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDeliveriesByDate = `-- name: GetDeliveriesByDate :many
|
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) {
|
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.Floor,
|
||||||
&i.WarehouseRequestSource,
|
&i.WarehouseRequestSource,
|
||||||
&i.WarehouseRequestSource2,
|
&i.WarehouseRequestSource2,
|
||||||
|
&i.PaymentStatus,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -177,7 +181,7 @@ func (q *Queries) GetDeliveriesByDate(ctx context.Context, date pgtype.Date) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getDeliveryByID = `-- name: GetDeliveryByID :one
|
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) {
|
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.Floor,
|
||||||
&i.WarehouseRequestSource,
|
&i.WarehouseRequestSource,
|
||||||
&i.WarehouseRequestSource2,
|
&i.WarehouseRequestSource2,
|
||||||
|
&i.PaymentStatus,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -317,10 +322,11 @@ UPDATE deliveries SET
|
|||||||
phone = $15,
|
phone = $15,
|
||||||
additional_phone = $16,
|
additional_phone = $16,
|
||||||
has_elevator = $17,
|
has_elevator = $17,
|
||||||
service_info = $18,
|
payment_status = $18,
|
||||||
comment = $19,
|
service_info = $19,
|
||||||
|
comment = $20,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $20
|
WHERE id = $21
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateDeliveryParams struct {
|
type UpdateDeliveryParams struct {
|
||||||
@@ -341,6 +347,7 @@ type UpdateDeliveryParams struct {
|
|||||||
Phone string `db:"phone" json:"phone"`
|
Phone string `db:"phone" json:"phone"`
|
||||||
AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"`
|
AdditionalPhone pgtype.Text `db:"additional_phone" json:"additional_phone"`
|
||||||
HasElevator bool `db:"has_elevator" json:"has_elevator"`
|
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"`
|
ServiceInfo pgtype.Text `db:"service_info" json:"service_info"`
|
||||||
Comment pgtype.Text `db:"comment" json:"comment"`
|
Comment pgtype.Text `db:"comment" json:"comment"`
|
||||||
ID pgtype.UUID `db:"id" json:"id"`
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
@@ -365,6 +372,7 @@ func (q *Queries) UpdateDelivery(ctx context.Context, arg UpdateDeliveryParams)
|
|||||||
arg.Phone,
|
arg.Phone,
|
||||||
arg.AdditionalPhone,
|
arg.AdditionalPhone,
|
||||||
arg.HasElevator,
|
arg.HasElevator,
|
||||||
|
arg.PaymentStatus,
|
||||||
arg.ServiceInfo,
|
arg.ServiceInfo,
|
||||||
arg.Comment,
|
arg.Comment,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type DeliveryRequest struct {
|
|||||||
Phone string `json:"phone" binding:"required"`
|
Phone string `json:"phone" binding:"required"`
|
||||||
AdditionalPhone *string `json:"additional_phone"`
|
AdditionalPhone *string `json:"additional_phone"`
|
||||||
HasElevator bool `json:"has_elevator"`
|
HasElevator bool `json:"has_elevator"`
|
||||||
|
PaymentStatus string `json:"payment_status" binding:"omitempty,oneof=prepaid paid"`
|
||||||
ServiceInfo *string `json:"service_info"`
|
ServiceInfo *string `json:"service_info"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
}
|
}
|
||||||
@@ -107,6 +108,7 @@ func (h *Handler) CreateDelivery(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
normalizePaymentStatus(&req)
|
||||||
|
|
||||||
// Parse date from DD-MM-YYYY
|
// Parse date from DD-MM-YYYY
|
||||||
t, err := parseDate(req.Date)
|
t, err := parseDate(req.Date)
|
||||||
@@ -133,6 +135,7 @@ func (h *Handler) CreateDelivery(c *gin.Context) {
|
|||||||
Phone: req.Phone,
|
Phone: req.Phone,
|
||||||
AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil},
|
AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil},
|
||||||
HasElevator: req.HasElevator,
|
HasElevator: req.HasElevator,
|
||||||
|
PaymentStatus: req.PaymentStatus,
|
||||||
ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil},
|
ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil},
|
||||||
Comment: pgtype.Text{String: req.Comment, Valid: true},
|
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()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
normalizePaymentStatus(&req)
|
||||||
|
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|
||||||
@@ -196,6 +200,7 @@ func (h *Handler) UpdateDelivery(c *gin.Context) {
|
|||||||
Phone: req.Phone,
|
Phone: req.Phone,
|
||||||
AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil},
|
AdditionalPhone: pgtype.Text{String: derefString(req.AdditionalPhone), Valid: req.AdditionalPhone != nil},
|
||||||
HasElevator: req.HasElevator,
|
HasElevator: req.HasElevator,
|
||||||
|
PaymentStatus: req.PaymentStatus,
|
||||||
ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil},
|
ServiceInfo: pgtype.Text{String: derefString(req.ServiceInfo), Valid: req.ServiceInfo != nil},
|
||||||
Comment: pgtype.Text{String: req.Comment, Valid: true},
|
Comment: pgtype.Text{String: req.Comment, Valid: true},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -308,3 +313,9 @@ func normalizeWarehouseRequestSources(req *DeliveryRequest) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizePaymentStatus(req *DeliveryRequest) {
|
||||||
|
if req.PaymentStatus == "" {
|
||||||
|
req.PaymentStatus = "prepaid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "delivery-tracker",
|
"name": "delivery-tracker",
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "delivery-tracker",
|
"name": "delivery-tracker",
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "delivery-tracker",
|
"name": "delivery-tracker",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.5",
|
"version": "0.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { api } from './client';
|
import { api } from './client';
|
||||||
import { backendDateToFrontend } from '../utils/date';
|
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
|
// Types matching backend responses
|
||||||
export interface BackendDelivery {
|
export interface BackendDelivery {
|
||||||
@@ -22,6 +22,7 @@ export interface BackendDelivery {
|
|||||||
phone: string;
|
phone: string;
|
||||||
additional_phone: string | null;
|
additional_phone: string | null;
|
||||||
has_elevator: boolean;
|
has_elevator: boolean;
|
||||||
|
payment_status: PaymentStatus;
|
||||||
service_info: string | null;
|
service_info: string | null;
|
||||||
comment: string;
|
comment: string;
|
||||||
status: DeliveryStatus;
|
status: DeliveryStatus;
|
||||||
@@ -77,6 +78,7 @@ export function mapBackendToFrontend(backend: BackendDelivery): Delivery {
|
|||||||
phone: backend.phone,
|
phone: backend.phone,
|
||||||
additionalPhone: backend.additional_phone || undefined,
|
additionalPhone: backend.additional_phone || undefined,
|
||||||
hasElevator: backend.has_elevator,
|
hasElevator: backend.has_elevator,
|
||||||
|
paymentStatus: backend.payment_status || 'prepaid',
|
||||||
serviceInfo: backend.service_info || undefined,
|
serviceInfo: backend.service_info || undefined,
|
||||||
comment: backend.comment,
|
comment: backend.comment,
|
||||||
status: backend.status,
|
status: backend.status,
|
||||||
@@ -133,6 +135,7 @@ export const deliveriesApi = {
|
|||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
additional_phone: data.additionalPhone || null,
|
additional_phone: data.additionalPhone || null,
|
||||||
has_elevator: data.hasElevator,
|
has_elevator: data.hasElevator,
|
||||||
|
payment_status: data.paymentStatus,
|
||||||
service_info: data.serviceInfo || null,
|
service_info: data.serviceInfo || null,
|
||||||
comment: data.comment,
|
comment: data.comment,
|
||||||
};
|
};
|
||||||
@@ -163,6 +166,7 @@ export const deliveriesApi = {
|
|||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
additional_phone: data.additionalPhone || null,
|
additional_phone: data.additionalPhone || null,
|
||||||
has_elevator: data.hasElevator,
|
has_elevator: data.hasElevator,
|
||||||
|
payment_status: data.paymentStatus,
|
||||||
service_info: data.serviceInfo || null,
|
service_info: data.serviceInfo || null,
|
||||||
comment: data.comment,
|
comment: data.comment,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { MapPin, Phone, Store, Calendar, MessageSquare, CheckCircle2, Circle, Ch
|
|||||||
import type { Delivery } from '../../types';
|
import type { Delivery } from '../../types';
|
||||||
import { formatPickupLocation } from '../../types';
|
import { formatPickupLocation } from '../../types';
|
||||||
import { StatusBadge } from './StatusBadge';
|
import { StatusBadge } from './StatusBadge';
|
||||||
|
import { PaymentBadge } from './PaymentBadge';
|
||||||
import { Card } from '../ui/Card';
|
import { Card } from '../ui/Card';
|
||||||
|
import { WhatsAppIcon } from '../ui/WhatsAppIcon';
|
||||||
|
import { getWhatsAppUrl } from '../../utils/phone';
|
||||||
|
|
||||||
const CITY = 'kokshetau';
|
const CITY = 'kokshetau';
|
||||||
const createdAtFormatter = new Intl.DateTimeFormat('ru-RU', {
|
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}`;
|
window.location.href = `tel:${delivery.phone}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWhatsAppClick = (phone: string) => {
|
||||||
|
window.location.href = getWhatsAppUrl(phone);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="relative">
|
<Card className="relative">
|
||||||
<div className="flex items-start justify-between gap-3 mb-3">
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
@@ -112,26 +119,46 @@ export const DeliveryCard = memo(({ delivery, onStatusChange, onEdit, onDelete }
|
|||||||
<span className="text-[#1b1b1d]">{delivery.customerName}</span>
|
<span className="text-[#1b1b1d]">{delivery.customerName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center gap-2 -mx-1">
|
||||||
onClick={handlePhoneClick}
|
|
||||||
className="flex items-center gap-2 text-sm w-full text-left hover:bg-[#f5f3f5] -mx-1 px-1 py-0.5 rounded transition-colors"
|
|
||||||
>
|
|
||||||
<Phone size={16} className="text-[#16a34a] shrink-0" />
|
|
||||||
<span className="text-[#16a34a] font-medium">
|
|
||||||
{delivery.phone}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{delivery.additionalPhone && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.href = `tel:${delivery.additionalPhone}`}
|
onClick={handlePhoneClick}
|
||||||
className="flex items-center gap-2 text-sm w-full text-left hover:bg-[#f5f3f5] -mx-1 px-1 py-0.5 rounded transition-colors"
|
className="flex min-w-0 items-center gap-2 text-sm text-left hover:bg-[#f5f3f5] px-1 py-0.5 rounded transition-colors"
|
||||||
>
|
>
|
||||||
<Phone size={16} className="text-[#75777d] shrink-0" />
|
<Phone size={16} className="text-[#16a34a] shrink-0" />
|
||||||
<span className="text-[#75777d]">
|
<span className="text-[#16a34a] font-medium">
|
||||||
{delivery.additionalPhone}
|
{delivery.phone}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleWhatsAppClick(delivery.phone)}
|
||||||
|
className="p-1.5 rounded-md text-[#16a34a] hover:bg-[#f5f3f5] transition-colors"
|
||||||
|
title="Открыть WhatsApp"
|
||||||
|
aria-label={`Открыть WhatsApp для номера ${delivery.phone}`}
|
||||||
|
>
|
||||||
|
<WhatsAppIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{delivery.additionalPhone && (
|
||||||
|
<div className="flex items-center gap-2 -mx-1">
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = `tel:${delivery.additionalPhone}`}
|
||||||
|
className="flex min-w-0 items-center gap-2 text-sm text-left hover:bg-[#f5f3f5] px-1 py-0.5 rounded transition-colors"
|
||||||
|
>
|
||||||
|
<Phone size={16} className="text-[#75777d] shrink-0" />
|
||||||
|
<span className="text-[#75777d]">
|
||||||
|
{delivery.additionalPhone}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleWhatsAppClick(delivery.additionalPhone ?? '')}
|
||||||
|
className="p-1.5 rounded-md text-[#16a34a] hover:bg-[#f5f3f5] transition-colors"
|
||||||
|
title="Открыть WhatsApp"
|
||||||
|
aria-label={`Открыть WhatsApp для номера ${delivery.additionalPhone}`}
|
||||||
|
>
|
||||||
|
<WhatsAppIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
@@ -141,6 +168,10 @@ export const DeliveryCard = memo(({ delivery, onStatusChange, onEdit, onDelete }
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<PaymentBadge status={delivery.paymentStatus} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{delivery.serviceInfo && (
|
{delivery.serviceInfo && (
|
||||||
<div className="flex items-start gap-2 text-sm">
|
<div className="flex items-start gap-2 text-sm">
|
||||||
<Wrench size={16} className="text-[#F28C28] mt-0.5 shrink-0" />
|
<Wrench size={16} className="text-[#F28C28] mt-0.5 shrink-0" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import { Button, Input, Select, Modal } from '../ui';
|
import { Button, Input, Select, Modal } from '../ui';
|
||||||
import { deliveryRequestSourceOptions, pickupOptions } from '../../constants/pickup';
|
import { deliveryRequestSourceOptions, pickupOptions } from '../../constants/pickup';
|
||||||
import { formatDateForInput, parseDateFromInput, getTodayFrontend } from '../../utils/date';
|
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 {
|
interface DeliveryFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -22,6 +22,10 @@ const requestSourceOptions = [
|
|||||||
{ value: '', label: 'Выберите источник заявки' },
|
{ value: '', label: 'Выберите источник заявки' },
|
||||||
...deliveryRequestSourceOptions,
|
...deliveryRequestSourceOptions,
|
||||||
];
|
];
|
||||||
|
const paymentStatusOptions = [
|
||||||
|
{ value: 'prepaid', label: 'Предоплата' },
|
||||||
|
{ value: 'paid', label: 'Полная оплата' },
|
||||||
|
];
|
||||||
|
|
||||||
const buildAddressString = (
|
const buildAddressString = (
|
||||||
street: string,
|
street: string,
|
||||||
@@ -56,6 +60,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
|||||||
phone: '',
|
phone: '',
|
||||||
additionalPhone: '',
|
additionalPhone: '',
|
||||||
hasElevator: false,
|
hasElevator: false,
|
||||||
|
paymentStatus: 'prepaid' as PaymentStatus,
|
||||||
serviceInfo: '',
|
serviceInfo: '',
|
||||||
comment: '',
|
comment: '',
|
||||||
status: 'new' as DeliveryStatus,
|
status: 'new' as DeliveryStatus,
|
||||||
@@ -82,6 +87,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
|||||||
phone: initialData.phone,
|
phone: initialData.phone,
|
||||||
additionalPhone: initialData.additionalPhone || '',
|
additionalPhone: initialData.additionalPhone || '',
|
||||||
hasElevator: initialData.hasElevator,
|
hasElevator: initialData.hasElevator,
|
||||||
|
paymentStatus: initialData.paymentStatus || 'prepaid',
|
||||||
serviceInfo: initialData.serviceInfo || '',
|
serviceInfo: initialData.serviceInfo || '',
|
||||||
comment: initialData.comment,
|
comment: initialData.comment,
|
||||||
status: initialData.status,
|
status: initialData.status,
|
||||||
@@ -147,6 +153,7 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
|||||||
phone: '',
|
phone: '',
|
||||||
additionalPhone: '',
|
additionalPhone: '',
|
||||||
hasElevator: false,
|
hasElevator: false,
|
||||||
|
paymentStatus: 'prepaid',
|
||||||
serviceInfo: '',
|
serviceInfo: '',
|
||||||
comment: '',
|
comment: '',
|
||||||
status: 'new',
|
status: 'new',
|
||||||
@@ -383,6 +390,14 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Оплата"
|
||||||
|
value={formData.paymentStatus}
|
||||||
|
onChange={(e) => setFormData({ ...formData, paymentStatus: e.target.value as PaymentStatus })}
|
||||||
|
options={paymentStatusOptions}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Услуги (сборка, подъём)"
|
label="Услуги (сборка, подъём)"
|
||||||
value={formData.serviceInfo}
|
value={formData.serviceInfo}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export const DeliveryList = ({ deliveries, onStatusChange, onEdit, onDelete, onA
|
|||||||
<th className="px-4 py-3">Клиент</th>
|
<th className="px-4 py-3">Клиент</th>
|
||||||
<th className="px-4 py-3">Адрес</th>
|
<th className="px-4 py-3">Адрес</th>
|
||||||
<th className="px-4 py-3">Телефон</th>
|
<th className="px-4 py-3">Телефон</th>
|
||||||
|
<th className="px-4 py-3">Оплата</th>
|
||||||
<th className="px-4 py-3">Комментарий</th>
|
<th className="px-4 py-3">Комментарий</th>
|
||||||
<th className="px-4 py-3">Действия</th>
|
<th className="px-4 py-3">Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { MapPin, Phone } from 'lucide-react';
|
|||||||
import type { Delivery } from '../../types';
|
import type { Delivery } from '../../types';
|
||||||
import { formatPickupLocation } from '../../types';
|
import { formatPickupLocation } from '../../types';
|
||||||
import { StatusBadge } from './StatusBadge';
|
import { StatusBadge } from './StatusBadge';
|
||||||
|
import { PaymentBadge } from './PaymentBadge';
|
||||||
|
import { WhatsAppIcon } from '../ui/WhatsAppIcon';
|
||||||
|
import { getWhatsAppUrl } from '../../utils/phone';
|
||||||
|
|
||||||
const CITY = 'kokshetau';
|
const CITY = 'kokshetau';
|
||||||
|
|
||||||
@@ -25,6 +28,11 @@ export const DeliveryRow = memo(({ delivery, onStatusChange, onEdit, onDelete }:
|
|||||||
window.location.href = `tel:${delivery.phone}`;
|
window.location.href = `tel:${delivery.phone}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWhatsAppClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.location.href = getWhatsAppUrl(delivery.phone);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="hover:bg-[#f5f3f5] transition-colors border-b border-[#e4e2e4] last:border-b-0">
|
<tr className="hover:bg-[#f5f3f5] transition-colors border-b border-[#e4e2e4] last:border-b-0">
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
@@ -57,13 +65,26 @@ export const DeliveryRow = memo(({ delivery, onStatusChange, onEdit, onDelete }:
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={handlePhoneClick}
|
<button
|
||||||
className="flex items-center gap-1.5 text-sm text-[#16a34a] hover:underline transition-colors"
|
onClick={handlePhoneClick}
|
||||||
>
|
className="flex items-center gap-1.5 text-sm text-[#16a34a] hover:underline transition-colors"
|
||||||
<Phone size={14} />
|
>
|
||||||
{delivery.phone}
|
<Phone size={14} />
|
||||||
</button>
|
{delivery.phone}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleWhatsAppClick}
|
||||||
|
className="p-1.5 rounded-md text-[#16a34a] hover:bg-[#f5f3f5] transition-colors"
|
||||||
|
title="Открыть WhatsApp"
|
||||||
|
aria-label={`Открыть WhatsApp для номера ${delivery.phone}`}
|
||||||
|
>
|
||||||
|
<WhatsAppIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-[#45474d] max-w-[200px] truncate">
|
||||||
|
<PaymentBadge status={delivery.paymentStatus} size="sm" />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-[#45474d] max-w-[200px] truncate">
|
<td className="px-4 py-3 text-sm text-[#45474d] max-w-[200px] truncate">
|
||||||
{delivery.comment || '-'}
|
{delivery.comment || '-'}
|
||||||
|
|||||||
@@ -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<PaymentStatus, string> = {
|
||||||
|
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 (
|
||||||
|
<span className={`inline-flex items-center gap-1.5 rounded-md border font-medium ${sizeClass} ${paymentBadgeClasses[status]}`}>
|
||||||
|
<CircleDollarSign size={iconSize} className="shrink-0" />
|
||||||
|
{paymentStatusLabels[status]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { SVGProps } from 'react';
|
||||||
|
|
||||||
|
export const WhatsAppIcon = (props: SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M5.5 18.5 4 22l3.7-1.3A9 9 0 1 0 3 12a8.9 8.9 0 0 0 2.5 6.5Z" />
|
||||||
|
<path d="M9.5 8.5c.3-.3.7-.3 1-.1l.9 1.2c.2.3.2.7 0 1l-.5.7c.7 1.2 1.6 2.1 2.8 2.8l.7-.5c.3-.2.7-.2 1 0l1.2.9c.3.2.3.7.1 1-.4.6-1 1-1.7 1-3.4-.1-7.4-4.1-7.5-7.5 0-.7.4-1.3 1-1.7Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export type PickupLocation = 'warehouse' | 'symbat' | 'nursaya' | 'galaktika';
|
export type PickupLocation = 'warehouse' | 'symbat' | 'nursaya' | 'galaktika';
|
||||||
export type DeliveryRequestSource = 'symbat' | 'nursaya' | 'galaktika';
|
export type DeliveryRequestSource = 'symbat' | 'nursaya' | 'galaktika';
|
||||||
export type DeliveryStatus = 'new' | 'delivered';
|
export type DeliveryStatus = 'new' | 'delivered';
|
||||||
|
export type PaymentStatus = 'prepaid' | 'paid';
|
||||||
|
|
||||||
export interface Delivery {
|
export interface Delivery {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -22,6 +23,7 @@ export interface Delivery {
|
|||||||
floor?: string;
|
floor?: string;
|
||||||
serviceInfo?: string;
|
serviceInfo?: string;
|
||||||
hasElevator: boolean;
|
hasElevator: boolean;
|
||||||
|
paymentStatus: PaymentStatus;
|
||||||
comment: string;
|
comment: string;
|
||||||
status: DeliveryStatus;
|
status: DeliveryStatus;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
@@ -56,6 +58,11 @@ export const statusLabels: Record<DeliveryStatus, string> = {
|
|||||||
delivered: 'Доставлено',
|
delivered: 'Доставлено',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const paymentStatusLabels: Record<PaymentStatus, string> = {
|
||||||
|
prepaid: 'Предоплата',
|
||||||
|
paid: 'Полная оплата',
|
||||||
|
};
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const getWhatsAppUrl = (phone: string) => {
|
||||||
|
const digits = phone.replace(/\D/g, '');
|
||||||
|
|
||||||
|
return `https://wa.me/${digits}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user