Compare commits
2 Commits
b36a6fb262
...
0540218332
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0540218332 | ||
|
|
7f410e814b |
@@ -5,9 +5,11 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
db "github.com/chedius/delivery-tracker/internal/db/sqlc"
|
db "github.com/chedius/delivery-tracker/internal/db/sqlc"
|
||||||
"github.com/chedius/delivery-tracker/internal/delivery"
|
"github.com/chedius/delivery-tracker/internal/delivery"
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
@@ -29,6 +31,15 @@ func main() {
|
|||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
// CORS middleware - allow all origins in development
|
||||||
|
r.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: []string{"*"},
|
||||||
|
AllowMethods: []string{"GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowHeaders: []string{"*"},
|
||||||
|
AllowCredentials: false,
|
||||||
|
MaxAge: 12 * time.Hour,
|
||||||
|
}))
|
||||||
|
|
||||||
r.GET("/health", func(c *gin.Context) {
|
r.GET("/health", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ require (
|
|||||||
github.com/jackc/pgx/v5 v5.9.1
|
github.com/jackc/pgx/v5 v5.9.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/google/uuid v1.6.0
|
require (
|
||||||
|
github.com/gin-contrib/cors v1.7.7
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
@@ -37,11 +40,11 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
golang.org/x/arch v0.22.0 // indirect
|
golang.org/x/arch v0.23.0 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/cors v1.7.7 h1:Oh9joP463x7Mw72vhvJ61YQm8ODh9b04YR7vsOErD0Q=
|
||||||
|
github.com/gin-contrib/cors v1.7.7/go.mod h1:K5tW0RkzJtWSiOdikXloy8VEZlgdVNpHNw8FpjUPNrE=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
@@ -83,19 +85,19 @@ go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF
|
|||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
2
frontend/.env.example
Normal file
2
frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# API Configuration
|
||||||
|
VITE_API_URL=http://localhost:8080
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Truck } from 'lucide-react';
|
import { Truck } from 'lucide-react';
|
||||||
import { Dashboard } from './pages/Dashboard';
|
import { Dashboard } from './pages/Dashboard';
|
||||||
import { DeliveryListPage } from './pages/DeliveryListPage';
|
import { DeliveryListPage } from './pages/DeliveryListPage';
|
||||||
@@ -10,8 +10,17 @@ function App() {
|
|||||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||||
const [formDate, setFormDate] = useState<string>('');
|
const [formDate, setFormDate] = useState<string>('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const addDelivery = useDeliveryStore(state => state.addDelivery);
|
const addDelivery = useDeliveryStore(state => state.addDelivery);
|
||||||
|
const fetchDeliveryCounts = useDeliveryStore(state => state.fetchDeliveryCounts);
|
||||||
|
|
||||||
|
// Refresh counts when form closes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFormOpen) {
|
||||||
|
fetchDeliveryCounts();
|
||||||
|
}
|
||||||
|
}, [isFormOpen, fetchDeliveryCounts]);
|
||||||
|
|
||||||
const handleDateSelect = (date: string) => {
|
const handleDateSelect = (date: string) => {
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
@@ -29,13 +38,22 @@ function App() {
|
|||||||
setIsFormOpen(true);
|
setIsFormOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = (data: Parameters<typeof addDelivery>[0]) => {
|
const handleFormSubmit = async (data: Parameters<typeof addDelivery>[0]) => {
|
||||||
addDelivery(data);
|
setIsSubmitting(true);
|
||||||
setIsFormOpen(false);
|
try {
|
||||||
|
await addDelivery(data);
|
||||||
if (data.date !== new Date().toLocaleDateString('ru-RU').split('.').join('-')) {
|
setIsFormOpen(false);
|
||||||
setSelectedDate(data.date);
|
|
||||||
setView('delivery-list');
|
// If created for different date, navigate to that date
|
||||||
|
const today = new Date().toLocaleDateString('ru-RU').split('.').join('-');
|
||||||
|
if (data.date !== today) {
|
||||||
|
setSelectedDate(data.date);
|
||||||
|
setView('delivery-list');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Error is handled by store
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,6 +94,7 @@ function App() {
|
|||||||
onClose={() => setIsFormOpen(false)}
|
onClose={() => setIsFormOpen(false)}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
defaultDate={formDate}
|
defaultDate={formDate}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
58
frontend/src/api/client.ts
Normal file
58
frontend/src/api/client.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
status: number;
|
||||||
|
details?: unknown;
|
||||||
|
|
||||||
|
constructor(message: string, status: number, details?: unknown) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.status = status;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchApi<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options?: RequestInit
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options?.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null);
|
||||||
|
throw new ApiError(
|
||||||
|
errorData?.error || `HTTP ${response.status}`,
|
||||||
|
response.status,
|
||||||
|
errorData?.details
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
get: <T>(endpoint: string) => fetchApi<T>(endpoint, { method: 'GET' }),
|
||||||
|
|
||||||
|
post: <T>(endpoint: string, data: unknown) =>
|
||||||
|
fetchApi<T>(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}),
|
||||||
|
|
||||||
|
patch: <T>(endpoint: string, data?: unknown) =>
|
||||||
|
fetchApi<T>(endpoint, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: <T>(endpoint: string) =>
|
||||||
|
fetchApi<T>(endpoint, { method: 'DELETE' }),
|
||||||
|
};
|
||||||
148
frontend/src/api/deliveries.ts
Normal file
148
frontend/src/api/deliveries.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { api } from './client';
|
||||||
|
import type { Delivery, PickupLocation, DeliveryStatus } from '../types';
|
||||||
|
|
||||||
|
// Types matching backend responses
|
||||||
|
interface BackendDelivery {
|
||||||
|
id: string;
|
||||||
|
date: string; // YYYY-MM-DD from pgtype.Date
|
||||||
|
pickup_location: PickupLocation;
|
||||||
|
product_name: string;
|
||||||
|
address: string;
|
||||||
|
phone: string;
|
||||||
|
additional_phone: string | null;
|
||||||
|
has_elevator: boolean;
|
||||||
|
comment: string;
|
||||||
|
status: DeliveryStatus;
|
||||||
|
created_at: string; // ISO timestamp
|
||||||
|
updated_at: string; // ISO timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeliveryCount {
|
||||||
|
date: string; // YYYY-MM-DD
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Response types
|
||||||
|
interface GetDeliveriesResponse {
|
||||||
|
deliveries: BackendDelivery[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetDeliveryResponse {
|
||||||
|
delivery: BackendDelivery;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetDeliveryCountResponse {
|
||||||
|
counts: DeliveryCount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateDeliveryResponse {
|
||||||
|
message: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateDeliveryResponse {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert backend date format (YYYY-MM-DD) to frontend format (DD-MM-YYYY)
|
||||||
|
function backendDateToFrontend(dateStr: string): string {
|
||||||
|
const [year, month, day] = dateStr.split('-');
|
||||||
|
return `${day}-${month}-${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert frontend date format (DD-MM-YYYY) to backend format (YYYY-MM-DD)
|
||||||
|
export function frontendDateToBackend(dateStr: string): string {
|
||||||
|
const [day, month, year] = dateStr.split('-');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map backend delivery to frontend delivery
|
||||||
|
function mapBackendToFrontend(backend: BackendDelivery): Delivery {
|
||||||
|
return {
|
||||||
|
id: backend.id,
|
||||||
|
date: backendDateToFrontend(backend.date),
|
||||||
|
pickupLocation: backend.pickup_location,
|
||||||
|
productName: backend.product_name,
|
||||||
|
address: backend.address,
|
||||||
|
phone: backend.phone,
|
||||||
|
additionalPhone: backend.additional_phone || undefined,
|
||||||
|
hasElevator: backend.has_elevator,
|
||||||
|
comment: backend.comment,
|
||||||
|
status: backend.status,
|
||||||
|
createdAt: new Date(backend.created_at).getTime(),
|
||||||
|
updatedAt: new Date(backend.updated_at).getTime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delivery API methods
|
||||||
|
export const deliveriesApi = {
|
||||||
|
// Get deliveries by date (DD-MM-YYYY)
|
||||||
|
getByDate: async (date: string): Promise<Delivery[]> => {
|
||||||
|
const response = await api.get<GetDeliveriesResponse>(
|
||||||
|
`/api/deliveries?date=${encodeURIComponent(date)}`
|
||||||
|
);
|
||||||
|
return response.deliveries.map(mapBackendToFrontend);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get single delivery by ID
|
||||||
|
getById: async (id: string): Promise<Delivery> => {
|
||||||
|
const response = await api.get<GetDeliveryResponse>(`/api/deliveries/${id}`);
|
||||||
|
return mapBackendToFrontend(response.delivery);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get delivery counts by date
|
||||||
|
getCounts: async (): Promise<Record<string, number>> => {
|
||||||
|
const response = await api.get<GetDeliveryCountResponse>('/api/deliveries/count');
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
response.counts.forEach(({ date, count }) => {
|
||||||
|
counts[backendDateToFrontend(date)] = count;
|
||||||
|
});
|
||||||
|
return counts;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create delivery
|
||||||
|
create: async (
|
||||||
|
data: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>
|
||||||
|
): Promise<string> => {
|
||||||
|
const payload = {
|
||||||
|
date: data.date,
|
||||||
|
pickup_location: data.pickupLocation,
|
||||||
|
product_name: data.productName,
|
||||||
|
address: data.address,
|
||||||
|
phone: data.phone,
|
||||||
|
additional_phone: data.additionalPhone || '',
|
||||||
|
has_elevator: data.hasElevator,
|
||||||
|
comment: data.comment,
|
||||||
|
};
|
||||||
|
const response = await api.post<CreateDeliveryResponse>('/api/deliveries', payload);
|
||||||
|
return response.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update delivery
|
||||||
|
update: async (
|
||||||
|
id: string,
|
||||||
|
data: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>
|
||||||
|
): Promise<void> => {
|
||||||
|
const payload = {
|
||||||
|
date: data.date,
|
||||||
|
pickup_location: data.pickupLocation,
|
||||||
|
product_name: data.productName,
|
||||||
|
address: data.address,
|
||||||
|
phone: data.phone,
|
||||||
|
additional_phone: data.additionalPhone || '',
|
||||||
|
has_elevator: data.hasElevator,
|
||||||
|
comment: data.comment,
|
||||||
|
};
|
||||||
|
await api.patch<UpdateDeliveryResponse>(`/api/deliveries/${id}`, payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update delivery status
|
||||||
|
updateStatus: async (id: string, status: DeliveryStatus): Promise<void> => {
|
||||||
|
await api.patch(`/api/deliveries/${id}/status`, { status });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete delivery
|
||||||
|
delete: async (id: string): Promise<void> => {
|
||||||
|
await api.delete(`/api/deliveries/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
2
frontend/src/api/index.ts
Normal file
2
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { api, ApiError } from './client';
|
||||||
|
export { deliveriesApi, frontendDateToBackend } from './deliveries';
|
||||||
@@ -6,9 +6,10 @@ import { pickupLocationLabels } from '../../types';
|
|||||||
interface DeliveryFormProps {
|
interface DeliveryFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (delivery: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
onSubmit: (delivery: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>) => void | Promise<void>;
|
||||||
initialData?: Delivery | null;
|
initialData?: Delivery | null;
|
||||||
defaultDate?: string;
|
defaultDate?: string;
|
||||||
|
isSubmitting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pickupOptions: { value: PickupLocation; label: string }[] = [
|
const pickupOptions: { value: PickupLocation; label: string }[] = [
|
||||||
@@ -18,7 +19,7 @@ const pickupOptions: { value: PickupLocation; label: string }[] = [
|
|||||||
{ value: 'galaktika', label: pickupLocationLabels.galaktika },
|
{ value: 'galaktika', label: pickupLocationLabels.galaktika },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDate }: DeliveryFormProps) => {
|
export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDate, isSubmitting }: DeliveryFormProps) => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
date: defaultDate || new Date().toLocaleDateString('ru-RU').split('.').join('-'),
|
date: defaultDate || new Date().toLocaleDateString('ru-RU').split('.').join('-'),
|
||||||
pickupLocation: 'warehouse' as PickupLocation,
|
pickupLocation: 'warehouse' as PickupLocation,
|
||||||
@@ -85,11 +86,11 @@ export const DeliveryForm = ({ isOpen, onClose, onSubmit, initialData, defaultDa
|
|||||||
title={initialData ? 'Редактировать доставку' : 'Новая доставка'}
|
title={initialData ? 'Редактировать доставку' : 'Новая доставка'}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant="ghost" onClick={onClose}>
|
<Button variant="ghost" onClick={onClose} disabled={isSubmitting}>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form="delivery-form">
|
<Button type="submit" form="delivery-form" disabled={isSubmitting}>
|
||||||
{initialData ? 'Сохранить' : 'Создать'}
|
{isSubmitting ? 'Сохранение...' : initialData ? 'Сохранить' : 'Создать'}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,6 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import { mockDeliveries } from './utils/mockData'
|
|
||||||
import { useDeliveryStore } from './stores/deliveryStore'
|
|
||||||
|
|
||||||
// Seed mock data if no data exists
|
|
||||||
const stored = localStorage.getItem('delivery-tracker-data')
|
|
||||||
if (!stored) {
|
|
||||||
const store = useDeliveryStore.getState()
|
|
||||||
mockDeliveries.forEach(delivery => {
|
|
||||||
store.addDelivery(delivery)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, Printer, ChevronRight, CalendarDays } from 'lucide-react';
|
import { Plus, Printer, ChevronRight, CalendarDays } from 'lucide-react';
|
||||||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isToday } from 'date-fns';
|
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isToday } from 'date-fns';
|
||||||
import { ru } from 'date-fns/locale';
|
import { ru } from 'date-fns/locale';
|
||||||
import { useDeliveryStore } from '../stores/deliveryStore';
|
import { useDeliveryStore } from '../stores/deliveryStore';
|
||||||
|
import type { Delivery } from '../types';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
|
|
||||||
@@ -12,21 +13,36 @@ interface DashboardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => {
|
export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => {
|
||||||
const deliveries = useDeliveryStore(state => state.deliveries);
|
const deliveryCounts = useDeliveryStore(state => state.deliveryCounts);
|
||||||
|
const fetchDeliveryCounts = useDeliveryStore(state => state.fetchDeliveryCounts);
|
||||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
|
|
||||||
|
// Fetch counts on mount
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDeliveryCounts();
|
||||||
|
}, [fetchDeliveryCounts]);
|
||||||
|
|
||||||
const monthStart = startOfMonth(currentMonth);
|
const monthStart = startOfMonth(currentMonth);
|
||||||
const monthEnd = endOfMonth(currentMonth);
|
const monthEnd = endOfMonth(currentMonth);
|
||||||
const days = eachDayOfInterval({ start: monthStart, end: monthEnd });
|
const days = eachDayOfInterval({ start: monthStart, end: monthEnd });
|
||||||
|
|
||||||
const getCountForDate = (date: Date) => {
|
const getCountForDate = (date: Date) => {
|
||||||
const dateStr = format(date, 'dd-MM-yyyy');
|
const dateStr = format(date, 'dd-MM-yyyy');
|
||||||
return deliveries.filter(d => d.date === dateStr).length;
|
return deliveryCounts[dateStr] || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrintDay = (date: Date) => {
|
const handlePrintDay = (date: Date) => {
|
||||||
const dateStr = format(date, 'dd-MM-yyyy');
|
const dateStr = format(date, 'dd-MM-yyyy');
|
||||||
const dayDeliveries = deliveries.filter(d => d.date === dateStr);
|
const fetchDeliveriesByDate = useDeliveryStore.getState().fetchDeliveriesByDate;
|
||||||
|
|
||||||
|
// Fetch and print
|
||||||
|
fetchDeliveriesByDate(dateStr).then(() => {
|
||||||
|
const deliveries = useDeliveryStore.getState().deliveries;
|
||||||
|
printDeliveries(date, deliveries);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const printDeliveries = (date: Date, dayDeliveries: Delivery[]) => {
|
||||||
|
|
||||||
const printWindow = window.open('', '_blank');
|
const printWindow = window.open('', '_blank');
|
||||||
if (!printWindow) return;
|
if (!printWindow) return;
|
||||||
@@ -57,7 +73,7 @@ export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => {
|
|||||||
<th>Телефон</th>
|
<th>Телефон</th>
|
||||||
<th>Комментарий</th>
|
<th>Комментарий</th>
|
||||||
</tr>
|
</tr>
|
||||||
${dayDeliveries.map(d => `
|
${dayDeliveries.map((d: Delivery) => `
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class="status-${d.status}">${d.status === 'new' ? 'Новое' : 'Доставлено'}</span></td>
|
<td><span class="status-${d.status}">${d.status === 'new' ? 'Новое' : 'Доставлено'}</span></td>
|
||||||
<td>${d.pickupLocation === 'warehouse' ? 'Склад' : d.pickupLocation === 'symbat' ? 'Сымбат' : d.pickupLocation === 'nursaya' ? 'Нурсая' : 'Галактика'}</td>
|
<td>${d.pickupLocation === 'warehouse' ? 'Склад' : d.pickupLocation === 'symbat' ? 'Сымбат' : d.pickupLocation === 'nursaya' ? 'Нурсая' : 'Галактика'}</td>
|
||||||
@@ -74,7 +90,7 @@ export const Dashboard = ({ onDateSelect, onAddDelivery }: DashboardProps) => {
|
|||||||
|
|
||||||
printWindow.document.write(html);
|
printWindow.document.write(html);
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
printWindow.print();
|
printWindow?.print();
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateMonth = (direction: 'prev' | 'next') => {
|
const navigateMonth = (direction: 'prev' | 'next') => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ArrowLeft, Filter } from 'lucide-react';
|
import { ArrowLeft, Filter, Loader2, AlertCircle } from 'lucide-react';
|
||||||
import { useDeliveryStore } from '../stores/deliveryStore';
|
import { useDeliveryStore } from '../stores/deliveryStore';
|
||||||
import { DeliveryList as DeliveryListComponent } from '../components/delivery/DeliveryList';
|
import { DeliveryList as DeliveryListComponent } from '../components/delivery/DeliveryList';
|
||||||
import { DeliveryForm } from '../components/delivery/DeliveryForm';
|
import { DeliveryForm } from '../components/delivery/DeliveryForm';
|
||||||
@@ -15,16 +15,26 @@ interface DeliveryListPageProps {
|
|||||||
|
|
||||||
export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps) => {
|
export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps) => {
|
||||||
const deliveries = useDeliveryStore(state => state.deliveries);
|
const deliveries = useDeliveryStore(state => state.deliveries);
|
||||||
|
const isLoading = useDeliveryStore(state => state.isLoading);
|
||||||
|
const error = useDeliveryStore(state => state.error);
|
||||||
|
const fetchDeliveriesByDate = useDeliveryStore(state => state.fetchDeliveriesByDate);
|
||||||
const toggleStatus = useDeliveryStore(state => state.toggleStatus);
|
const toggleStatus = useDeliveryStore(state => state.toggleStatus);
|
||||||
const deleteDelivery = useDeliveryStore(state => state.deleteDelivery);
|
const deleteDelivery = useDeliveryStore(state => state.deleteDelivery);
|
||||||
const updateDelivery = useDeliveryStore(state => state.updateDelivery);
|
const updateDelivery = useDeliveryStore(state => state.updateDelivery);
|
||||||
const addDelivery = useDeliveryStore(state => state.addDelivery);
|
const addDelivery = useDeliveryStore(state => state.addDelivery);
|
||||||
|
const clearError = useDeliveryStore(state => state.clearError);
|
||||||
|
|
||||||
|
// Fetch deliveries when date changes
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDeliveriesByDate(selectedDate);
|
||||||
|
}, [selectedDate, fetchDeliveriesByDate]);
|
||||||
|
|
||||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||||
const [editingDelivery, setEditingDelivery] = useState<Delivery | null>(null);
|
const [editingDelivery, setEditingDelivery] = useState<Delivery | null>(null);
|
||||||
const [pickupFilter, setPickupFilter] = useState<PickupLocation | 'all'>('all');
|
const [pickupFilter, setPickupFilter] = useState<PickupLocation | 'all'>('all');
|
||||||
|
|
||||||
const dayDeliveries = deliveries.filter(d => d.date === selectedDate);
|
// Use all deliveries from store (already filtered by API)
|
||||||
|
const dayDeliveries = deliveries;
|
||||||
const filteredDeliveries = pickupFilter === 'all'
|
const filteredDeliveries = pickupFilter === 'all'
|
||||||
? dayDeliveries
|
? dayDeliveries
|
||||||
: dayDeliveries.filter(d => d.pickupLocation === pickupFilter);
|
: dayDeliveries.filter(d => d.pickupLocation === pickupFilter);
|
||||||
@@ -37,8 +47,11 @@ export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps
|
|||||||
{ value: 'galaktika', label: pickupLocationLabels.galaktika },
|
{ value: 'galaktika', label: pickupLocationLabels.galaktika },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleStatusChange = (id: string) => {
|
const handleStatusChange = async (id: string) => {
|
||||||
toggleStatus(id);
|
const delivery = deliveries.find(d => d.id === id);
|
||||||
|
if (delivery) {
|
||||||
|
await toggleStatus(id, delivery.status);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (delivery: Delivery) => {
|
const handleEdit = (delivery: Delivery) => {
|
||||||
@@ -46,19 +59,27 @@ export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps
|
|||||||
setIsFormOpen(true);
|
setIsFormOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
if (confirm('Удалить эту доставку?')) {
|
if (confirm('Удалить эту доставку?')) {
|
||||||
deleteDelivery(id);
|
try {
|
||||||
|
await deleteDelivery(id);
|
||||||
|
} catch {
|
||||||
|
// Error is handled by store
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (data: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>) => {
|
const handleSubmit = async (data: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
if (editingDelivery) {
|
try {
|
||||||
updateDelivery(editingDelivery.id, data);
|
if (editingDelivery) {
|
||||||
} else {
|
await updateDelivery(editingDelivery.id, data);
|
||||||
addDelivery(data);
|
} else {
|
||||||
|
await addDelivery(data);
|
||||||
|
}
|
||||||
|
setEditingDelivery(null);
|
||||||
|
} catch {
|
||||||
|
// Error is handled by store
|
||||||
}
|
}
|
||||||
setEditingDelivery(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
@@ -94,14 +115,30 @@ export const DeliveryListPage = ({ selectedDate, onBack }: DeliveryListPageProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeliveryListComponent
|
{isLoading ? (
|
||||||
deliveries={filteredDeliveries}
|
<div className="flex items-center justify-center py-12">
|
||||||
onStatusChange={handleStatusChange}
|
<Loader2 className="w-8 h-8 animate-spin text-[#1B263B]" />
|
||||||
onEdit={handleEdit}
|
</div>
|
||||||
onDelete={handleDelete}
|
) : error ? (
|
||||||
onAdd={handleAdd}
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3">
|
||||||
date={selectedDate}
|
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||||
/>
|
<div className="flex-1">
|
||||||
|
<p className="text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => { clearError(); fetchDeliveriesByDate(selectedDate); }}>
|
||||||
|
Повторить
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DeliveryListComponent
|
||||||
|
deliveries={filteredDeliveries}
|
||||||
|
onStatusChange={handleStatusChange}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onAdd={handleAdd}
|
||||||
|
date={selectedDate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DeliveryForm
|
<DeliveryForm
|
||||||
isOpen={isFormOpen}
|
isOpen={isFormOpen}
|
||||||
|
|||||||
@@ -1,83 +1,164 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { deliveriesApi } from '../api';
|
||||||
import type { Delivery } from '../types';
|
import type { Delivery, DeliveryStatus } from '../types';
|
||||||
|
|
||||||
interface DeliveryState {
|
interface DeliveryState {
|
||||||
|
// Data
|
||||||
deliveries: Delivery[];
|
deliveries: Delivery[];
|
||||||
addDelivery: (delivery: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>) => void;
|
deliveryCounts: Record<string, number>;
|
||||||
updateDelivery: (id: string, updates: Partial<Delivery>) => void;
|
|
||||||
deleteDelivery: (id: string) => void;
|
// Loading states
|
||||||
toggleStatus: (id: string) => void;
|
isLoading: boolean;
|
||||||
|
isLoadingCounts: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchDeliveriesByDate: (date: string) => Promise<void>;
|
||||||
|
fetchDeliveryCounts: () => Promise<void>;
|
||||||
|
addDelivery: (delivery: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>) => Promise<void>;
|
||||||
|
updateDelivery: (id: string, updates: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>) => Promise<void>;
|
||||||
|
deleteDelivery: (id: string) => Promise<void>;
|
||||||
|
toggleStatus: (id: string, currentStatus: DeliveryStatus) => Promise<void>;
|
||||||
getDeliveriesByDate: (date: string) => Delivery[];
|
getDeliveriesByDate: (date: string) => Delivery[];
|
||||||
getDeliveriesByDateRange: (startDate: string, endDate: string) => Delivery[];
|
getDeliveriesByDateRange: (startDate: string, endDate: string) => Delivery[];
|
||||||
getDeliveryCountsByDate: () => Record<string, number>;
|
getDeliveryCountsByDate: () => Record<string, number>;
|
||||||
|
clearError: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'delivery-tracker-data';
|
export const useDeliveryStore = create<DeliveryState>()((set, get) => ({
|
||||||
|
// Initial state
|
||||||
|
deliveries: [],
|
||||||
|
deliveryCounts: {},
|
||||||
|
isLoading: false,
|
||||||
|
isLoadingCounts: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
export const useDeliveryStore = create<DeliveryState>()(
|
// Fetch deliveries for a specific date
|
||||||
persist(
|
fetchDeliveriesByDate: async (date: string) => {
|
||||||
(set, get) => ({
|
set({ isLoading: true, error: null });
|
||||||
deliveries: [],
|
try {
|
||||||
|
const deliveries = await deliveriesApi.getByDate(date);
|
||||||
addDelivery: (delivery) => {
|
set({ deliveries, isLoading: false });
|
||||||
const now = Date.now();
|
} catch (err) {
|
||||||
const newDelivery: Delivery = {
|
set({
|
||||||
...delivery,
|
error: err instanceof Error ? err.message : 'Failed to fetch deliveries',
|
||||||
id: crypto.randomUUID(),
|
isLoading: false,
|
||||||
createdAt: now,
|
});
|
||||||
updatedAt: now,
|
|
||||||
};
|
|
||||||
set((state) => ({
|
|
||||||
deliveries: [...state.deliveries, newDelivery],
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
updateDelivery: (id, updates) => {
|
|
||||||
set((state) => ({
|
|
||||||
deliveries: state.deliveries.map((d) =>
|
|
||||||
d.id === id ? { ...d, ...updates, updatedAt: Date.now() } : d
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteDelivery: (id) => {
|
|
||||||
set((state) => ({
|
|
||||||
deliveries: state.deliveries.filter((d) => d.id !== id),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleStatus: (id) => {
|
|
||||||
set((state) => ({
|
|
||||||
deliveries: state.deliveries.map((d) =>
|
|
||||||
d.id === id
|
|
||||||
? { ...d, status: d.status === 'new' ? 'delivered' : 'new', updatedAt: Date.now() }
|
|
||||||
: d
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
getDeliveriesByDate: (date) => {
|
|
||||||
return get().deliveries.filter((d) => d.date === date);
|
|
||||||
},
|
|
||||||
|
|
||||||
getDeliveriesByDateRange: (startDate, endDate) => {
|
|
||||||
return get().deliveries.filter((d) => {
|
|
||||||
const date = d.date;
|
|
||||||
return date >= startDate && date <= endDate;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getDeliveryCountsByDate: () => {
|
|
||||||
const counts: Record<string, number> = {};
|
|
||||||
get().deliveries.forEach((d) => {
|
|
||||||
counts[d.date] = (counts[d.date] || 0) + 1;
|
|
||||||
});
|
|
||||||
return counts;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: STORAGE_KEY,
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
);
|
|
||||||
|
// Fetch delivery counts for calendar
|
||||||
|
fetchDeliveryCounts: async () => {
|
||||||
|
set({ isLoadingCounts: true, error: null });
|
||||||
|
try {
|
||||||
|
const counts = await deliveriesApi.getCounts();
|
||||||
|
set({ deliveryCounts: counts, isLoadingCounts: false });
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
error: err instanceof Error ? err.message : 'Failed to fetch counts',
|
||||||
|
isLoadingCounts: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add new delivery
|
||||||
|
addDelivery: async (delivery) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
await deliveriesApi.create(delivery);
|
||||||
|
// Refresh deliveries for that date
|
||||||
|
await get().fetchDeliveriesByDate(delivery.date);
|
||||||
|
// Refresh counts
|
||||||
|
await get().fetchDeliveryCounts();
|
||||||
|
set({ isLoading: false });
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
error: err instanceof Error ? err.message : 'Failed to create delivery',
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update delivery
|
||||||
|
updateDelivery: async (id, updates) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
await deliveriesApi.update(id, updates);
|
||||||
|
// Refresh deliveries for that date
|
||||||
|
await get().fetchDeliveriesByDate(updates.date);
|
||||||
|
// Refresh counts (in case date changed)
|
||||||
|
await get().fetchDeliveryCounts();
|
||||||
|
set({ isLoading: false });
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
error: err instanceof Error ? err.message : 'Failed to update delivery',
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete delivery
|
||||||
|
deleteDelivery: async (id) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
await deliveriesApi.delete(id);
|
||||||
|
// Remove from local state
|
||||||
|
set((state) => ({
|
||||||
|
deliveries: state.deliveries.filter((d) => d.id !== id),
|
||||||
|
isLoading: false,
|
||||||
|
}));
|
||||||
|
// Refresh counts
|
||||||
|
await get().fetchDeliveryCounts();
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
error: err instanceof Error ? err.message : 'Failed to delete delivery',
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toggle delivery status
|
||||||
|
toggleStatus: async (id, currentStatus) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const newStatus = currentStatus === 'new' ? 'delivered' : 'new';
|
||||||
|
await deliveriesApi.updateStatus(id, newStatus);
|
||||||
|
// Update local state
|
||||||
|
set((state) => ({
|
||||||
|
deliveries: state.deliveries.map((d) =>
|
||||||
|
d.id === id
|
||||||
|
? { ...d, status: newStatus, updatedAt: Date.now() }
|
||||||
|
: d
|
||||||
|
),
|
||||||
|
isLoading: false,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
set({
|
||||||
|
error: err instanceof Error ? err.message : 'Failed to update status',
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Getters (local filtering)
|
||||||
|
getDeliveriesByDate: (date) => {
|
||||||
|
return get().deliveries.filter((d) => d.date === date);
|
||||||
|
},
|
||||||
|
|
||||||
|
getDeliveriesByDateRange: (startDate, endDate) => {
|
||||||
|
return get().deliveries.filter((d) => {
|
||||||
|
const date = d.date;
|
||||||
|
return date >= startDate && date <= endDate;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getDeliveryCountsByDate: () => {
|
||||||
|
return get().deliveryCounts;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import type { Delivery } from '../types';
|
|
||||||
|
|
||||||
export const mockDeliveries: Omit<Delivery, 'id' | 'createdAt' | 'updatedAt'>[] = [
|
|
||||||
{
|
|
||||||
date: new Date().toLocaleDateString('ru-RU').split('.').join('-'),
|
|
||||||
pickupLocation: 'symbat',
|
|
||||||
productName: 'Диван прямой Милан',
|
|
||||||
address: 'ул. Ленина, д. 10, кв. 25',
|
|
||||||
phone: '+7 (771)-123-45-67',
|
|
||||||
additionalPhone: '',
|
|
||||||
hasElevator: true,
|
|
||||||
comment: 'Доставить после 18:00',
|
|
||||||
status: 'new',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: new Date().toLocaleDateString('ru-RU').split('.').join('-'),
|
|
||||||
pickupLocation: 'warehouse',
|
|
||||||
productName: 'Шкаф двухдверный',
|
|
||||||
address: 'ул. Гагарина, д. 5, офис 304',
|
|
||||||
phone: '+7 (777)-234-56-78',
|
|
||||||
additionalPhone: '+7 (702)-111-22-33',
|
|
||||||
hasElevator: false,
|
|
||||||
comment: 'Предварительно позвонить',
|
|
||||||
status: 'new',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: new Date(Date.now() + 86400000).toLocaleDateString('ru-RU').split('.').join('-'),
|
|
||||||
pickupLocation: 'nursaya',
|
|
||||||
productName: 'Стол обеденный + 4 стула',
|
|
||||||
address: 'пр. Мира, д. 15',
|
|
||||||
phone: '+7 (705)-345-67-89',
|
|
||||||
additionalPhone: '',
|
|
||||||
hasElevator: true,
|
|
||||||
comment: '',
|
|
||||||
status: 'new',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: new Date(Date.now() - 86400000).toLocaleDateString('ru-RU').split('.').join('-'),
|
|
||||||
pickupLocation: 'galaktika',
|
|
||||||
productName: 'Матрас ортопедический 160x200',
|
|
||||||
address: 'ул. Пушкина, д. 20',
|
|
||||||
phone: '+7 (701)-456-78-90',
|
|
||||||
additionalPhone: '',
|
|
||||||
hasElevator: false,
|
|
||||||
comment: 'Доставлено успешно',
|
|
||||||
status: 'delivered',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: new Date(Date.now() + 172800000).toLocaleDateString('ru-RU').split('.').join('-'),
|
|
||||||
pickupLocation: 'warehouse',
|
|
||||||
productName: 'Кресло реклайнер',
|
|
||||||
address: 'ул. Чехова, д. 8, кв. 12',
|
|
||||||
phone: '+7 (776)-567-89-01',
|
|
||||||
additionalPhone: '',
|
|
||||||
hasElevator: true,
|
|
||||||
comment: 'Подъезд с торца',
|
|
||||||
status: 'new',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
Reference in New Issue
Block a user