add warehouse_request_source and warehouse_request_source_2 fields to deliveries table with validation and normalization logic
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
<Store size={16} className="text-[#75777d] mt-0.5 shrink-0" />
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<div className="flex items-baseline gap-2 flex-wrap">
|
||||
<span className="text-[#1b1b1d] font-medium">{pickupLocationLabels[delivery.pickupLocation]}</span>
|
||||
<span className="text-[#1b1b1d] font-medium">{formatPickupLocation(delivery.pickupLocation, delivery.warehouseRequestSource)}</span>
|
||||
<span className="text-[#75777d]">—</span>
|
||||
<span className="text-[#1b1b1d]">{delivery.productName}</span>
|
||||
</div>
|
||||
{delivery.pickupLocation2 && (
|
||||
<div className="flex items-baseline gap-2 flex-wrap">
|
||||
<span className="text-[#1b1b1d] font-medium">{pickupLocationLabels[delivery.pickupLocation2]}</span>
|
||||
<span className="text-[#1b1b1d] font-medium">{formatPickupLocation(delivery.pickupLocation2, delivery.warehouseRequestSource2)}</span>
|
||||
<span className="text-[#75777d]">—</span>
|
||||
<span className="text-[#1b1b1d]">{delivery.productName2 || '—'}</span>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
<Select
|
||||
label="Место загрузки"
|
||||
value={formData.pickupLocation}
|
||||
onChange={(e) => setFormData({ ...formData, pickupLocation: e.target.value as PickupLocation })}
|
||||
onChange={(e) => handlePickupLocationChange(e.target.value as PickupLocation)}
|
||||
options={pickupOptions}
|
||||
/>
|
||||
|
||||
{formData.pickupLocation === 'warehouse' && (
|
||||
<Select
|
||||
label="От кого заявка *"
|
||||
value={formData.warehouseRequestSource || ''}
|
||||
onChange={(e) => setFormData({ ...formData, warehouseRequestSource: e.target.value ? e.target.value as DeliveryRequestSource : null })}
|
||||
options={requestSourceOptions}
|
||||
required
|
||||
error={!isWarehouseRequestSourceValid ? 'Выберите, от кого исходит заявка' : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Название товара"
|
||||
value={formData.productName}
|
||||
@@ -287,10 +326,14 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
||||
id="hasSecondPickup"
|
||||
checked={showSecondPickup}
|
||||
onChange={(e) => {
|
||||
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
|
||||
<Select
|
||||
label="Место загрузки 2"
|
||||
value={formData.pickupLocation2 || ''}
|
||||
onChange={(e) => setFormData({ ...formData, pickupLocation2: e.target.value as PickupLocation })}
|
||||
onChange={(e) => handlePickupLocation2Change(e.target.value as PickupLocation)}
|
||||
options={pickupOptions}
|
||||
/>
|
||||
{formData.pickupLocation2 === 'warehouse' && (
|
||||
<Select
|
||||
label="От кого заявка 2 *"
|
||||
value={formData.warehouseRequestSource2 || ''}
|
||||
onChange={(e) => setFormData({ ...formData, warehouseRequestSource2: e.target.value ? e.target.value as DeliveryRequestSource : null })}
|
||||
options={requestSourceOptions}
|
||||
required
|
||||
error={!isWarehouseRequestSource2Valid ? 'Выберите, от кого исходит заявка' : undefined}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
label="Название товара 2"
|
||||
value={formData.productName2}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { MapPin, Phone } from 'lucide-react';
|
||||
import type { Delivery } from '../../types';
|
||||
import { pickupLocationLabels } from '../../types';
|
||||
import { formatPickupLocation } from '../../types';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
|
||||
const CITY = 'kokshetau';
|
||||
@@ -37,8 +37,8 @@ export const DeliveryRow = memo(({ delivery, onStatusChange, onEdit, onDelete }:
|
||||
<td className="px-4 py-3 text-sm text-[#1b1b1d]">{delivery.date}</td>
|
||||
<td className="px-4 py-3 text-sm text-[#1b1b1d]">
|
||||
{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)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-[#1b1b1d]">
|
||||
{delivery.productName}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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) => {
|
||||
</tr>
|
||||
${dayDeliveries.map((d: Delivery) => `
|
||||
<tr>
|
||||
<td>${d.pickupLocation2 ? pickupLocationLabels[d.pickupLocation] + ' + ' + pickupLocationLabels[d.pickupLocation2] : pickupLocationLabels[d.pickupLocation]}</td>
|
||||
<td>${d.pickupLocation2 ? formatPickupLocation(d.pickupLocation, d.warehouseRequestSource) + ' + ' + formatPickupLocation(d.pickupLocation2, d.warehouseRequestSource2) : formatPickupLocation(d.pickupLocation, d.warehouseRequestSource)}</td>
|
||||
<td>${d.productName}${d.productName2 ? '<br><small>+ ' + d.productName2 + '</small>' : ''}</td>
|
||||
<td>${d.customerName}</td>
|
||||
<td>
|
||||
|
||||
@@ -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<PickupLocation, string> = {
|
||||
galaktika: 'Галактика',
|
||||
};
|
||||
|
||||
export const deliveryRequestSourceLabels: Record<DeliveryRequestSource, string> = {
|
||||
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<DeliveryStatus, string> = {
|
||||
new: 'Новое',
|
||||
|
||||
Reference in New Issue
Block a user