diff --git a/.vscode/launch.json b/.vscode/launch.json index 41839db..2772a38 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,6 +9,11 @@ "type": "node", "request": "launch", "envFile": "${workspaceFolder}/.env", + "env": { + "USE_MOCK_AUTH": "true", + "MOCK_USER_ID": "109754742613069927799", + "MOCK_USER_NAME": "Nikola Derežić" + }, "runtimeArgs": [ "run", // this is `run` from `npm run` "dev" // this is `dev` from `npm run dev` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bcaa9d7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- `npm run dev` - Start development server (Next.js) +- `npm run build` - Build production version +- `npm start` - Start production server +- `npm run prettier` - Format code with Prettier +- `npm run prettier:check` - Check code formatting +- `npm run seed` - Seed database with initial data + +## Deployment Commands + +- `./build.sh` - Build Docker image for deployment +- `./deploy.sh` - Deploy Docker service to production +- `./debug-deploy.sh` - Deploy with debug configuration + +## Architecture Overview + +This is a Next.js 14 utility bills tracking application ("Evidencija Režija") with the following key components: + +### Tech Stack +- **Framework**: Next.js 14 with App Router and standalone output for Docker +- **Authentication**: NextAuth v5 with Google OAuth +- **Database**: MongoDB with connection pooling +- **Internationalization**: next-intl (Croatian/English) +- **Styling**: Tailwind CSS with DaisyUI components +- **Deployment**: Docker with MongoDB 4.4.27 (AVX compatibility) + +### Core Architecture Patterns + +**Multi-user Data Isolation**: All database operations use the `withUser` higher-order function from `app/lib/auth.ts:102` to automatically inject authenticated user ID into queries, ensuring data isolation between users. + +**Server Actions Pattern**: Form handling uses Next.js Server Actions with Zod validation. Actions are defined in `app/lib/actions/` and follow the pattern: +```typescript +export const actionName = withUser(async (user: AuthenticatedUser, ...args) => { + // Server action implementation with automatic user context +}); +``` + +**Internationalization**: Uses next-intl with locale-based routing. Messages are in `messages/` directory. The middleware handles both auth and i18n routing. + +### Key Files & Responsibilities + +- `middleware.ts` - Handles authentication and i18n routing, defines public pages +- `app/lib/auth.ts` - NextAuth configuration, `withUser` HOF for user context +- `app/lib/dbClient.ts` - MongoDB connection with development/production handling +- `app/lib/actions/` - Server actions for data mutations (locations, bills, months) +- `app/i18n.ts` - Internationalization configuration (Croatian default) +- `next.config.js` - Standalone build config with `serverActions.allowedOrigins` for Docker deployment + +### Database Schema +- **Collections**: Locations, Bills, Months (year-month periods) +- **User Association**: All documents include `userId` field for multi-tenant isolation +- **Database Name**: "utility-bills" + +### Docker Deployment Notes +- Uses standalone Next.js build for Docker optimization +- MongoDB 4.4.27 required for older CPU compatibility (no AVX instructions) +- `HOSTNAME=0.0.0.0` with `serverActions.allowedOrigins` config to handle reverse proxy headers +- Environment variables: `MONGODB_URI`, `GOOGLE_ID`, `GOOGLE_SECRET`, `AUTH_SECRET` + +### Barcode/QR Code Feature +- Uses `@zxing/library` and `@zxing/browser` for PDF document scanning +- Heavy barcode decoding operations should be moved to background threads if performance issues arise +- PDF processing with `pdfjs-dist` for utility bill scanning + +### Testing & Code Quality +- ESLint with Next.js and Prettier configurations +- No specific test framework configured - check with user before assuming testing approach \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4da1e50..b33c234 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # This file is inspired by https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile -FROM node:18-alpine AS base +FROM node:24-alpine AS base #----------------------------------------- # STAGE 1: Build the Next.js project @@ -27,11 +27,11 @@ RUN npm run build #----------------------------------------- # STAGE 3: Run the Next.js server #----------------------------------------- -FROM base as production +FROM base AS production WORKDIR /app -ENV NODE_ENV production +ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs @@ -54,7 +54,7 @@ USER nextjs EXPOSE 3000 -ENV PORT 3000 +ENV PORT=3000 # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/next-config-js/output diff --git a/app/lib/actions/billActions.ts b/app/lib/actions/billActions.ts index 757999b..f618d7b 100644 --- a/app/lib/actions/billActions.ts +++ b/app/lib/actions/billActions.ts @@ -28,6 +28,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ _id: z.string(), billName: z.coerce.string().min(1, t("bill-name-required")), billNotes: z.string(), + addToSubsequentMonths: z.boolean().optional(), payedAmount: z.string().nullable().transform((val, ctx) => { if(!val || val === '') { @@ -123,6 +124,7 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI .safeParse({ billName: formData.get('billName'), billNotes: formData.get('billNotes'), + addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on', payedAmount: formData.get('payedAmount'), }); @@ -138,6 +140,7 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI const { billName, billNotes, + addToSubsequentMonths, payedAmount, } = validatedFields.data; @@ -183,25 +186,95 @@ export const updateOrAddBill = withUser(async (user:AuthenticatedUser, locationI ] }); } else { - // find a location with the given locationID - const post = await dbClient.collection("lokacije").updateOne( + // Create new bill - add to current location first + const newBill = { + _id: (new ObjectId()).toHexString(), + name: billName, + paid: billPaid, + attachment: billAttachment, + notes: billNotes, + payedAmount, + barcodeImage, + }; + + // Add to current location + await dbClient.collection("lokacije").updateOne( { _id: locationId, // find a location with the given locationID userId // make sure that the location belongs to the user }, { $push: { - bills: { - _id: (new ObjectId()).toHexString(), - name: billName, - paid: billPaid, - attachment: billAttachment, - notes: billNotes, - payedAmount, - barcodeImage, - } + bills: newBill } }); + + // If addToSubsequentMonths is enabled, add to subsequent months + if (addToSubsequentMonths && billYear && billMonth) { + // Get the current location to find its name + const currentLocation = await dbClient.collection("lokacije") + .findOne({ _id: locationId, userId }, { projection: { bills: 0 } }); + + if (currentLocation) { + // Find all subsequent months that have the same location name + const subsequentLocations = await dbClient.collection("lokacije") + .find({ + userId, + name: currentLocation.name, + $or: [ + { "yearMonth.year": { $gt: billYear } }, + { + "yearMonth.year": billYear, + "yearMonth.month": { $gt: billMonth } + } + ] + }, { projection: { bills: 0 } }) + .toArray(); + + // For each subsequent location, check if bill with same name already exists + const updateOperations = []; + for (const location of subsequentLocations) { + const existingBill = await dbClient.collection("lokacije") + .findOne({ + _id: location._id, + "bills.name": billName + }, { + projection: { + "bills.$": 1, + "bills.attachment": 0, + "bills.barcodeImage": 0 + } + }); + + // Only add if bill with same name doesn't already exist + if (!existingBill) { + updateOperations.push({ + updateOne: { + filter: { _id: location._id, userId }, + update: { + $push: { + bills: { + _id: (new ObjectId()).toHexString(), + name: billName, + paid: false, // New bills in subsequent months are unpaid + attachment: null, // No attachment for subsequent months + notes: billNotes, + payedAmount: null, + barcodeImage: undefined, + } + } + } + } + }); + } + } + + // Execute all update operations at once if any + if (updateOperations.length > 0) { + await dbClient.collection("lokacije").bulkWrite(updateOperations); + } + } + } } if(billYear && billMonth ) { await gotoHome({ year: billYear, month: billMonth }); @@ -285,27 +358,91 @@ export const fetchBillById = async (locationID:string, billID:string, includeAtt return([billLocation, bill] as [BillingLocation, Bill]); }; -export const deleteBillById = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, year:number, month:number) => { +export const deleteBillById = withUser(async (user:AuthenticatedUser, locationID:string, billID:string, year:number, month:number, _prevState:any, formData?: FormData) => { const { id: userId } = user; const dbClient = await getDbClient(); + + const deleteInSubsequentMonths = formData?.get('deleteInSubsequentMonths') === 'on'; - // find a location with the given locationID - const post = await dbClient.collection("lokacije").updateOne( - { - _id: locationID, // find a location with the given locationID - userId // make sure that the location belongs to the user - }, - { - // remove the bill with the given billID - $pull: { - bills: { - _id: billID + if (deleteInSubsequentMonths) { + // Get the current location and bill to find the bill name and location name + const location = await dbClient.collection("lokacije") + .findOne({ _id: locationID, userId }, { + projection: { + "bills.attachment.fileContentsBase64": 0, + "bills.barcodeImage": 0 + } + }); + + if (location) { + const bill = location.bills.find(b => b._id === billID); + + if (bill) { + // Find all subsequent locations with the same name that have the same bill + const subsequentLocations = await dbClient.collection("lokacije") + .find({ + userId, + name: location.name, + $or: [ + { "yearMonth.year": { $gt: year } }, + { + "yearMonth.year": year, + "yearMonth.month": { $gt: month } + } + ], + "bills.name": bill.name + }, { projection: { bills: 0 } }) + .toArray(); + + // Delete the bill from all subsequent locations (by name) + const updateOperations = subsequentLocations.map(loc => ({ + updateOne: { + filter: { _id: loc._id, userId }, + update: { + $pull: { + bills: { name: bill.name } as Partial + } + } + } + })); + + // Also delete from current location (by ID for precision) + updateOperations.push({ + updateOne: { + filter: { _id: locationID, userId }, + update: { + $pull: { + bills: { _id: billID } + } + } + } + }); + + // Execute all delete operations + if (updateOperations.length > 0) { + await dbClient.collection("lokacije").bulkWrite(updateOperations); } } - }); + } + } else { + // Delete only from current location (original behavior) + await dbClient.collection("lokacije").updateOne( + { + _id: locationID, // find a location with the given locationID + userId // make sure that the location belongs to the user + }, + { + // remove the bill with the given billID + $pull: { + bills: { + _id: billID + } + } + }); + } await gotoHome({year, month}); - return(post.modifiedCount); + return { message: null }; }); \ No newline at end of file diff --git a/app/lib/actions/locationActions.ts b/app/lib/actions/locationActions.ts index d1ef4c4..057f757 100644 --- a/app/lib/actions/locationActions.ts +++ b/app/lib/actions/locationActions.ts @@ -27,6 +27,8 @@ const FormSchema = (t:IntlTemplateFn) => z.object({ _id: z.string(), locationName: z.coerce.string().min(1, t("location-name-required")), locationNotes: z.string(), + addToSubsequentMonths: z.boolean().optional(), + updateScope: z.enum(["current", "subsequent", "all"]).optional(), }) // dont include the _id field in the response .omit({ _id: true }); @@ -47,6 +49,8 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const validatedFields = FormSchema(t).safeParse({ locationName: formData.get('locationName'), locationNotes: formData.get('locationNotes'), + addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on', + updateScope: formData.get('updateScope') as "current" | "subsequent" | "all" | undefined, }); // If form validation fails, return errors early. Otherwise, continue... @@ -60,6 +64,8 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const { locationName, locationNotes, + addToSubsequentMonths, + updateScope, } = validatedFields.data; // update the bill in the mongodb @@ -68,18 +74,70 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat const { id: userId, email: userEmail } = user; if(locationId) { - await dbClient.collection("lokacije").updateOne( - { - _id: locationId, // find a location with the given locationID - userId // make sure the location belongs to the user - }, - { - $set: { - name: locationName, - notes: locationNotes, + // Get the current location first to find its name + const currentLocation = await dbClient.collection("lokacije") + .findOne({ _id: locationId, userId }, { projection: { bills: 0 } }); + + if (!currentLocation) { + return { + message: "Location not found", + errors: undefined, + }; + } + + // Handle different update scopes + if (updateScope === "current" || !updateScope) { + // Update only the current location (default behavior) + await dbClient.collection("lokacije").updateOne( + { + _id: locationId, + userId + }, + { + $set: { + name: locationName, + notes: locationNotes, + } } - }); + ); + } else if (updateScope === "subsequent") { + // Update current and all subsequent months + await dbClient.collection("lokacije").updateMany( + { + userId, + name: currentLocation.name, + $or: [ + { "yearMonth.year": { $gt: currentLocation.yearMonth.year } }, + { + "yearMonth.year": currentLocation.yearMonth.year, + "yearMonth.month": { $gte: currentLocation.yearMonth.month } + } + ] + }, + { + $set: { + name: locationName, + notes: locationNotes, + } + } + ); + } else if (updateScope === "all") { + // Update all locations with the same name across all months + await dbClient.collection("lokacije").updateMany( + { + userId, + name: currentLocation.name + }, + { + $set: { + name: locationName, + notes: locationNotes, + } + } + ); + } } else if(yearMonth) { + // Always add location to the specified month await dbClient.collection("lokacije").insertOne({ _id: (new ObjectId()).toHexString(), userId, @@ -89,6 +147,78 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat yearMonth: yearMonth, bills: [], }); + + // If addToSubsequentMonths is enabled, add to all subsequent months + if (addToSubsequentMonths) { + // Find all subsequent months that exist in the database + const subsequentMonths = await dbClient.collection("lokacije") + .aggregate([ + { + $match: { + userId, + $or: [ + { "yearMonth.year": { $gt: yearMonth.year } }, + { + "yearMonth.year": yearMonth.year, + "yearMonth.month": { $gt: yearMonth.month } + } + ] + } + }, + { + $group: { + _id: { + year: "$yearMonth.year", + month: "$yearMonth.month" + } + } + }, + { + $project: { + _id: 0, + year: "$_id.year", + month: "$_id.month" + } + }, + { + $sort: { + year: 1, + month: 1 + } + } + ]) + .toArray(); + + // For each subsequent month, check if location with same name already exists + const locationsToInsert = []; + for (const monthData of subsequentMonths) { + const existingLocation = await dbClient.collection("lokacije") + .findOne({ + userId, + name: locationName, + "yearMonth.year": monthData.year, + "yearMonth.month": monthData.month + }, { projection: { bills: 0 } }); + + // Only add if location with same name doesn't already exist in that month + if (!existingLocation) { + locationsToInsert.push({ + _id: (new ObjectId()).toHexString(), + userId, + userEmail, + name: locationName, + notes: locationNotes, + yearMonth: { year: monthData.year, month: monthData.month }, + bills: [], + }); + } + } + + // Insert all new locations at once if any + if (locationsToInsert.length > 0) { + await dbClient.collection("lokacije").insertMany(locationsToInsert); + } + } } if(yearMonth) await gotoHome(yearMonth); @@ -226,7 +356,7 @@ export const fetchLocationById = async (locationID:string) => { return(billLocation); }; -export const deleteLocationById = withUser(async (user:AuthenticatedUser, locationID:string, yearMonth:YearMonth) => { +export const deleteLocationById = withUser(async (user:AuthenticatedUser, locationID:string, yearMonth:YearMonth, _prevState:any, formData: FormData) => { noStore(); @@ -234,8 +364,35 @@ export const deleteLocationById = withUser(async (user:AuthenticatedUser, locati const { id: userId } = user; - // find a location with the given locationID - const post = await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId }); + const deleteInSubsequentMonths = formData.get('deleteInSubsequentMonths') === 'on'; + + if (deleteInSubsequentMonths) { + // Get the location name first to find all locations with the same name + const location = await dbClient.collection("lokacije") + .findOne({ _id: locationID, userId }, { projection: { bills: 0 } }); + + if (location) { + // Delete all locations with the same name in current and subsequent months + await dbClient.collection("lokacije").deleteMany({ + userId, + name: location.name, + $or: [ + { "yearMonth.year": { $gt: yearMonth.year } }, + { + "yearMonth.year": yearMonth.year, + "yearMonth.month": { $gte: yearMonth.month } + } + ] + }); + } + } else { + // Delete only the specific location (current behavior) + await dbClient.collection("lokacije").deleteOne({ _id: locationID, userId }); + } - await gotoHome(yearMonth) + await gotoHome(yearMonth); + + return { + message: null + }; }) \ No newline at end of file diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 501c647..63e1fd2 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -1,25 +1,25 @@ import NextAuth, { NextAuthConfig } from 'next-auth'; import GoogleProvider from 'next-auth/providers/google'; -import LinkedinProvider from 'next-auth/providers/linkedin'; +// import LinkedinProvider from 'next-auth/providers/linkedin'; import { Session } from 'next-auth'; import { AuthenticatedUser } from './types/next-auth'; import { defaultLocale } from '../i18n'; export const myAuth = () => { + // Use mock authentication in development when enabled via environment variable + if (process.env.NODE_ENV === 'development' && process.env.USE_MOCK_AUTH === 'true') { + const session: Session = { + user: { + id: process.env.MOCK_USER_ID || "109754742613069927799", + name: process.env.MOCK_USER_NAME || "Nikola Derežić", + }, + expires: "123", + }; + + return Promise.resolve(session); + } - // Ovo koristim u developmentu - - // const session:Session = { - // user: { - // id: "109754742613069927799", - // name: "Nikola Derežić", - // }, - // expires: "123", - // }; - - // return(Promise.resolve(session)); - - return(auth()); + return auth(); } export const authConfig: NextAuthConfig = { diff --git a/app/ui/BillDeleteForm.tsx b/app/ui/BillDeleteForm.tsx index b5d79b0..77d39ae 100644 --- a/app/ui/BillDeleteForm.tsx +++ b/app/ui/BillDeleteForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { FC, ReactNode } from "react"; +import { FC, ReactNode, useState } from "react"; import { Bill, BillingLocation } from "../lib/db-types"; import { useFormState } from "react-dom"; import { Main } from "./Main"; @@ -19,6 +19,7 @@ export const BillDeleteForm:FC = ({ bill, location }) => { const handleAction = deleteBillById.bind(null, location._id, bill._id, year, month); const [ state, dispatch ] = useFormState(handleAction, null); const t = useTranslations("bill-delete-form"); + const [deleteInSubsequentMonths, setDeleteInSubsequentMonths] = useState(false); return(
@@ -33,9 +34,35 @@ export const BillDeleteForm:FC = ({ bill, location }) => { }) }

+ +
+ +
+ + {deleteInSubsequentMonths && ( +
+
+ ⚠️ +
+

{t("warning-title")}

+
{t("warning-message")}
+
+
+
+ )} +
- - {t("cancel-button")} + + {t("cancel-button")}
diff --git a/app/ui/BillEditForm.tsx b/app/ui/BillEditForm.tsx index 9f605e5..29a61a1 100644 --- a/app/ui/BillEditForm.tsx +++ b/app/ui/BillEditForm.tsx @@ -202,6 +202,16 @@ export const BillEditForm:FC = ({ location, bill }) => { ))} + {/* Show toggle only when adding a new bill (not editing) */} + {!bill && ( +
+ +
+ )} +
{t("cancel-button")} diff --git a/app/ui/HomePage.tsx b/app/ui/HomePage.tsx index 2f3f59a..df76dc6 100644 --- a/app/ui/HomePage.tsx +++ b/app/ui/HomePage.tsx @@ -32,7 +32,7 @@ export const HomePage:FC = async ({ searchParams }) => { return (); } - const currentYear = Number(searchParams?.year) || availableYears[0]; + const currentYear = Number(searchParams?.year) || new Date().getFullYear(); const locations = await fetchAllLocations(currentYear); diff --git a/app/ui/LocationDeleteForm.tsx b/app/ui/LocationDeleteForm.tsx index 0524aa9..c51b456 100644 --- a/app/ui/LocationDeleteForm.tsx +++ b/app/ui/LocationDeleteForm.tsx @@ -1,10 +1,9 @@ "use client"; -import { FC, ReactNode } from "react"; +import { FC, ReactNode, useState } from "react"; import { BillingLocation } from "../lib/db-types"; import { deleteLocationById } from "../lib/actions/locationActions"; import { useFormState } from "react-dom"; -import { gotoUrl } from "../lib/actions/navigationActions"; import Link from "next/link"; import { useTranslations } from "next-intl"; @@ -16,13 +15,9 @@ export interface LocationDeleteFormProps { export const LocationDeleteForm:FC = ({ location }) => { const handleAction = deleteLocationById.bind(null, location._id, location.yearMonth); - const [ state, dispatch ] = useFormState(handleAction, null); + const [ , dispatch ] = useFormState(handleAction, null); const t = useTranslations("location-delete-form"); - - - const handleCancel = () => { - gotoUrl(`/location/${location._id}/edit/`); - }; + const [deleteInSubsequentMonths, setDeleteInSubsequentMonths] = useState(false); return(
@@ -36,6 +31,31 @@ export const LocationDeleteForm:FC = ({ location }) => }) }

+ +
+ +
+ + {deleteInSubsequentMonths && ( +
+
+ ⚠️ +
+

{t("warning-title")}

+
{t("warning-message")}
+
+
+
+ )}
{t("cancel-button")} diff --git a/app/ui/LocationEditForm.tsx b/app/ui/LocationEditForm.tsx index a64ea24..cf95c0c 100644 --- a/app/ui/LocationEditForm.tsx +++ b/app/ui/LocationEditForm.tsx @@ -60,6 +60,36 @@ export const LocationEditForm:FC = ({ location, yearMonth ))}
+ {/* Show different options for add vs edit operations */} + {!location ? ( +
+ +
+ ) : ( +
+
+ {t("update-scope")} +
+
+ + + +
+
+ )} +
{ state.message && diff --git a/messages/en.json b/messages/en.json index 98edd7c..9bbc3e0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -62,9 +62,12 @@ } }, "bill-delete-form": { - "text": "Please confirm deletion of bill “{bill_name}” at “{location_name}”.", + "text": "Please confirm deletion of bill \"{bill_name}\" at \"{location_name}\".", "cancel-button": "Cancel", - "confirm-button": "Confirm" + "confirm-button": "Confirm", + "delete-in-subsequent-months": "Also delete in all subsequent months", + "warning-title": "Warning", + "warning-message": "This operation cannot be undone and will delete the bill in all future months!" }, "bill-edit-form": { "bill-name-placeholder": "Bill name", @@ -77,6 +80,7 @@ "save-button": "Save", "cancel-button": "Cancel", "delete-tooltip": "Delete bill", + "add-to-subsequent-months": "Add to all subsequent months", "validation": { "bill-name-required": "Bill name is required", "payed-amount-required": "Payed amount is required", @@ -88,9 +92,12 @@ "back-button": "Back" }, "location-delete-form": { - "text": "Please confirm deletion of realestate “{name}””.", + "text": "Please confirm deletion of realestate \"{name}\".", "cancel-button": "Cancel", - "confirm-button": "Confirm" + "confirm-button": "Confirm", + "delete-in-subsequent-months": "Also delete in all subsequent months", + "warning-title": "Warning", + "warning-message": "This operation cannot be undone and will delete the location in all future months!" }, "location-edit-form": { "location-name-placeholder": "Realestate name", @@ -98,6 +105,11 @@ "save-button": "Save", "cancel-button": "Cancel", "delete-tooltip": "Delete realestate", + "add-to-subsequent-months": "Add to all subsequent months", + "update-scope": "Update scope:", + "update-current-month": "current month only", + "update-subsequent-months": "current and all future months", + "update-all-months": "all months", "validation": { "location-name-required": "Relaestate name is required" } diff --git a/messages/hr.json b/messages/hr.json index cdc916b..ada51c9 100644 --- a/messages/hr.json +++ b/messages/hr.json @@ -62,9 +62,12 @@ } }, "bill-delete-form": { - "text": "Molim potvrdi brisanje računa “{bill_name}” koji pripada nekretnini “{location_name}”.", + "text": "Molim potvrdi brisanje računa \"{bill_name}\" koji pripada nekretnini \"{location_name}\".", "cancel-button": "Odustani", - "confirm-button": "Potvrdi" + "confirm-button": "Potvrdi", + "delete-in-subsequent-months": "Također obriši i u svim mjesecima koji slijede", + "warning-title": "Upozorenje", + "warning-message": "Ova operacija je nepovratna i obrisat će račun u svim mjesecima koji slijede!" }, "bill-edit-form": { "bill-name-placeholder": "Ime računa", @@ -77,6 +80,7 @@ "save-button": "Spremi", "cancel-button": "Odbaci", "delete-tooltip": "Obriši račun", + "add-to-subsequent-months": "Dodaj u sve mjesece koji slijede", "validation": { "bill-name-required": "Ime računa je obavezno", "not-a-number": "Vrijednost mora biti brojka", @@ -87,9 +91,12 @@ "back-button": "Nazad" }, "location-delete-form": { - "text": "Molim potvrdi brisanje nekretnine “{name}””.", + "text": "Molim potvrdi brisanje nekretnine \"{name}\".", "cancel-button": "Odustani", - "confirm-button": "Potvrdi" + "confirm-button": "Potvrdi", + "delete-in-subsequent-months": "Također obriši i u svim mjesecima koji slijede", + "warning-title": "Upozorenje", + "warning-message": "Ova operacija je nepovratna i obrisat će lokaciju u svim mjesecima koji slijede!" }, "location-edit-form": { "location-name-placeholder": "Ime nekretnine", @@ -97,6 +104,11 @@ "save-button": "Spremi", "cancel-button": "Odbaci", "delete-tooltip": "Brisanje nekretnine", + "add-to-subsequent-months": "Dodaj u sve mjesece koji slijede", + "update-scope": "Opseg ažuriranja:", + "update-current-month": "samo trenutni mjesec", + "update-subsequent-months": "trenutni i svi budući mjeseci", + "update-all-months": "svi mjeseci", "validation": { "location-name-required": "Ime nekretnine je obavezno" }