Revert "Merge pull request 'Dev' (#58) from Dev into production"

This reverts commit 821e96e011, reversing
changes made to 0ff39e9e58.
This commit is contained in:
2026-09-17 11:25:33 +05:30
parent efb37fce7a
commit 256e39ccff
52 changed files with 87 additions and 2224 deletions
-7
View File
@@ -345,10 +345,3 @@ Spec: `docs/21-FRONTEND-PHASE2.md` (flows/screens) · contract: `docs/30-BACKEND
5. **Inline "add variant category":** a "+" icon button next to the checkboxes opens an inline name field that calls `variantCategoriesApi.create()` directly from `/new`, appends the result to the in-memory list, and auto-checks it — so a brand-new dimension (e.g. "Material") can be added without leaving the Item form, and it also then appears on `/dashboard/products/variants`.
6. **Color gets a real color picker:** for whichever checked category is literally named "Color" (case-insensitive), the free-text input is replaced with a native `<input type="color">` swatch picker *plus* a required "Color name" text field — picking red alone isn't enough, a name is mandatory too. The pair is encoded as a single string `"<name>|<hex>"` in `valuesByCategory` (helpers `encodeColorValue`/`decodeColorValue`/`partLabel` in `new/page.tsx`) so the existing generic value-list plumbing didn't need a parallel data shape; every place that displays or SKU-generates from a color value decodes it back to just the name (the hex only ever drives the swatch dot next to chips and table cells) — so SKUs read `HAR-CRI` (from "Crimson"), never `HAR-EF4`.
- Verified: `tsc --noEmit` clean after every step (same pre-existing `login/page.tsx` error only, confirmed unchanged throughout). Each change was driven end-to-end through a headless Playwright session against the dev server and screenshotted — field removal, subcategory always-visible + disabled state, checkbox show/hide of category builders, cartesian flat table with 2+ active categories, inline category creation followed by its builder appearing immediately, and the color picker + name → chip swatch → table swatch → final SKU/item name chain — with `console --errors` clean at every step and at least one full create-and-redirect-to-Items-list confirmed per major change.
## Deployment readiness (2026-09-14) — `docs/40-DEPLOYMENT.md`
- [x] **Standalone build + runtime image.** `next.config.ts` → `output: "standalone"`; runtime-only `Dockerfile` (`node:24-alpine`, non-root, copies `.next/standalone` + `.next/static` + `public`) and `.dockerignore` (keeps `.next`). The `/api` rewrite is resolved at build time and only matters locally — Apache sends `/api/` straight to ERPCore in deployed environments, so one image serves both test and release. **Deviation from the plan:** Node 24 instead of 20 (Node 20 is past end-of-life). Verified: `npm run build` produced `.next/standalone/server.js`; started on :3999 → `GET /login` 200, `GET /dashboard` without a session cookie → 307 to `/login`.
- [x] **Lint can gate CI.** `react-hooks/set-state-in-effect` temporarily `"warn"` (React Compiler rule from eslint-config-next 16, ~80 existing hits). Fixed the other 12 errors: 7 unescaped apostrophes (`sales/day-end`, `settings/invoice-template`, `login/forgot`, `login/forgot/otp`, `login/forgot/reset`); `PasswordRequirement` hoisted to module scope in `login/forgot/reset` (a component created during render remounts on every keystroke); the `visibleGrns` memo moved above `toggleAll` in `procurement/purchase-invoices/new` (it was read before its declaration, so the compiler could not preserve the memo). Now `eslint .` → 0 errors / 114 warnings; `tsc --noEmit` clean.
- [ ] Clear the `set-state-in-effect` backlog, then set the rule back to `"error"`.
- [x] **2026-09-15 — production-only deploy (`docs/40-DEPLOYMENT.md` "Active").** Supersedes the runtime-only image above: `Dockerfile` is now multi-stage (deps → builder → runner on `node:24-alpine`, the Client_New_Fe pattern), so `next build` runs inside `docker build` on the server, and `.dockerignore` excludes `.next` and `node_modules`. Still `output: "standalone"` and no build args. The deploy job replaces the `erp-web` container on `127.0.0.1:3020` only when `Frontend/erp-system` changed and fails the job if `/login` doesn't answer within 30 s. PR checks now run `tsc` + `eslint` + `next build` in a capped container without producing an image. The multi-stage `docker build` has not been run yet (no Docker locally).
- [x] **2026-09-15 — one workflow file.** `.gitea/workflows/ci.yml` and `deploy.yml` merged into `.gitea/workflows/ci-cd.yml`: the frontend PR check (`tsc` + `eslint` + `next build`) runs while a PR is open, and the `Build & Deploy` job (`docker build` → replace `erp-web` → `/login` check) runs only when a PR is merged into `production` or on a manual run. No change to the Dockerfile or the app.
-10
View File
@@ -1,10 +0,0 @@
# `next build` runs inside the image (see Dockerfile), so local build output and dependencies must
# not be sent as build context: they are large, and a Windows node_modules would break the Linux build.
node_modules
.next
.git
.env*
*.tsbuildinfo
npm-debug.log*
Dockerfile
.dockerignore
-38
View File
@@ -1,38 +0,0 @@
# ERP frontend image — built on the server by .gitea/workflows/ci-cd.yml (`docker build`), using the
# same pattern as Client_New_Fe. There are no build args: the app only calls same-origin /api/v1 and
# Apache routes /api/ straight to ERPCore (docs/40-DEPLOYMENT.md, "Active").
FROM node:24-alpine AS deps
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
FROM node:24-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Shared 8 GB VPS: keep the build's heap bounded (docker build has no --memory under BuildKit).
ENV NEXT_TELEMETRY_DISABLED=1 \
NODE_ENV=production \
NODE_OPTIONS=--max-old-space-size=2048
RUN npm run build
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000 \
HOSTNAME=0.0.0.0
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
@@ -112,17 +112,6 @@ function NewPurchaseInvoiceContent() {
[vendors]
)
// Deep-linked from a GRN detail page's "Create Purchase Invoice" prompt — that GRN is the
// whole reason this page was opened, so show only it rather than the vendor's full eligible
// list (which could pull in unrelated GRNs the user never meant to consolidate here).
// Declared before toggleAll, which reads it: referencing it ahead of its declaration stopped
// the React Compiler from preserving this memo (react-hooks/preserve-manual-memoization).
const deepLinkedGrnId = grnIdFromQuery ? Number(grnIdFromQuery) : null
const visibleGrns = useMemo(
() => (deepLinkedGrnId === null ? (eligibleGrns ?? []) : (eligibleGrns ?? []).filter((g) => g.grnId === deepLinkedGrnId)),
[eligibleGrns, deepLinkedGrnId]
)
function toggleGrn(grnId: number, checked: boolean) {
setSelectedGrnIds((prev) => {
const next = new Set(prev)
@@ -136,6 +125,15 @@ function NewPurchaseInvoiceContent() {
setSelectedGrnIds(checked ? new Set(visibleGrns.map((g) => g.grnId)) : new Set())
}
// Deep-linked from a GRN detail page's "Create Purchase Invoice" prompt — that GRN is the
// whole reason this page was opened, so show only it rather than the vendor's full eligible
// list (which could pull in unrelated GRNs the user never meant to consolidate here).
const deepLinkedGrnId = grnIdFromQuery ? Number(grnIdFromQuery) : null
const visibleGrns = useMemo(
() => (deepLinkedGrnId === null ? (eligibleGrns ?? []) : (eligibleGrns ?? []).filter((g) => g.grnId === deepLinkedGrnId)),
[eligibleGrns, deepLinkedGrnId]
)
const selectedGrns = useMemo(
() => (eligibleGrns ?? []).filter((g) => selectedGrnIds.has(g.grnId)),
[eligibleGrns, selectedGrnIds]
@@ -88,7 +88,7 @@ export default function SalesDayEndPage() {
<div>
<h1 className="text-2xl font-bold text-foreground">Day End</h1>
<p className="text-base text-muted-foreground">
Close a cashier&apos;s business day — locks their posted sales slips and bundle sales, and posts one consolidated journal entry to the ledger.
Close a cashier's business day — locks their posted sales slips and bundle sales, and posts one consolidated journal entry to the ledger.
</p>
</div>
</div>
@@ -149,14 +149,14 @@ export default function InvoiceTemplatePage() {
<Switch checked={showBankDetails} onCheckedChange={setShowBankDetails} className="mt-1 shrink-0" aria-label="Show bank details" />
<div className="flex flex-col gap-1">
<span className="text-base font-medium text-foreground">Show bank details</span>
<span className="text-sm text-muted-foreground">Prints the company&apos;s bank name, account and SWIFT code on invoices.</span>
<span className="text-sm text-muted-foreground">Prints the company's bank name, account and SWIFT code on invoices.</span>
</div>
</div>
<div className="flex items-start gap-4 py-4">
<Switch checked={showTaxRegNo} onCheckedChange={setShowTaxRegNo} className="mt-1 shrink-0" aria-label="Show tax registration no." />
<div className="flex flex-col gap-1">
<span className="text-base font-medium text-foreground">Show tax registration no.</span>
<span className="text-sm text-muted-foreground">Prints the company&apos;s tax registration number in the invoice header.</span>
<span className="text-sm text-muted-foreground">Prints the company's tax registration number in the invoice header.</span>
</div>
</div>
</div>
@@ -32,7 +32,7 @@ export default function ForgotOtpPage() {
</div>
<h1 className="text-2xl font-bold text-foreground text-center">Verify your email</h1>
</div>
<p className="text-sm text-muted-foreground text-center mb-8">We&apos;ve sent a verification code to your email address. Enter it below to continue.</p>
<p className="text-sm text-muted-foreground text-center mb-8">We've sent a verification code to your email address. Enter it below to continue.</p>
<div className="space-y-6">
<div className="flex justify-center">
@@ -48,7 +48,7 @@ export default function ForgotOtpPage() {
}}
className="text-sm font-medium text-foreground hover:text-foreground/80 transition-colors"
>
Didn&apos;t receive the code? Resend
Didn't receive the code? Resend
</button>
) : (
<p className="text-sm text-muted-foreground">Resend code available in <span className="font-semibold text-foreground">{Math.max(0, secondsLeft)}s</span></p>
@@ -22,7 +22,7 @@ export default function ForgotEmailPage() {
<div className="w-full max-w-2xl rounded-xl bg-card p-8 pl-10 shadow-lg border border-border">
<div className="mb-8 text-center">
<h1 className="text-2xl font-bold text-foreground">Reset your password</h1>
<p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we&apos;ll send you a one-time code to verify your identity.</p>
<p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we'll send you a one-time code to verify your identity.</p>
</div>
<div className="space-y-6">
@@ -13,19 +13,6 @@ import {
} from "@/components/ui/dialog"
import { Check, Eye, EyeOff } from "lucide-react"
// Declared at module scope: a component defined inside the page body is a new type on every
// render, so React would remount (and reset) each requirement row on each keystroke.
function PasswordRequirement({ met, label }: { met: boolean; label: string }) {
return (
<div className="flex items-center gap-3">
<div className={`flex items-center justify-center w-5 h-5 rounded-full shrink-0 text-sm font-semibold ${met ? 'bg-success/10 text-success' : 'bg-muted text-muted-foreground'}`}>
{met ? <Check className="size-4" /> : <span>○</span>}
</div>
<span className={`text-sm ${met ? 'text-foreground font-medium' : 'text-muted-foreground'}`}>{label}</span>
</div>
)
}
export default function ForgotResetPage() {
const router = useRouter()
const [password, setPassword] = useState("")
@@ -41,6 +28,15 @@ export default function ForgotResetPage() {
const passwordsMatch = password === confirm && password.length > 0
const allValid = meetsLength && hasUpper && hasNumber && hasSpecial && passwordsMatch
const PasswordRequirement = ({ met, label }: { met: boolean; label: string }) => (
<div className="flex items-center gap-3">
<div className={`flex items-center justify-center w-5 h-5 rounded-full shrink-0 text-sm font-semibold ${met ? 'bg-success/10 text-success' : 'bg-muted text-muted-foreground'}`}>
{met ? <Check className="size-4" /> : <span>○</span>}
</div>
<span className={`text-sm ${met ? 'text-foreground font-medium' : 'text-muted-foreground'}`}>{label}</span>
</div>
)
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
<div className="w-full max-w-xl rounded-xl bg-card p-8 shadow-lg border border-border">
@@ -101,7 +97,7 @@ export default function ForgotResetPage() {
</div>
{!passwordsMatch && confirm.length > 0 && (
<p className="text-sm text-error font-medium">Passwords don&apos;t match.</p>
<p className="text-sm text-error font-medium">Passwords don't match.</p>
)}
<div className="flex gap-3">
-8
View File
@@ -5,14 +5,6 @@ import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
{
rules: {
// Temporarily a warning: ~80 pre-existing violations from the React Compiler rules that
// eslint-config-next 16 enabled. Everything else stays an error so CI can gate on lint
// now; flip this back to "error" once the backlog is cleared (docs/40-DEPLOYMENT.md §7).
"react-hooks/set-state-in-effect": "warn",
},
},
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
-7
View File
@@ -8,16 +8,9 @@ import path from "path"
//
// Defaults to the backend's HTTP port: the HTTPS port uses a self-signed dev cert that
// this proxy would reject.
//
// Rewrites are resolved at `next build`, so this is effectively a local-dev setting. In the
// deployed environments Apache sends /api/ straight to ERPCore before Next ever sees it, which
// keeps one built image valid for both test and release (docs/40-DEPLOYMENT.md).
const BACKEND_ORIGIN = process.env.BACKEND_ORIGIN ?? "http://localhost:5224"
const nextConfig: NextConfig = {
// Self-contained server bundle (.next/standalone) for the runtime-only Docker image
// (Dockerfile in this folder). Has no effect on `next dev`.
output: "standalone",
turbopack: {
root: path.join(__dirname),
},