mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'freight/feature/vehicle_2' of github.com:Tria-plc/edr-platform into freight/feature/vehicle_2
This commit is contained in:
27
.github/workflows/deploy.yml
vendored
27
.github/workflows/deploy.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
NON_DEPLOYABLE_PATTERN="^docs/|^README[.]md$|^DEPLOYMENT[.]md$|^CLAUDE[.]md$|^checkpoint[.]md$|^orgstructure[.]md$|^ITMLS_DB_Design[.]md$|.*[.]md$|^[.]eslintrc|^[.]prettierrc|^[.]editorconfig|^[.]gitignore|^[.]gitattributes|^commitlint[.]config[.]js$"
|
||||
|
||||
GLOBAL_PATTERN="^[.]github/|^docker-compose[.]yaml$|^turbo[.]json$|^tsconfig[.]json$|^tsconfig[.]base[.]json$|^pnpm-workspace[.]yaml$|^pnpm-lock[.]yaml$|^package[.]json$|^[.]env([.][a-z]+)?$|^packages/|^local-packages/|^infrastructure/|^scripts/deploy/|^wagon[.][^/]*[.]ts$|^cargo[.][^/]*[.]ts$|^container[.][^/]*[.]ts$|^use-[^/]*[.]ts$|^[^/]*[.]service[.]ts$|^[^/]*[.]entity[.]ts$|^[^/]*-types[.]ts$"
|
||||
|
||||
|
||||
DEPLOYABLE=$(echo "$CHANGED" | grep -vE "$NON_DEPLOYABLE_PATTERN" || true)
|
||||
if [ -z "$DEPLOYABLE" ]; then
|
||||
echo "Only non-deployable files changed. Skipping deploy."
|
||||
@@ -142,6 +142,31 @@ jobs:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
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 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
DateInput,
|
||||
Divider,
|
||||
Group,
|
||||
Menu,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
@@ -327,6 +329,10 @@ const FirstMilePage = () => {
|
||||
const [acceptVehicleValue, setAcceptVehicleValue] = useState<string | null>(null);
|
||||
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({
|
||||
queryKey: QUERY_KEYS.FIRST_MILE.list(),
|
||||
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({
|
||||
mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => {
|
||||
const res = await firstMileService.accept(reference);
|
||||
@@ -454,6 +475,29 @@ const FirstMilePage = () => {
|
||||
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) => {
|
||||
switch (statusFilter) {
|
||||
case "ALL": return true;
|
||||
@@ -726,6 +770,11 @@ const FirstMilePage = () => {
|
||||
>
|
||||
View detail
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => openDistance(row.original.id)}
|
||||
>
|
||||
Add Actual distance
|
||||
</Menu.Item>
|
||||
{canPrint && (
|
||||
<Menu.Item
|
||||
leftSection={<Printer size={15} />}
|
||||
@@ -1030,6 +1079,59 @@ const FirstMilePage = () => {
|
||||
</Stack>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# 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
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS pruner
|
||||
COPY . .
|
||||
RUN pnpm dlx turbo prune "@edr/passenger-api" --docker
|
||||
|
||||
FROM base AS installer
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \
|
||||
--mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
COPY --from=installer /app/ .
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
RUN pnpm --filter "@edr/passenger-api" exec prisma generate
|
||||
RUN pnpm turbo build --filter="@edr/passenger-api..."
|
||||
|
||||
FROM base AS deployer
|
||||
COPY --from=builder /app/ .
|
||||
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; \
|
||||
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
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN corepack enable && corepack prepare pnpm@11.1.1 --activate
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 --ingroup nodejs nestjs
|
||||
COPY --from=deployer /deploy .
|
||||
COPY apps/edr-passenger-api/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh \
|
||||
&& chown -R nestjs:nodejs /app
|
||||
COPY --from=deployer --chown=nestjs:nodejs /deploy .
|
||||
USER nestjs
|
||||
ENV CI=true
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
EXPOSE 4000
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
# 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
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS pruner
|
||||
COPY . .
|
||||
RUN pnpm dlx turbo prune "@edr/payment-api" --docker
|
||||
|
||||
FROM base AS installer
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN --mount=type=secret,id=npmrc,target=./.npmrc,required=false \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
COPY --from=installer /app/ .
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
RUN pnpm turbo build --filter="@edr/payment-api..."
|
||||
|
||||
FROM base AS deployer
|
||||
COPY --from=builder /app/ .
|
||||
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
|
||||
RUN apk add --no-cache libc6-compat
|
||||
ENV NODE_ENV=production
|
||||
@@ -32,10 +34,6 @@ WORKDIR /app
|
||||
RUN addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 --ingroup nodejs nestjs
|
||||
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
|
||||
EXPOSE 3008
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/main.js"]
|
||||
|
||||
Reference in New Issue
Block a user