Merge branch 'freight/feature/vehicle_2' of github.com:Tria-plc/edr-platform into freight/feature/vehicle_2

This commit is contained in:
yaschalew
2026-06-29 11:40:42 +03:00
4 changed files with 150 additions and 25 deletions

View File

@@ -142,6 +142,31 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: ./scripts/deploy/create-npmrc.sh run: ./scripts/deploy/create-npmrc.sh
- name: Resolve env file path for ${{ matrix.service }}
if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service)
run: |
case "${{ matrix.service }}" in
passenger-api) echo "SERVICE_ENV_FILE=apps/edr-passenger-api/.env" >> "$GITHUB_ENV" ;;
payment-api) echo "SERVICE_ENV_FILE=apps/edr-payment-api/.env" >> "$GITHUB_ENV" ;;
esac
- name: Build migration image for ${{ matrix.service }}
if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service)
run: |
set -euo pipefail
docker build \
--secret id=npmrc,src=.npmrc \
--target migration \
-f "apps/edr-${{ matrix.service }}/Dockerfile" \
-t "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}-migration" \
.
- name: Run migrations for ${{ matrix.service }}
if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service)
run: |
set -euo pipefail
docker run --rm --env-file "${SERVICE_ENV_FILE}" "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}-migration"
- name: Build ${{ matrix.service }} - name: Build ${{ matrix.service }}
run: | run: |
set -euo pipefail set -euo pipefail

View File

@@ -18,10 +18,12 @@ import {
Button, Button,
Card, Card,
Checkbox, Checkbox,
DateInput,
Divider, Divider,
Group, Group,
Menu, Menu,
Modal, Modal,
NumberInput,
Select, Select,
SimpleGrid, SimpleGrid,
Stack, Stack,
@@ -327,6 +329,10 @@ const FirstMilePage = () => {
const [acceptVehicleValue, setAcceptVehicleValue] = useState<string | null>(null); const [acceptVehicleValue, setAcceptVehicleValue] = useState<string | null>(null);
const [bookingSearch, setBookingSearch] = useState(""); const [bookingSearch, setBookingSearch] = useState("");
const [distanceOpen, setDistanceOpen] = useState(false);
const [distanceValue, setDistanceValue] = useState("");
const [distanceDate, setDistanceDate] = useState<Date | null>(null);
const { data: listData, isLoading } = useQuery({ const { data: listData, isLoading } = useQuery({
queryKey: QUERY_KEYS.FIRST_MILE.list(), queryKey: QUERY_KEYS.FIRST_MILE.list(),
queryFn: async () => { queryFn: async () => {
@@ -379,6 +385,21 @@ const FirstMilePage = () => {
}, },
}); });
const updateDistanceMutation = useMutation({
mutationFn: ({ id, exactKm }: { id: string; exactKm: number }) =>
firstMileService.update(id, { exactKm }),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT });
if (activeRecord) {
toast({ title: "Distance updated", description: `${bookingRef(activeRecord)}${distanceValue} km` });
}
closeDistance();
},
onError: () => {
toast({ title: "Update failed", variant: "destructive" });
},
});
const acceptMutation = useMutation({ const acceptMutation = useMutation({
mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => { mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => {
const res = await firstMileService.accept(reference); const res = await firstMileService.accept(reference);
@@ -454,6 +475,29 @@ const FirstMilePage = () => {
acceptMutation.mutate({ reference: selectedBooking.reference, vehicleId: acceptVehicleValue }); acceptMutation.mutate({ reference: selectedBooking.reference, vehicleId: acceptVehicleValue });
}; };
const openDistance = (id: string) => {
setActiveId(id);
setDistanceValue("");
setDistanceDate(new Date());
setDistanceOpen(true);
};
const closeDistance = () => {
setDistanceOpen(false);
setActiveId(null);
setDistanceValue("");
setDistanceDate(null);
};
const handleSaveDistance = () => {
const distance = parseFloat(distanceValue);
if (!activeId || isNaN(distance) || distance < 0) {
toast({ title: "Invalid distance", description: "Enter a valid distance value.", variant: "destructive" });
return;
}
updateDistanceMutation.mutate({ id: activeId, exactKm: distance });
};
const matchesFilter = (r: FirstMileRecord) => { const matchesFilter = (r: FirstMileRecord) => {
switch (statusFilter) { switch (statusFilter) {
case "ALL": return true; case "ALL": return true;
@@ -726,6 +770,11 @@ const FirstMilePage = () => {
> >
View detail View detail
</Menu.Item> </Menu.Item>
<Menu.Item
onClick={() => openDistance(row.original.id)}
>
Add Actual distance
</Menu.Item>
{canPrint && ( {canPrint && (
<Menu.Item <Menu.Item
leftSection={<Printer size={15} />} leftSection={<Printer size={15} />}
@@ -1030,6 +1079,59 @@ const FirstMilePage = () => {
</Stack> </Stack>
)} )}
</Modal> </Modal>
{/* Add Actual Distance modal */}
<Modal
opened={distanceOpen}
onClose={closeDistance}
title={<Text fw={600}>Add Actual Distance</Text>}
size="md"
radius="lg"
centered
>
<Stack gap="md">
{activeRecord && (
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Text fw={600} size="sm">{bookingRef(activeRecord)}</Text>
<Group justify="space-between">
<Text size="xs" c="dimmed">Customer</Text>
<Text size="sm">{customerName(activeRecord)}</Text>
</Group>
<Group justify="space-between">
<Text size="xs" c="dimmed">Est. Distance (KM)</Text>
<Text size="sm">{activeRecord.estimatedKm ?? "—"}</Text>
</Group>
</Stack>
</Card>
)}
<NumberInput
label="Actual Distance (KM)"
placeholder="Enter distance"
value={distanceValue}
onChange={(v) => setDistanceValue(String(v ?? ""))}
min={0}
step={0.1}
decimalScale={2}
/>
<DateInput
label="Date"
placeholder="Select date"
value={distanceDate}
onChange={setDistanceDate}
/>
<Group justify="flex-end" gap="sm">
<Button variant="default" onClick={closeDistance}>Cancel</Button>
<Button
onClick={handleSaveDistance}
loading={updateDistanceMutation.isPending}
disabled={!distanceValue}
>
Save
</Button>
</Group>
</Stack>
</Modal>
</Stack> </Stack>
); );
}; };

View File

@@ -1,29 +1,25 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build from monorepo root: docker build -f apps/edr-passenger-api/Dockerfile . # Build from monorepo root: docker build -f apps/edr-passenger-api/Dockerfile .
# On start: runs prisma migrate deploy + seed, then the API. # On start: runs the API only — migrations/seed now run separately via the
# `migration` stage, invoked as a one-shot container in CI before deploy.
FROM node:24.15.0-alpine AS base FROM node:24.15.0-alpine AS base
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN corepack enable RUN corepack enable
WORKDIR /app WORKDIR /app
FROM base AS pruner FROM base AS pruner
COPY . . COPY . .
RUN pnpm dlx turbo prune "@edr/passenger-api" --docker RUN pnpm dlx turbo prune "@edr/passenger-api" --docker
FROM base AS installer FROM base AS installer
COPY --from=pruner /app/out/json/ . COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \ RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \
--mount=type=cache,id=pnpm,target=/pnpm/store \ --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
FROM base AS builder FROM base AS builder
COPY --from=installer /app/ . COPY --from=installer /app/ .
COPY --from=pruner /app/out/full/ . COPY --from=pruner /app/out/full/ .
RUN pnpm --filter "@edr/passenger-api" exec prisma generate RUN pnpm --filter "@edr/passenger-api" exec prisma generate
RUN pnpm turbo build --filter="@edr/passenger-api..." RUN pnpm turbo build --filter="@edr/passenger-api..."
FROM base AS deployer FROM base AS deployer
COPY --from=builder /app/ . COPY --from=builder /app/ .
RUN pnpm deploy --filter="@edr/passenger-api" --legacy /deploy RUN pnpm deploy --filter="@edr/passenger-api" --legacy /deploy
@@ -32,20 +28,24 @@ RUN if [ -d node_modules/.prisma ]; then \
cp -r node_modules/.prisma /deploy/node_modules/.prisma; \ cp -r node_modules/.prisma /deploy/node_modules/.prisma; \
fi fi
# --- Migration image: built in CI, run as a one-shot `docker run --rm --env-file ...`
# against the real DB, as its own gated step *before* the app image is built/deployed.
# Runs the same generate + migrate + seed sequence the old entrypoint used to run at
# container start, but now as a throwaway container, before the app container exists.
FROM deployer AS migration
WORKDIR /deploy
RUN corepack enable && corepack prepare pnpm@11.1.1 --activate
ENV CI=true
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
CMD ["sh", "-c", "npm run prisma:generate && npm run prisma:migrate && npm run prisma:seed"]
FROM node:24.15.0-alpine AS runner FROM node:24.15.0-alpine AS runner
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN corepack enable && corepack prepare pnpm@11.1.1 --activate
ENV NODE_ENV=production ENV NODE_ENV=production
WORKDIR /app WORKDIR /app
RUN addgroup --system --gid 1001 nodejs \ RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 --ingroup nodejs nestjs && adduser --system --uid 1001 --ingroup nodejs nestjs
COPY --from=deployer /deploy . COPY --from=deployer --chown=nestjs:nodejs /deploy .
COPY apps/edr-passenger-api/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh \
&& chown -R nestjs:nodejs /app
USER nestjs USER nestjs
ENV CI=true
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
EXPOSE 4000 EXPOSE 4000
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["node", "dist/main.js"] CMD ["node", "dist/main.js"]

View File

@@ -1,30 +1,32 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build from monorepo root: docker build -f apps/edr-freight-api/Dockerfile . # Build from monorepo root: docker build -f apps/edr-payment-api/Dockerfile .
FROM node:24.15.0-alpine AS base FROM node:24.15.0-alpine AS base
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
RUN corepack enable RUN corepack enable
WORKDIR /app WORKDIR /app
FROM base AS pruner FROM base AS pruner
COPY . . COPY . .
RUN pnpm dlx turbo prune "@edr/payment-api" --docker RUN pnpm dlx turbo prune "@edr/payment-api" --docker
FROM base AS installer FROM base AS installer
COPY --from=pruner /app/out/json/ . COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \ RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
FROM base AS builder FROM base AS builder
COPY --from=installer /app/ . COPY --from=installer /app/ .
COPY --from=pruner /app/out/full/ . COPY --from=pruner /app/out/full/ .
RUN pnpm turbo build --filter="@edr/payment-api..." RUN pnpm turbo build --filter="@edr/payment-api..."
FROM base AS deployer FROM base AS deployer
COPY --from=builder /app/ . COPY --from=builder /app/ .
RUN pnpm deploy --filter="@edr/payment-api" --prod --legacy --ignore-scripts /deploy RUN pnpm deploy --filter="@edr/payment-api" --prod --legacy --ignore-scripts /deploy
# --- Migration image: built in CI, run as a one-shot `docker run --rm --env-file ...`
# against the real DB, as its own gated step *before* the app image is built/deployed.
# The runner below only ever runs `node dist/main.js` — migrations happen here, not there.
FROM deployer AS migration
WORKDIR /deploy
CMD ["node", "dist/scripts/migrate.js"]
FROM node:24.15.0-alpine AS runner FROM node:24.15.0-alpine AS runner
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -32,10 +34,6 @@ WORKDIR /app
RUN addgroup --system --gid 1001 nodejs \ RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 --ingroup nodejs nestjs && adduser --system --uid 1001 --ingroup nodejs nestjs
COPY --from=deployer --chown=nestjs:nodejs /deploy . COPY --from=deployer --chown=nestjs:nodejs /deploy .
COPY apps/edr-payment-api/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh \
&& chown -R nestjs:nodejs /app
USER nestjs USER nestjs
EXPOSE 3008 EXPOSE 3008
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["node", "dist/main.js"] CMD ["node", "dist/main.js"]