Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ dist
.git
.github
*.md

# Never copy real config into an image layer — values are passed as build args
# (see Dockerfile). The template is harmless but has no use in the build.
.env
.env.*
!.env.example
src/environments/env.generated.ts
src/environments/environment.local.ts
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ testem.log
.DS_Store
Thumbs.db

# Local Fuse theme reference — never push to the repo
/fuse-demo-v20.0.0/

# Local environment overrides (real backend origin / keys — never commit)
/src/environments/environment.local.ts

Expand Down
4 changes: 2 additions & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
src/@fuse/styles/user-themes.scss

# Generated API client + spec snapshot — never hand-format (npm run generate:api)
src/api/generated
src/api/openapi.json
src/contract/generated
src/contract/openapi.json
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/001-photo-albums"
"feature_directory": "specs/001-restaurant-onboarding"
}
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
shell commands, and other important information, read the current plan:
`specs/001-restaurant-onboarding/plan.md`
<!-- SPECKIT END -->

# FreshFlow Web — Agent Guide
Expand Down
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

# Config for the browser bundle. `.env` is excluded from the build context (see
# .dockerignore) so secrets never end up in an image layer — the values are
# passed in instead, and `scripts/generate-env.mjs` prefers the environment over
# any file. API_BASE_URL has no default: a build without it fails here rather
# than shipping a bundle whose requests resolve against the page URL.
#
# docker build \
# --build-arg API_BASE_URL=https://api.example.com \
# --build-arg GOONG_MAPS_KEY=... \
# --build-arg GOONG_PLACES_KEY=... .
ARG API_BASE_URL
ARG GOONG_MAPS_KEY
ARG GOONG_PLACES_KEY
ENV API_BASE_URL=$API_BASE_URL \
GOONG_MAPS_KEY=$GOONG_MAPS_KEY \
GOONG_PLACES_KEY=$GOONG_PLACES_KEY

RUN npm run build -- --configuration production

# Stage 2: Serve
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const angular = require("angular-eslint");
module.exports = defineConfig([
{
// Generated API client — never linted (re-run npm run generate:api).
ignores: ["src/api/generated/**"],
ignores: ["src/contract/generated/**"],
},
{
linterOptions: {
Expand Down
35 changes: 32 additions & 3 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,39 @@ server {
root /usr/share/nginx/html;
index index.html;

gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;

# Angular fingerprints its JS/CSS, so a changed file always has a new name.
# Those can be cached forever; anything whose name is stable must not be,
# or a deploy ships new code against assets the browser kept.
location ~* \.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Translations are fetched at runtime from a fixed path, so the filename
# gives the browser no way to notice a new build. Without an explicit
# Cache-Control the browser guesses a lifetime from Last-Modified and can
# serve a stale copy for hours — which renders the raw i18n keys of any
# string added since. `no-cache` means revalidate, not "don't store": the
# ETag turns the check into a 304 with no body.
location /i18n/ {
add_header Cache-Control "no-cache";
}

# index.html names the hashed bundles, so a stale copy pins the whole app to
# the previous deploy. Always revalidate.
location = /index.html {
add_header Cache-Control "no-cache";
}

# Remaining static files (icons, fonts, images) are unhashed too. A day is
# long enough to be worth caching and short enough that a fix propagates.
location ~* \.(svg|png|jpe?g|gif|ico|webp|woff2?|ttf|eot)$ {
add_header Cache-Control "public, max-age=86400, must-revalidate";
}

location / {
try_files $uri $uri/ /index.html;
}

gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
}
157 changes: 155 additions & 2 deletions public/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,84 @@
"admin.users.create.submit": "Create user",
"admin.users.create.success": "User created",
"admin.users.create.error": "Could not create the user",
"admin.users.create.errors.emailRequired": "Email is required",
"admin.users.create.errors.emailInvalid": "Enter a valid email address",
"admin.users.create.errors.emailMax": "Must not exceed 255 characters",
"admin.users.create.errors.passwordRequired": "Password is required",
"admin.users.create.errors.roleRequired": "Please choose a role",
"admin.users.create.errors.marketRequired": "Market agents must be assigned a market",
"admin.users.create.errors.restaurantRequired": "Restaurant name is required",
"admin.users.create.errors.restaurantMax": "Must not exceed 200 characters",
"admin.users.create.errors.phoneInvalid": "Enter 7–15 digits, optional leading +",
"admin.users.create.errors.phoneMax": "Must not exceed 20 characters",
"admin.users.create.password.hint": "Password must contain:",
"admin.users.create.password.minLength": "At least 8 characters",
"admin.users.create.password.uppercase": "An uppercase letter (A–Z)",
"admin.users.create.password.digit": "A number (0–9)",
"admin.users.create.password.special": "A special character (e.g. @ # ! $)",
"errors.api.generic": "Something went wrong, please try again",
"errors.api.validation": "Please check the highlighted fields and try again",
"errors.api.businessRule": "This action isn't allowed right now",
"errors.api.invalidCredentials": "Incorrect email/phone or password",
"errors.api.invalidCurrentPassword": "Your current password is incorrect",
"errors.api.accountInactive": "This account is inactive",
"errors.api.accountLocked": "This account is temporarily locked",
"errors.api.emailAlreadyExists": "An account with this email already exists",
"errors.api.phoneAlreadyExists": "An account with this phone number already exists",
"errors.api.weakPassword": "The password does not meet the strength requirements",
"errors.api.sessionExpired": "Your session has expired, please sign in again",
"errors.api.resetTokenInvalid": "This reset link is invalid or has expired",
"errors.api.otpInvalid": "The verification code is incorrect or has expired",
"errors.api.channelNotSupported": "This channel is not supported yet",
"errors.api.userNotFound": "This user no longer exists",
"errors.api.invalidRole": "The selected role is not valid",
"errors.api.invalidMarket": "The selected market is not valid",
"errors.api.cannotDisableSelf": "You cannot deactivate your own account",
"errors.api.cannotChangeOwnRole": "You cannot change your own role",
"errors.api.restaurantNotFound": "No restaurant was found for this id",
"errors.api.alreadyApproved": "This restaurant has already been approved",
"errors.api.marketNotFound": "This market no longer exists",
"errors.api.productNotFound": "This product no longer exists",
"errors.api.marketAccessDenied": "You are not assigned to this market",
"errors.api.concurrencyConflict": "Someone else just updated this. Please refresh and try again",
"errors.api.invalidPrice": "The price is not valid",
"errors.api.invalidQuantity": "The quantity is not valid",
"errors.api.restaurantNotApproved": "Your restaurant is pending admin approval",
"errors.api.emptyOrder": "The order has no items",
"errors.api.invalidProduct": "One or more items are no longer available",
"errors.api.insufficientStock": "One or more items exceed the available stock",
"errors.api.scheduledTooSoon": "The scheduled time must be at least 2 hours from now",
"errors.api.forbidden": "You do not have permission to perform this action",
"errors.api.rateLimited": "Too many requests, please wait a moment and try again",
"errors.api.notFound": "The requested item was not found or no longer exists",
"errors.api.conflict": "This conflicts with the current state — please refresh and try again",
"errors.api.serverError": "A server error occurred, please try again in a moment",
"errors.api.network": "Could not reach the server. Check your connection and try again",
"errors.field.emailInvalid": "Must be a valid email address",
"errors.field.max255": "Must not exceed 255 characters",
"errors.field.passwordMinLength": "Must be at least 8 characters",
"errors.field.passwordUppercase": "Must contain at least one uppercase letter",
"errors.field.passwordDigit": "Must contain at least one number",
"errors.field.passwordSpecial": "Must contain at least one special character",
"errors.field.passwordDifferent": "New password must be different from the current password",
"errors.field.identifierInvalid": "Must be a valid email address or phone number",
"errors.field.phoneInvalid": "Must be a valid phone number (7–15 digits, optional leading +)",
"errors.field.max200": "Must not exceed 200 characters",
"errors.field.roleInvalid": "Role must be one of the accepted values",
"errors.field.pricePositive": "Price must be greater than 0",
"errors.field.priceDecimals": "Price must have at most 2 decimal places",
"errors.field.quantityInteger": "Quantity must be a whole number",
"errors.field.quantityNonNegative": "Quantity must be 0 or greater",
"errors.field.quantityPositive": "Quantity must be greater than 0",
"errors.field.ordersRequired": "At least one order must be provided",
"errors.field.ordersMax20": "Cannot process more than 20 orders at once",
"errors.field.orderNotCancellable": "This order cannot be cancelled in its current status",
"errors.field.ordersMustBeConfirmed": "All orders must be confirmed before grouping",
"errors.field.ordersAlreadyGrouped": "One or more orders are already in an active group",
"errors.field.autoBatchRunning": "Auto-batch is already running for this date",
"errors.field.hubStockExceeded": "Requested quantity exceeds available hub stock",
"errors.field.endDateBeforeStart": "The end date must be on or after the start date",
"errors.field.capacityPositive": "Capacity must be greater than 0",
"admin.userDetail.title": "User detail",
"admin.userDetail.actionError": "The action failed, please try again",
"admin.userDetail.profile.title": "Profile",
Expand All @@ -305,7 +383,25 @@
"admin.userDetail.assignments.empty": "No markets available to assign",
"admin.userDetail.assignments.agentOnly": "Market assignments apply only to market-agent users. Set this user's role to Market Agent and save it first.",
"admin.restaurants.title": "Restaurants",
"admin.restaurants.subtitle": "Approve restaurants and manage credit by restaurant id (UUID).",
"admin.restaurants.subtitle": "Manage restaurant accounts, approval and credit.",
"admin.restaurants.empty": "No restaurants found",
"admin.restaurants.loadError": "Could not load the restaurant list",
"admin.restaurants.noRestaurantId": "This account has no restaurant id yet — approval and credit are unavailable.",
"admin.restaurants.filters.searchPlaceholder": "Email, restaurant name...",
"admin.restaurants.filters.status": "Status",
"admin.restaurants.filters.allStatuses": "All statuses",
"admin.restaurants.table.restaurant": "Restaurant",
"admin.restaurants.table.email": "Email",
"admin.restaurants.table.phone": "Phone",
"admin.restaurants.table.status": "Status",
"admin.restaurants.table.actions": "Actions",
"admin.restaurants.unnamed": "Unnamed restaurant",
"admin.restaurants.create.trigger": "Add restaurant",
"admin.restaurants.create.title": "Create restaurant account",
"admin.restaurants.create.submit": "Create restaurant",
"admin.restaurants.create.success": "Restaurant account created",
"admin.restaurants.create.error": "Could not create the restaurant account",
"admin.restaurants.lifecycle.title": "Approval",
"admin.restaurants.actionError": "The action failed, please try again",
"admin.restaurants.lookup.label": "Restaurant id (UUID)",
"admin.restaurants.lookup.placeholder": "e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6",
Expand All @@ -316,7 +412,10 @@
"admin.restaurants.credit.limit": "Credit limit",
"admin.restaurants.credit.balance": "Current balance",
"admin.restaurants.credit.available": "Available credit",
"admin.restaurants.credit.snapshot": "Credit",
"admin.restaurants.credit.empty": "No credit data yet",
"admin.restaurants.creditLimit.title": "Set credit limit",
"admin.restaurants.creditLimit.activate": "Activate credit limit",
"admin.restaurants.creditLimit.amount": "Credit limit",
"admin.restaurants.creditLimit.note": "Note",
"admin.restaurants.creditLimit.submit": "Update limit",
Expand All @@ -328,6 +427,25 @@
"admin.restaurants.settle.note": "Note",
"admin.restaurants.settle.submit": "Confirm settlement",
"admin.restaurants.settle.success": "Credit settlement recorded",
"admin.restaurants.statements.title": "Credit statements",
"admin.restaurants.statements.year": "Year",
"admin.restaurants.statements.month": "Month",
"admin.restaurants.statements.generateSubmit": "Generate statement",
"admin.restaurants.statements.generateSuccess": "Statement generated",
"admin.restaurants.statements.period": "Period",
"admin.restaurants.statements.opening": "Opening balance",
"admin.restaurants.statements.closing": "Closing balance",
"admin.restaurants.statements.charges": "Charges",
"admin.restaurants.statements.payments": "Payments",
"admin.restaurants.statements.pdf": "Download PDF",
"admin.restaurants.statements.empty": "No statements yet",
"admin.restaurants.transactions.title": "Credit transactions",
"admin.restaurants.transactions.date": "Date",
"admin.restaurants.transactions.type": "Type",
"admin.restaurants.transactions.amount": "Amount",
"admin.restaurants.transactions.balance": "Balance after",
"admin.restaurants.transactions.description": "Description",
"admin.restaurants.transactions.empty": "No transactions yet",
"admin.crud.search": "Search",
"admin.crud.edit": "Edit",
"admin.crud.remove": "Remove",
Expand All @@ -348,6 +466,8 @@
"admin.crud.errors.maxLength": "Must be at most {{max}} characters",
"admin.crud.errors.min": "Must be at least {{min}}",
"admin.crud.filterAll": "All",
"admin.crud.clearFilters": "Clear all filters",
"admin.crud.details": "Details",
"admin.crud.image.upload": "Upload image",
"admin.crud.image.hint": "Drag & drop or click to browse",
"admin.crud.image.change": "Change image",
Expand Down Expand Up @@ -383,16 +503,29 @@
"admin.products.description": "Description",
"admin.products.image": "Image",
"admin.products.imageUrl": "Product image",
"admin.products.details": "Details",
"admin.products.noThumb": "No thumb",
"admin.markets.title": "Markets",
"admin.markets.subtitle": "Manage source markets",
"admin.markets.create": "Add market",
"admin.markets.searchPlaceholder": "Search by name, location or address",
"admin.markets.name": "Market name",
"admin.markets.location": "Location",
"admin.markets.address": "Address",
"admin.markets.coordinates": "Coordinates",
"admin.markets.details": "Details",
"admin.markets.latitude": "Latitude",
"admin.markets.longitude": "Longitude",
"admin.markets.pricing": "Manage pricing",
"admin.markets.agent": "Market agent",
"admin.markets.agentNone": "Unassigned",
"admin.markets.agentDialog.title": "Assign market agent",
"admin.markets.agentDialog.current": "Current agent",
"admin.markets.agentDialog.select": "Market agent",
"admin.markets.agentDialog.clear": "Clear assignment",
"admin.markets.agentDialog.save": "Save",
"admin.markets.agentDialog.success": "Market agent updated",
"admin.markets.agentDialog.error": "Could not update the market agent",
"admin.markets.pricingTitle": "Per-market price & quantity",
"admin.markets.pricingSubtitle": "Update product price and available quantity at this market",
"admin.markets.addProduct": "Add product",
Expand Down Expand Up @@ -479,6 +612,8 @@
"admin.analytics.to": "To",
"admin.analytics.apply": "Apply",
"admin.analytics.noData": "No data for this period.",
"admin.analytics.kpiLabel": "Overview",
"admin.analytics.welcomeStatus": "{{count}} recent activities on the platform",
"admin.analytics.recentActivity": "Recent activity",
"admin.analytics.export": "Export",
"admin.analytics.exportError": "The export could not be downloaded.",
Expand Down Expand Up @@ -516,5 +651,23 @@
"admin.crud.reactivate": "Reactivate",
"admin.crud.reactivateSuccess": "Reactivated.",
"admin.crud.reactivateIgnored": "The server accepted the request but the item is still inactive — the API may not support reactivating yet.",
"admin.crud.missingIdError": "Cannot save: this record has no id in the API response. Reload the page; if it persists, report it — saving would create a duplicate."
"admin.crud.missingIdError": "Cannot save: this record has no id in the API response. Reload the page; if it persists, report it — saving would create a duplicate.",
"restaurantProfile.title": "Restaurant profile",
"restaurantProfile.subtitle": "Keep your business details up to date so we can serve and deliver to you.",
"restaurantProfile.profile.sectionTitle": "Business details",
"restaurantProfile.profile.sectionSubtitle": "Name, address, contact, and the daily window when you can receive deliveries.",
"restaurantProfile.profile.name": "Business name",
"restaurantProfile.profile.namePlaceholder": "e.g. Green Garden Restaurant",
"restaurantProfile.profile.nameRequired": "Business name is required.",
"restaurantProfile.profile.address": "Address",
"restaurantProfile.profile.contactPerson": "Contact person",
"restaurantProfile.profile.pickupWindow": "Receiving window",
"restaurantProfile.profile.pickupStart": "From",
"restaurantProfile.profile.pickupEnd": "To",
"restaurantProfile.profile.pickupIncomplete": "Please provide both a start and an end time.",
"restaurantProfile.profile.pickupEndBeforeStart": "The end time must be after the start time.",
"restaurantProfile.profile.save": "Save changes",
"restaurantProfile.profile.saved": "Profile updated.",
"restaurantProfile.profile.saveError": "Could not save your profile. Please try again.",
"restaurantProfile.profile.loadError": "Could not load your profile. Please refresh to try again."
}
Loading
Loading