454 lines
14 KiB
Markdown
454 lines
14 KiB
Markdown
# Manual Delivery Agreement Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Make manager/logistician manual delivery agreement safe and clear: only future delivery dates are allowed, controls match the app theme, order group counters are correct, and confusing technical fields are removed from the card.
|
||
|
||
**Architecture:** Keep the workflow centered on `order_groups`. The UI validates future dates before submit, the Edge Function enforces the same rule server-side, and the repository maps partial `order_groups` rows into a clean view model for dashboards and cards.
|
||
|
||
**Tech Stack:** React 18, Vite, Vitest, Supabase JS, Supabase Edge Functions in Deno, Tailwind utility classes with CSS variables.
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
- Modify: `src/components/orders/OrderDetailPanel.jsx`
|
||
Responsible for the order group detail card, manual agreement UI, local form validation, and hiding confusing technical fields.
|
||
|
||
- Modify: `src/components/orders/OrderDetailPanel.test.jsx`
|
||
Server-rendered component tests for the detail card, editable controls, and visible copy.
|
||
|
||
- Modify: `src/services/supabase/orderGroupRepository.js`
|
||
Responsible for mapping raw `order_groups` rows into the frontend delivery group model and saving manual delivery choices through the Edge Function.
|
||
|
||
- Modify: `src/services/supabase/orderGroupRepository.test.js`
|
||
Mapping tests for missing counters, real delivery dates, and fallback behavior.
|
||
|
||
- Modify: `supabase/functions/update-order-group-delivery-choice/index.ts`
|
||
Server-side manual agreement validation and update logic.
|
||
|
||
- Optional modify: `src/components/UI/Select.jsx`
|
||
Only touch if other select controls still need a global design correction after the manual agreement block switches to app-styled buttons.
|
||
|
||
- Optional modify: `docs/sql/order-groups-manual-delivery-choice.sql`
|
||
Only touch if database constraints are added later. Current requirement can be enforced in the Edge Function.
|
||
|
||
---
|
||
|
||
## Chunk 1: Data Mapping Correctness
|
||
|
||
### Task 1: Stop Showing Fake Delivery Dates
|
||
|
||
**Files:**
|
||
- Modify: `src/services/supabase/orderGroupRepository.js`
|
||
- Test: `src/services/supabase/orderGroupRepository.test.js`
|
||
|
||
- [ ] **Step 1: Write the failing mapping test**
|
||
|
||
Add a case where `customer_date` exists but `delivery_date` is null.
|
||
|
||
```js
|
||
const group = mapOrderGroupRowToDeliveryGroup({
|
||
id: "group-without-delivery-date",
|
||
group_key: "9781632663|28.04.26",
|
||
customer_date: "28.04.26",
|
||
order_numbers: ["СФ Т\\ЕА-26979"],
|
||
status: "ready_for_notification",
|
||
delivery_status: "pending_confirmation",
|
||
created_at: "2026-05-05 09:43:53.750061+00",
|
||
updated_at: "2026-05-05 09:43:53.750061+00",
|
||
});
|
||
|
||
expect(group.customerDate).toBe("28.04.26");
|
||
expect(group.deliveryDate).toBe("");
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test**
|
||
|
||
Run: `npm test -- --run src/services/supabase/orderGroupRepository.test.js`
|
||
|
||
Expected before implementation: FAIL because `deliveryDate` is incorrectly filled from `customerDate`.
|
||
|
||
- [ ] **Step 3: Implement the mapping fix**
|
||
|
||
In `mapOrderGroupRowToDeliveryGroup`, set:
|
||
|
||
```js
|
||
const deliveryDate = normalizeText(row.delivery_date);
|
||
```
|
||
|
||
Do not fall back to `customerDate` for actual delivery agreement data.
|
||
|
||
- [ ] **Step 4: Run the focused test**
|
||
|
||
Run: `npm test -- --run src/services/supabase/orderGroupRepository.test.js`
|
||
|
||
Expected: PASS.
|
||
|
||
### Task 2: Infer Counters When `order_groups` Counter Columns Are Empty
|
||
|
||
**Files:**
|
||
- Modify: `src/services/supabase/orderGroupRepository.js`
|
||
- Test: `src/services/supabase/orderGroupRepository.test.js`
|
||
|
||
- [ ] **Step 1: Write the failing counter test**
|
||
|
||
Use a real-shaped row where `order_numbers` has values, but `orders_count`, `ready_count`, and `not_ready_count` are missing.
|
||
|
||
```js
|
||
const group = mapOrderGroupRowToDeliveryGroup({
|
||
id: "group-without-counters",
|
||
group_key: "9781632663|28.04.26",
|
||
order_numbers: ["СФ Т\\ЕА-26979"],
|
||
status: "ready_for_notification",
|
||
delivery_status: "pending_confirmation",
|
||
created_at: "2026-05-05 09:43:53.750061+00",
|
||
updated_at: "2026-05-05 09:43:53.750061+00",
|
||
});
|
||
|
||
expect(group.ordersCount).toBe(1);
|
||
expect(group.readyCount).toBe(1);
|
||
expect(group.notReadyCount).toBe(0);
|
||
```
|
||
|
||
- [ ] **Step 2: Run the focused test**
|
||
|
||
Run: `npm test -- --run src/services/supabase/orderGroupRepository.test.js`
|
||
|
||
Expected before implementation: FAIL with `0` counters.
|
||
|
||
- [ ] **Step 3: Implement fallback counters**
|
||
|
||
Use `order_numbers.length` as a fallback for total count. For `status === "ready_for_notification"`, infer `readyCount` as `ordersCount` when explicit ready counters are absent.
|
||
|
||
```js
|
||
const orderNumbers = toStringArray(row.order_numbers);
|
||
const inferredOrderCount = orderNumbers.length;
|
||
const ordersCount = toNumber(row.orders_count ?? row.orders_total ?? row.legacy_orders_total, inferredOrderCount);
|
||
const readyCount = toNumber(
|
||
row.ready_count ?? row.orders_ready ?? row.legacy_orders_ready,
|
||
row.status === "ready_for_notification" ? ordersCount : 0,
|
||
);
|
||
const notReadyCount = toNumber(
|
||
row.not_ready_count ?? row.orders_not_ready ?? row.legacy_orders_not_ready,
|
||
Math.max(ordersCount - readyCount, 0),
|
||
);
|
||
```
|
||
|
||
- [ ] **Step 4: Run mapping tests**
|
||
|
||
Run: `npm test -- --run src/services/supabase/orderGroupRepository.test.js`
|
||
|
||
Expected: PASS.
|
||
|
||
---
|
||
|
||
## Chunk 2: Manual Agreement UI
|
||
|
||
### Task 3: Replace Native Date Input With Themed Future-Date Picker
|
||
|
||
**Files:**
|
||
- Modify: `src/components/orders/OrderDetailPanel.jsx`
|
||
- Test: `src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
- [ ] **Step 1: Add date helper functions**
|
||
|
||
Add local helpers near `normalizeDateForInput`:
|
||
|
||
```js
|
||
const toDateKey = (date) => {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||
const day = String(date.getDate()).padStart(2, "0");
|
||
return `${year}-${month}-${day}`;
|
||
};
|
||
|
||
const addDays = (date, amount) => {
|
||
const nextDate = new Date(date);
|
||
nextDate.setDate(nextDate.getDate() + amount);
|
||
return nextDate;
|
||
};
|
||
|
||
const getTomorrowDateKey = () => toDateKey(addDays(new Date(), 1));
|
||
const isFutureDeliveryDate = (value) => Boolean(value) && value >= getTomorrowDateKey();
|
||
```
|
||
|
||
- [ ] **Step 2: Default the editable form to tomorrow**
|
||
|
||
When the selected group has no valid future `deliveryDate`, initialize the manual form with tomorrow.
|
||
|
||
```js
|
||
const normalizedDeliveryDate = normalizeDateForInput(order?.deliveryDate);
|
||
setDeliveryDate(isFutureDeliveryDate(normalizedDeliveryDate) ? normalizedDeliveryDate : getTomorrowDateKey());
|
||
```
|
||
|
||
- [ ] **Step 3: Replace `<Input type="date">`**
|
||
|
||
Render a styled button that opens a compact 21-day date grid. The grid should use app CSS variables: `--color-card`, `--color-surface`, `--color-border`, `--color-accent`, `--color-accent-soft`, `--color-text`, and `--color-text-muted`.
|
||
|
||
- [ ] **Step 4: Test editable controls**
|
||
|
||
Update `OrderDetailPanel.test.jsx` so editable markup includes:
|
||
|
||
```js
|
||
expect(editableMarkup).toContain("Ближайшие даты");
|
||
expect(editableMarkup).toContain("Согласовать");
|
||
```
|
||
|
||
- [ ] **Step 5: Run component test**
|
||
|
||
Run: `npm test -- --run src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
Expected: PASS.
|
||
|
||
### Task 4: Replace Native Time Select With Themed Segmented Buttons
|
||
|
||
**Files:**
|
||
- Modify: `src/components/orders/OrderDetailPanel.jsx`
|
||
- Test: `src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
- [ ] **Step 1: Remove `Select` import from `OrderDetailPanel.jsx`**
|
||
|
||
The manual agreement block should no longer use the native dropdown.
|
||
|
||
- [ ] **Step 2: Render time options as buttons**
|
||
|
||
Use `DELIVERY_TIME_OPTIONS`:
|
||
|
||
```js
|
||
const DELIVERY_TIME_OPTIONS = ["Первая половина дня", "Вторая половина дня"];
|
||
```
|
||
|
||
Each option should be a `button type="button"` with `aria-pressed={deliveryTime === option}` and selected styling through app CSS variables.
|
||
|
||
- [ ] **Step 3: Ensure mobile layout is comfortable**
|
||
|
||
Use a responsive grid:
|
||
|
||
```jsx
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
```
|
||
|
||
- [ ] **Step 4: Run component test**
|
||
|
||
Run: `npm test -- --run src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
Expected: PASS.
|
||
|
||
---
|
||
|
||
## Chunk 3: Validation And Server Enforcement
|
||
|
||
### Task 5: Block Today And Past Dates In The UI
|
||
|
||
**Files:**
|
||
- Modify: `src/components/orders/OrderDetailPanel.jsx`
|
||
- Test: `src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
- [ ] **Step 1: Add submit validation**
|
||
|
||
Before calling `onSaveManualDeliveryChoice`, check:
|
||
|
||
```js
|
||
if (!isFutureDeliveryDate(deliveryDate)) {
|
||
setFormMessage("Выберите дату доставки позже сегодняшнего дня.");
|
||
return;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Add a client-side interaction test if the test setup supports events**
|
||
|
||
If this component is only tested with `renderToStaticMarkup`, keep validation covered by a server-side Edge Function test/check instead. Do not add a brittle DOM test just to satisfy coverage.
|
||
|
||
- [ ] **Step 3: Run component tests**
|
||
|
||
Run: `npm test -- --run src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
Expected: PASS.
|
||
|
||
### Task 6: Enforce Future Dates In Edge Function
|
||
|
||
**Files:**
|
||
- Modify: `supabase/functions/update-order-group-delivery-choice/index.ts`
|
||
|
||
- [ ] **Step 1: Add date comparison helpers**
|
||
|
||
```ts
|
||
const getTodayKey = () => new Date().toISOString().slice(0, 10);
|
||
const isFutureDeliveryDate = (value: string) => isValidDate(value) && value > getTodayKey();
|
||
```
|
||
|
||
- [ ] **Step 2: Replace date validation**
|
||
|
||
Change:
|
||
|
||
```ts
|
||
if (!isValidDate(deliveryDate)) {
|
||
return jsonResponse({ ok: false, error: "Valid deliveryDate is required" }, 400, corsHeaders);
|
||
}
|
||
```
|
||
|
||
to:
|
||
|
||
```ts
|
||
if (!isFutureDeliveryDate(deliveryDate)) {
|
||
return jsonResponse({ ok: false, error: "Future deliveryDate is required" }, 400, corsHeaders);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Run Deno check**
|
||
|
||
Run: `deno check supabase/functions/update-order-group-delivery-choice/index.ts`
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 4: Deploy function after local verification**
|
||
|
||
Run when ready:
|
||
|
||
```bash
|
||
supabase functions deploy update-order-group-delivery-choice
|
||
```
|
||
|
||
Expected: deployed function rejects today/past dates even if someone bypasses the UI.
|
||
|
||
---
|
||
|
||
## Chunk 4: Detail Card Cleanup
|
||
|
||
### Task 7: Remove Confusing Legacy Fields From The Card
|
||
|
||
**Files:**
|
||
- Modify: `src/components/orders/OrderDetailPanel.jsx`
|
||
- Test: `src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
- [ ] **Step 1: Change empty value wording**
|
||
|
||
Use `Нет данных` for generic missing data instead of `Не указано`, except for binary fields where the user expects `Да` or `Нет`.
|
||
|
||
```js
|
||
const renderValue = (value) => value || "Нет данных";
|
||
```
|
||
|
||
- [ ] **Step 2: Show SMS as a binary value**
|
||
|
||
Change the SMS field:
|
||
|
||
```jsx
|
||
<p className="mt-1 font-medium">{order.smsSentAt ? "Да" : "Нет"}</p>
|
||
```
|
||
|
||
- [ ] **Step 3: Hide legacy customer**
|
||
|
||
Remove the visible `Клиент из старых данных` field from the card.
|
||
|
||
- [ ] **Step 4: Hide empty technical fields**
|
||
|
||
Only render `Создано из обмена` and `Ключ источника` when values exist. Do not show `Нет данных` for these technical fields.
|
||
|
||
- [ ] **Step 5: Run component tests**
|
||
|
||
Run: `npm test -- --run src/components/orders/OrderDetailPanel.test.jsx`
|
||
|
||
Expected: PASS.
|
||
|
||
---
|
||
|
||
## Chunk 5: Verification
|
||
|
||
### Task 8: Run Focused Tests
|
||
|
||
**Files:**
|
||
- Test: `src/components/orders/OrderDetailPanel.test.jsx`
|
||
- Test: `src/services/supabase/orderGroupRepository.test.js`
|
||
- Test: `src/pages/DashboardPage.test.jsx`
|
||
|
||
- [ ] **Step 1: Run focused frontend tests**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm test -- --run src/components/orders/OrderDetailPanel.test.jsx src/services/supabase/orderGroupRepository.test.js src/pages/DashboardPage.test.jsx
|
||
```
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 2: Run Edge Function type check**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
deno check supabase/functions/update-order-group-delivery-choice/index.ts
|
||
```
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 3: Run production build**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
Expected: PASS.
|
||
|
||
### Task 9: Manual Browser Verification
|
||
|
||
**Files:**
|
||
- Manual check: `http://localhost:5174/dashboard`
|
||
|
||
- [ ] **Step 1: Open an order group as manager/logistician**
|
||
|
||
Expected: card shows order counters based on available orders, not misleading `0/0` when `order_numbers` has values.
|
||
|
||
- [ ] **Step 2: Check manual agreement block**
|
||
|
||
Expected: date picker starts from tomorrow, not today or an old customer date.
|
||
|
||
- [ ] **Step 3: Select date and half-day**
|
||
|
||
Expected: controls visually match dark/light theme, no native browser dropdown styling dominates the UI.
|
||
|
||
- [ ] **Step 4: Save manual agreement**
|
||
|
||
Expected: valid future date saves; today/past date cannot be sent from UI.
|
||
|
||
- [ ] **Step 5: Check additional data block**
|
||
|
||
Expected: `SMS отправлено` shows `Да` or `Нет`; no `Клиент из старых данных`; empty technical fields are hidden.
|
||
|
||
---
|
||
|
||
## Commit Plan
|
||
|
||
- [ ] **Commit 1: Data mapping**
|
||
|
||
```bash
|
||
git add src/services/supabase/orderGroupRepository.js src/services/supabase/orderGroupRepository.test.js
|
||
git commit -m "fix(order-groups): normalize delivery group counters"
|
||
```
|
||
|
||
- [ ] **Commit 2: Manual agreement UI**
|
||
|
||
```bash
|
||
git add src/components/orders/OrderDetailPanel.jsx src/components/orders/OrderDetailPanel.test.jsx
|
||
git commit -m "feat(order-groups): improve manual delivery agreement"
|
||
```
|
||
|
||
- [ ] **Commit 3: Edge Function validation**
|
||
|
||
```bash
|
||
git add supabase/functions/update-order-group-delivery-choice/index.ts
|
||
git commit -m "fix(edge): require future delivery dates"
|
||
```
|
||
|
||
---
|
||
|
||
## Rollout Notes
|
||
|
||
- The frontend change is immediate after deploy.
|
||
- The Edge Function must be deployed separately with `supabase functions deploy update-order-group-delivery-choice`.
|
||
- Existing rows with old `delivery_date` values will still contain those dates in the database. This plan prevents new manual agreements from writing today or past dates.
|
||
- Temporary open RLS used for testing should be revisited before production hardening.
|