Merge branch 'feature/default-active-year' into develop

This commit is contained in:
2025-08-11 12:25:13 +02:00
12 changed files with 555 additions and 73 deletions

5
.vscode/launch.json vendored
View File

@@ -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`

72
CLAUDE.md Normal file
View File

@@ -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

View File

@@ -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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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 }
}
}
}
}));
// 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<BillingLocation>("lokacije").bulkWrite(updateOperations);
}
}
});
}
} else {
// Delete only from current location (original behavior)
await dbClient.collection<BillingLocation>("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 };
});

View File

@@ -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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("lokacije").updateOne(
{
_id: locationId,
userId
},
{
$set: {
name: locationName,
notes: locationNotes,
}
}
});
);
} else if (updateScope === "subsequent") {
// Update current and all subsequent months
await dbClient.collection<BillingLocation>("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<BillingLocation>("lokacije").updateMany(
{
userId,
name: currentLocation.name
},
{
$set: {
name: locationName,
notes: locationNotes,
}
}
);
}
} else if(yearMonth) {
// Always add location to the specified month
await dbClient.collection<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("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<BillingLocation>("lokacije").deleteOne({ _id: locationID, userId });
}
await gotoHome(yearMonth)
await gotoHome(yearMonth);
return {
message: null
};
})

View File

@@ -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 = {

View File

@@ -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<BillDeleteFormProps> = ({ 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(
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
@@ -33,9 +34,35 @@ export const BillDeleteForm:FC<BillDeleteFormProps> = ({ bill, location }) => {
})
}
</p>
<div className="form-control">
<label className="label cursor-pointer justify-center gap-4">
<span className="label-text">{t("delete-in-subsequent-months")}</span>
<input
type="checkbox"
name="deleteInSubsequentMonths"
className="toggle toggle-error"
checked={deleteInSubsequentMonths}
onChange={(e) => setDeleteInSubsequentMonths(e.target.checked)}
/>
</label>
</div>
{deleteInSubsequentMonths && (
<div className="border-l-4 border-error bg-error/10 p-4 mt-4 rounded-r max-w-[24rem] mx-auto">
<div className="flex items-start gap-3">
<span className="text-xl flex-shrink-0"></span>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-error break-words">{t("warning-title")}</h3>
<div className="text-sm text-error/80 break-words">{t("warning-message")}</div>
</div>
</div>
</div>
)}
<div className="pt-4 text-center">
<button className="btn btn-primary">{t("confirm-button")}</button>
<Link className="btn btn-neutral ml-3" href={`/bill/${location._id}-${bill._id}/edit/`}>{t("cancel-button")}</Link>
<button className="btn btn-primary w-[5.5em]">{t("confirm-button")}</button>
<Link className="btn btn-neutral w-[5.5em] ml-3" href={`/bill/${location._id}-${bill._id}/edit/`}>{t("cancel-button")}</Link>
</div>
</form>
</div>

View File

@@ -202,6 +202,16 @@ export const BillEditForm:FC<BillEditFormProps> = ({ location, bill }) => {
))}
</div>
{/* Show toggle only when adding a new bill (not editing) */}
{!bill && (
<div className="form-control mt-4">
<label className="label cursor-pointer">
<span className="label-text">{t("add-to-subsequent-months")}</span>
<input type="checkbox" name="addToSubsequentMonths" className="toggle toggle-primary" />
</label>
</div>
)}
<div className="pt-4">
<button type="submit" className="btn btn-primary">{t("save-button")}</button>
<Link className="btn btn-neutral ml-3" href={`/?year=${billYear}&month=${billMonth}`}>{t("cancel-button")}</Link>

View File

@@ -32,7 +32,7 @@ export const HomePage:FC<HomePageProps> = async ({ searchParams }) => {
return (<MonthLocationList />);
}
const currentYear = Number(searchParams?.year) || availableYears[0];
const currentYear = Number(searchParams?.year) || new Date().getFullYear();
const locations = await fetchAllLocations(currentYear);

View File

@@ -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<LocationDeleteFormProps> = ({ 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(
<div className="card card-compact card-bordered min-w-[20em] max-w-[90em] bg-base-100 shadow-s my-1">
@@ -36,6 +31,31 @@ export const LocationDeleteForm:FC<LocationDeleteFormProps> = ({ location }) =>
})
}
</p>
<div className="form-control">
<label className="label cursor-pointer justify-center gap-4">
<span className="label-text">{t("delete-in-subsequent-months")}</span>
<input
type="checkbox"
name="deleteInSubsequentMonths"
className="toggle toggle-error"
checked={deleteInSubsequentMonths}
onChange={(e) => setDeleteInSubsequentMonths(e.target.checked)}
/>
</label>
</div>
{deleteInSubsequentMonths && (
<div className="border-l-4 border-error bg-error/10 p-4 mt-4 rounded-r max-w-[24rem]">
<div className="flex items-start gap-3">
<span className="text-xl flex-shrink-0"></span>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-error break-words">{t("warning-title")}</h3>
<div className="text-sm text-error/80 break-words">{t("warning-message")}</div>
</div>
</div>
</div>
)}
<div className="pt-4 text-center">
<button className="btn btn-primary w-[5.5em]">{t("confirm-button")}</button>
<Link className="btn btn-neutral w-[5.5em] ml-3" href={`/location/${location._id}/edit/`}>{t("cancel-button")}</Link>

View File

@@ -60,6 +60,36 @@ export const LocationEditForm:FC<LocationEditFormProps> = ({ location, yearMonth
))}
</div>
{/* Show different options for add vs edit operations */}
{!location ? (
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">{t("add-to-subsequent-months")}</span>
<input type="checkbox" name="addToSubsequentMonths" className="toggle toggle-primary" />
</label>
</div>
) : (
<div className="form-control">
<div className="label">
<span className="label-text font-medium">{t("update-scope")}</span>
</div>
<div className="flex flex-col gap-1 ml-4">
<label className="label cursor-pointer justify-start gap-3 py-1">
<input type="radio" name="updateScope" value="current" className="radio radio-primary" defaultChecked />
<span className="label-text">{t("update-current-month")}</span>
</label>
<label className="label cursor-pointer justify-start gap-3 py-1">
<input type="radio" name="updateScope" value="subsequent" className="radio radio-primary" />
<span className="label-text">{t("update-subsequent-months")}</span>
</label>
<label className="label cursor-pointer justify-start gap-3 py-1">
<input type="radio" name="updateScope" value="all" className="radio radio-primary" />
<span className="label-text">{t("update-all-months")}</span>
</label>
</div>
</div>
)}
<div id="status-error" aria-live="polite" aria-atomic="true">
{
state.message &&

View File

@@ -62,9 +62,12 @@
}
},
"bill-delete-form": {
"text": "Please confirm deletion of bill <strong>{bill_name}</strong> at <strong>{location_name}</strong>.",
"text": "Please confirm deletion of bill \"<strong>{bill_name}</strong>\" at \"<strong>{location_name}</strong>\".",
"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 <strong>{name}</strong>””.",
"text": "Please confirm deletion of realestate \"<strong>{name}</strong>\".",
"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"
}

View File

@@ -62,9 +62,12 @@
}
},
"bill-delete-form": {
"text": "Molim potvrdi brisanje računa <strong>{bill_name}</strong> koji pripada nekretnini <strong>{location_name}</strong>.",
"text": "Molim potvrdi brisanje računa \"<strong>{bill_name}</strong>\" koji pripada nekretnini \"<strong>{location_name}</strong>\".",
"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 <strong>{name}</strong>””.",
"text": "Molim potvrdi brisanje nekretnine \"<strong>{name}</strong>\".",
"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"
}