Compare commits

...

10 Commits

Author SHA1 Message Date
Deep Koluguri 9a40854884 Add Dockerfiles and Gitea CI workflow 2026-05-11 15:29:09 -04:00
Deep Koluguri c4439e76a8 update draw 2026-05-04 23:02:52 -04:00
Deep Koluguri e404721818 i8n 2026-04-06 01:03:36 -04:00
Deep Koluguri 45a12ea5bc i8n 2026-04-06 01:00:14 -04:00
Deep Koluguri b7b2aaf8a6 i8n 2026-04-06 00:59:31 -04:00
Deep Koluguri 6e19f57e27 i8n 2026-04-06 00:40:01 -04:00
Deep Koluguri 51e9c72464 i8n 2026-04-06 00:36:25 -04:00
Deep Koluguri bf37c3c1ed i8n 2026-04-06 00:28:57 -04:00
Deep Koluguri 644b2cd367 i8n 2026-04-06 00:20:54 -04:00
Deep Koluguri 2ee17c2edc i8n 2026-04-06 00:17:49 -04:00
17 changed files with 2507 additions and 1861 deletions

View File

@ -0,0 +1,38 @@
name: Build and Deploy Chitfund
on: [push]
jobs:
build-backend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push Backend
uses: docker/build-push-action@v4
with:
context: ./backend
push: true
tags: 192.168.8.250:5000/chitfund-backend:latest
# Local registry is insecure
labels: |
org.opencontainers.image.source=${{ gitea.repository_url }}
build-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push Frontend
uses: docker/build-push-action@v4
with:
context: ./luckychit
push: true
tags: 192.168.8.250:5000/chitfund-frontend:latest

16
backend/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
# Backend Dockerfile for Chitfund
FROM node:20-slim
WORKDIR /app
# Install build essentials for bcrypt if needed (though pre-built binaries usually work on slim)
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

View File

@ -633,7 +633,7 @@ const deleteMonthlyDraw = async (req, res) => {
const updateMonthlyDraw = async (req, res) => {
try {
const { draw_id } = req.params;
const { winner_id, prize_amount, notes } = req.body;
const { winner_id, prize_amount, notes, month, year, draw_date } = req.body;
const managerId = req.user.id;
// Find the draw
@ -664,9 +664,64 @@ const updateMonthlyDraw = async (req, res) => {
const updates = {};
// Update winner if provided
let nextMonth = monthlyDraw.month;
let nextYear = monthlyDraw.year;
const monthProvided = month !== undefined && month !== null && month !== '';
const yearProvided = year !== undefined && year !== null && year !== '';
if (monthProvided) {
nextMonth = parseInt(month, 10);
}
if (yearProvided) {
nextYear = parseInt(year, 10);
}
if (monthProvided || yearProvided) {
if (Number.isNaN(nextMonth) || nextMonth < 1 || nextMonth > 12) {
return res.status(400).json({
success: false,
message: 'Month must be between 1 and 12'
});
}
if (Number.isNaN(nextYear) || nextYear < 2020) {
return res.status(400).json({
success: false,
message: 'Year must be 2020 or later'
});
}
const conflict = await MonthlyDraw.findOne({
where: {
group_id: monthlyDraw.group_id,
month: nextMonth,
year: nextYear,
id: { [Op.ne]: draw_id }
}
});
if (conflict) {
return res.status(400).json({
success: false,
message: `Another draw already exists for ${nextMonth}/${nextYear}`
});
}
updates.month = nextMonth;
updates.year = nextYear;
}
if (draw_date !== undefined && draw_date !== null && draw_date !== '') {
const d = new Date(draw_date);
if (Number.isNaN(d.getTime())) {
return res.status(400).json({
success: false,
message: 'Invalid draw_date'
});
}
updates.draw_date = d;
}
// Update winner if provided and different from current
if (winner_id) {
// Verify the winner is a member of the group
const winnerStr = String(winner_id);
const currentWinnerStr = monthlyDraw.winner_id != null ? String(monthlyDraw.winner_id) : '';
if (winnerStr !== currentWinnerStr) {
const member = await GroupMember.findOne({
where: {
group_id: monthlyDraw.ChitGroup.id,
@ -683,22 +738,46 @@ const updateMonthlyDraw = async (req, res) => {
});
}
const duplicateWinner = await MonthlyDraw.findOne({
where: {
group_id: monthlyDraw.group_id,
winner_id,
id: { [Op.ne]: draw_id }
}
});
if (duplicateWinner) {
return res.status(400).json({
success: false,
message: `${member.User.full_name} has already won in another draw. Each member can only win once.`,
alreadyWon: true,
winnerName: member.User.full_name
});
}
updates.winner_id = winner_id;
if (!notes) {
if (notes === undefined || notes === null) {
updates.notes = `Winner updated to: ${member.User.full_name}`;
}
}
}
// Update prize amount if provided
if (prize_amount) {
// Update prize amount if provided (including 0)
if (prize_amount != null && prize_amount !== '') {
updates.prize_amount = prize_amount;
}
// Update notes if provided
if (notes) {
if (notes !== undefined && notes !== null) {
updates.notes = notes;
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({
success: false,
message: 'No valid fields to update'
});
}
// Perform update
await monthlyDraw.update(updates);

18
luckychit/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# Frontend Dockerfile for Chitfund (Flutter Web)
FROM node:20-slim
WORKDIR /app
# We serve the build/web directory.
# NOTE: This Dockerfile assumes you have already run 'flutter build web'
# or we will use a multi-stage build if you prefer.
# For simplicity in this home lab, we copy the build output.
COPY package*.json ./
RUN npm install express
COPY . .
EXPOSE 8080
CMD ["node", "web-server.js"]

View File

@ -1,6 +0,0 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
synthetic-package: false
output-dir: lib/l10n

View File

@ -584,6 +584,11 @@ class ApiService {
if (responseData is Map<String, dynamic>) {
message = responseData['message']?.toString() ?? 'Request failed';
final merged = Map<String, dynamic>.from(responseData);
merged['success'] = false;
merged['message'] = message;
merged['statusCode'] = e.response?.statusCode;
return merged;
} else if (responseData is String) {
message = responseData;
}

View File

@ -8,6 +8,7 @@ import '../../core/services/auth_service.dart';
import '../../core/services/api_service.dart';
import '../../core/utils/snackbar_util.dart';
import '../../l10n/app_localizations.dart';
import '../../l10n/l10n_x.dart';
String _themeModeLabel(AppLocalizations l10n) {
switch (ThemeController.to.themeMode) {
@ -25,7 +26,7 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settingsTitle),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../core/services/chit_group_service.dart';
import '../../core/services/api_service.dart';
import '../../core/models/monthly_draw.dart';
@ -26,6 +27,33 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
String? _selectedMemberId;
bool _isLoading = false;
late int _editMonth;
late int _editYear;
late DateTime _drawDate;
static const _monthNames = <String>[
'', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
];
DateTime _baselineDrawDate() {
final d = widget.draw.drawDate;
if (d.millisecondsSinceEpoch == 0) {
return DateTime(widget.draw.year, widget.draw.month, 1);
}
return d;
}
bool _sameCalendarDay(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day;
}
int _clampEditYear(int y) {
final minY = 2020;
final maxY = DateTime.now().year + 1;
if (y < minY) return minY;
if (y > maxY) return maxY;
return y;
}
@override
void initState() {
@ -33,6 +61,9 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
_selectedMemberId = widget.draw.winnerId;
_prizeAmountController.text = widget.draw.prizeAmount.toStringAsFixed(0);
_notesController.text = widget.draw.notes ?? '';
_editMonth = widget.draw.month.clamp(1, 12);
_editYear = _clampEditYear(widget.draw.year);
_drawDate = _baselineDrawDate();
}
@override
@ -42,6 +73,18 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
super.dispose();
}
Future<void> _pickDrawDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _drawDate,
firstDate: DateTime(2020),
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
);
if (picked != null) {
setState(() => _drawDate = picked);
}
}
Future<void> _handleSubmit() async {
if (_formKey.currentState!.validate()) {
if (_selectedMemberId == null) {
@ -54,7 +97,16 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
try {
final updates = <String, dynamic>{};
// Only include changed fields
if (_editMonth != widget.draw.month) {
updates['month'] = _editMonth;
}
if (_editYear != widget.draw.year) {
updates['year'] = _editYear;
}
if (!_sameCalendarDay(_drawDate, _baselineDrawDate())) {
updates['draw_date'] = _drawDate.toUtc().toIso8601String();
}
if (_selectedMemberId != widget.draw.winnerId) {
updates['winner_id'] = _selectedMemberId;
}
@ -64,7 +116,7 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
updates['prize_amount'] = newPrizeAmount;
}
if (_notesController.text.isNotEmpty && _notesController.text != widget.draw.notes) {
if (_notesController.text != (widget.draw.notes ?? '')) {
updates['notes'] = _notesController.text;
}
@ -82,12 +134,22 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
title: 'Success',
);
Get.back(result: true);
} else {
final isAlreadyWon = response['alreadyWon'] ?? false;
final winnerName = response['winnerName'] ?? '';
if (isAlreadyWon && winnerName.isNotEmpty) {
SnackbarUtil.showError(
'$winnerName has already won in another draw.\nEach member can only win once.',
title: 'Duplicate winner',
duration: const Duration(seconds: 4),
);
} else {
SnackbarUtil.showError(
response['message'] ?? 'Failed to update draw',
title: 'Error',
);
}
}
} catch (e) {
SnackbarUtil.showError('Error: ${e.toString()}');
} finally {
@ -137,7 +199,7 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
),
),
Text(
'Month ${widget.draw.month}/${widget.draw.year}',
'Originally ${widget.draw.month}/${widget.draw.year}',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
@ -191,6 +253,108 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
),
SizedBox(height: 24.h),
Text(
'Draw month & year',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: DropdownButtonFormField<int>(
value: _editMonth,
decoration: InputDecoration(
labelText: 'Month',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
filled: true,
fillColor: Colors.grey.shade50,
),
items: List.generate(12, (i) {
final m = i + 1;
return DropdownMenuItem(
value: m,
child: Text('${_monthNames[m]} ($m)'),
);
}),
onChanged: (v) {
if (v != null) setState(() => _editMonth = v);
},
),
),
SizedBox(width: 12.w),
Expanded(
child: DropdownButtonFormField<int>(
value: _editYear,
decoration: InputDecoration(
labelText: 'Year',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
filled: true,
fillColor: Colors.grey.shade50,
),
items: List.generate(
DateTime.now().year - 2019 + 2,
(i) {
final y = 2020 + i;
return DropdownMenuItem(
value: y,
child: Text('$y'),
);
},
),
onChanged: (v) {
if (v != null) setState(() => _editYear = v);
},
),
),
],
),
SizedBox(height: 8.h),
Text(
'This is the calendar month this draw belongs to. It must not clash with another draw in this group.',
style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade600),
),
SizedBox(height: 20.h),
Text(
'Draw date',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8.h),
InkWell(
onTap: _pickDrawDate,
borderRadius: BorderRadius.circular(12.r),
child: InputDecorator(
decoration: InputDecoration(
prefixIcon: const Icon(Icons.calendar_today_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
),
filled: true,
fillColor: Colors.grey.shade50,
),
child: Text(
DateFormat('d MMM yyyy').format(_drawDate),
style: TextStyle(fontSize: 16.sp),
),
),
),
SizedBox(height: 8.h),
Text(
'When the draw actually happened (shown in history).',
style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade600),
),
SizedBox(height: 20.h),
// Select Winner
Text(
'Winner',

View File

@ -0,0 +1,166 @@
{
"@memberWelcomeGreeting": {
"placeholders": {
"name": {}
}
},
"memberWelcomeGreeting": "Welcome, {name}!",
"ok": "OK",
"save": "Save",
"retry": "Retry",
"delete": "Delete",
"userLabel": "User",
"managerFallbackName": "Manager",
"snackTitleError": "Error",
"snackTitleSuccess": "Success",
"operationFailedShort": "Failed",
"failedLoadChitGroups": "Failed to load chit groups",
"chitfundCreatedSuccess": "Chitfund created successfully",
"failedCreateChitGroup": "Failed to create chit group",
"failedUpdateChitGroup": "Failed to update chit group",
"chitGroupDeletedSuccess": "Chit group deleted successfully",
"failedDeleteChitGroup": "Failed to delete chit group",
"failedLoadGroupDetails": "Failed to load group details",
"failedLoadGroupMembers": "Failed to load group members",
"memberAddedSuccess": "Member added successfully",
"failedAddMember": "Failed to add member",
"memberRemovedSuccess": "Member removed successfully",
"failedRemoveMember": "Failed to remove member",
"memberStatusUpdatedSuccess": "Member status updated successfully",
"failedUpdateMemberStatus": "Failed to update member status",
"failedLoadPayments": "Failed to load payments",
"paymentRecordedSuccess": "Payment recorded successfully",
"failedRecordPayment": "Failed to record payment",
"failedLoadGroupStatistics": "Failed to load group statistics",
"chitfundStartedSuccess": "Chitfund started successfully",
"failedStartChitGroup": "Failed to start chit group",
"failedLoadMonthlyDraws": "Failed to load monthly draws",
"failedCreateMonthlyDraw": "Failed to create monthly draw",
"failedLoadDrawStatistics": "Failed to load draw statistics",
"failedLoadFinancialData": "Failed to load financial data",
"signupFailedTitle": "Signup Failed",
"signupFailedGeneric": "Signup failed. Please try again.",
"loginFailedTitle": "Login Failed",
"loginFailedGeneric": "Login failed. Please try again.",
"passwordChangedSuccess": "Password changed successfully",
"failedChangePassword": "Failed to change password",
"stateSomethingWentWrong": "Something went wrong",
"emptyNoGroupsTitle": "No Chit Groups Yet",
"emptyNoGroupsMessage": "You haven't created any chit groups yet.\nCreate your first group or import an existing one!",
"emptyNoGroupsAction": "Create Group",
"emptyNoMembersTitle": "No Members Yet",
"emptyNoMembersMessage": "This group doesn't have any members yet.\nAdd members to get started!",
"emptyNoMembersAction": "Add Members",
"emptyNoPaymentsTitle": "No Payments Yet",
"emptyNoPaymentsMessage": "No payment records found.\nPayments will appear here once recorded.",
"emptyNoPaymentsAction": "Record Payment",
"emptyNoActivitiesTitle": "No Recent Activities",
"emptyNoActivitiesMessage": "Your recent activities will appear here.\nStart using the app to see updates!",
"emptyNoActivitiesAction": "Refresh",
"emptyNoResultsTitle": "No Results Found",
"emptyNoResultsMessage": "We couldn't find what you're looking for.\nTry adjusting your search or filters.",
"emptyNoResultsAction": "Clear Filters",
"emptyErrorTitle": "Oops! Something Went Wrong",
"emptyErrorMessage": "We encountered an error while loading data.\nPlease try again.",
"emptyErrorAction": "Retry",
"emptyNoInternetTitle": "No Internet Connection",
"emptyNoInternetMessage": "Please check your internet connection\nand try again.",
"emptyNoInternetAction": "Retry",
"dashboardTitle": "Dashboard",
"notificationsTooltip": "Notifications",
"recordingsTooltip": "View draw recordings",
"testDrawTooltip": "Test animated draw",
"chitFundManagerRole": "Chit Fund Manager",
"menuDashboard": "Dashboard",
"menuMyChitfunds": "My Chitfunds",
"menuMembers": "Members",
"menuPayments": "Payments",
"menuLotteryDraws": "Lottery Draws",
"menuReports": "Reports",
"welcomeBackTitle": "Welcome back!",
"welcomeBackSubtitle": "Here's what's happening with your chit funds today.",
"quickActionsTitle": "Quick Actions",
"qaCreateChitfundTitle": "Create New Chitfund",
"qaCreateChitfundSubtitle": "Start a new chit fund group",
"qaImportChitfundTitle": "Import Existing Chitfund",
"qaImportChitfundSubtitle": "Add a group that already started",
"qaViewAllChitfundsTitle": "View All Chitfunds",
"qaViewAllChitfundsSubtitle": "Manage your existing groups",
"qaManageMembersTitle": "Manage Members",
"qaManageMembersSubtitle": "Add or remove members",
"qaPaymentRecordsTitle": "Payment Records",
"qaPaymentRecordsSubtitle": "Track all transactions",
"sectionMyChitfunds": "My Chitfunds",
"viewAll": "View All",
"noChitFundsYetShort": "No chit funds yet",
"groupStatusActive": "Active",
"groupStatusForming": "Forming",
"groupStatusCompleted": "Completed",
"unnamedGroup": "Unnamed",
"actionRecord": "Record",
"actionDraw": "Draw",
"actionView": "View",
"actionManageGroup": "Manage Group",
"groupImportedMessage": "Group imported! Now add members and backfill past data.",
"groupImportedTitle": "Success",
"paymentsPageComingSoon": "Payments page will be implemented next",
"comingSoonTitle": "Coming Soon",
"pageMyChitfunds": "My Chitfunds",
"createNewGroupMenu": "Create New Group",
"importExistingGroupMenu": "Import Existing Group",
"appDisplayName": "LuckyChit",
"authLoginTagline": "Chit fund management that feels effortless.",
"authSignupScreenTitle": "Create account",
"authSignupTagline": "Set up your profile in under a minute.",
"labelMobileNumber": "Mobile number",
"labelMobileNumberRequired": "Mobile number *",
"labelPassword": "Password",
"labelPasswordRequired": "Password *",
"labelFullNameRequired": "Full name *",
"labelEmailOptional": "Email (optional)",
"labelAddressOptional": "Address (optional)",
"labelEmergencyContactOptional": "Emergency contact (optional)",
"labelConfirmPasswordRequired": "Confirm password *",
"validatorEnterMobile": "Please enter mobile number",
"validatorMobileTenDigits": "Mobile number must be 10 digits",
"validatorMobileDigitsOnly": "Mobile number must contain only digits",
"validatorEnterFullName": "Please enter your full name",
"validatorValidEmail": "Please enter a valid email address",
"validatorEmergencyTenDigits": "Emergency contact must be 10 digits",
"validatorEmergencyDigitsOnly": "Emergency contact must contain only digits",
"validatorEnterPasswordAuth": "Please enter password",
"validatorPasswordMinSixAuth": "Password must be at least 6 characters",
"validatorConfirmPassword": "Please confirm password",
"validatorPasswordsMismatch": "Passwords do not match",
"tooltipShowPassword": "Show password",
"tooltipHidePassword": "Hide password",
"signInButton": "Sign in",
"createAccountButton": "Create account",
"alreadyHaveAccount": "Already have an account? ",
"loginLink": "Login",
"loginInvalidCredentials": "Invalid mobile number or password. Please try again.",
"signupSuccessWelcome": "Account created successfully! Welcome to LuckyChit.",
"signupFailedGenericUi": "Failed to create account. Please try again.",
"featureComingSoonMessage": "Feature coming soon",
"memberFallbackName": "Member",
"memberSubtitleEmpty": "Join a chit fund to start managing your investments.",
"memberSubtitleHasGroups": "Manage your chit fund investments and track your payments.",
"navHome": "Home",
"navPayments": "Payments",
"navNotifications": "Notifications",
"navProfile": "Profile",
"memberEmptyChitTitle": "No Chit Funds Yet",
"memberEmptyChitBody": "You haven't joined any chit funds yet.\nContact your manager to get started!",
"memberHowToStartTitle": "How to get started?",
"memberHowToStartBody": "1. Your manager will add you to a chit group\n2. You'll receive a notification\n3. Start managing your payments here!",
"unnamedGroupLong": "Unnamed Group",
"labelTotalValue": "Total Value",
"labelDuration": "Duration",
"monthsSuffix": "months",
"labelInstallment": "Installment",
"labelStatus": "Status",
"groupStatusPending": "Pending",
"payNowButton": "Pay Now",
"detailsButton": "Details",
"memberInfoNotFound": "Member information not found"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,166 @@
{
"@memberWelcomeGreeting": {
"placeholders": {
"name": {}
}
},
"memberWelcomeGreeting": "స్వాగతం, {name}!",
"ok": "సరే",
"save": "సేవ్",
"retry": "మళ్లీ ప్రయత్నించండి",
"delete": "తొలగించు",
"userLabel": "వినియోగదారు",
"managerFallbackName": "మేనేజర్",
"snackTitleError": "లోపం",
"snackTitleSuccess": "విజయం",
"operationFailedShort": "విఫలమైంది",
"failedLoadChitGroups": "చిట్ గ్రూప్‌లను లోడ్ చేయడంలో విఫలమైంది",
"chitfundCreatedSuccess": "చిట్‌ఫండ్ విజయవంతంగా సృష్టించబడింది",
"failedCreateChitGroup": "చిట్ గ్రూప్ సృష్టించడంలో విఫలమైంది",
"failedUpdateChitGroup": "చిట్ గ్రూప్ నవీకరించడంలో విఫలమైంది",
"chitGroupDeletedSuccess": "చిట్ గ్రూప్ విజయవంతంగా తొలగించబడింది",
"failedDeleteChitGroup": "చిట్ గ్రూప్ తొలగించడంలో విఫలమైంది",
"failedLoadGroupDetails": "గ్రూప్ వివరాలను లోడ్ చేయడంలో విఫలమైంది",
"failedLoadGroupMembers": "గ్రూప్ సభ్యులను లోడ్ చేయడంలో విఫలమైంది",
"memberAddedSuccess": "సభ్యుడు విజయవంతంగా జోడించబడ్డారు",
"failedAddMember": "సభ్యుడిని జోడించడంలో విఫలమైంది",
"memberRemovedSuccess": "సభ్యుడు విజయవంతంగా తొలగించబడ్డారు",
"failedRemoveMember": "సభ్యుడిని తొలగించడంలో విఫలమైంది",
"memberStatusUpdatedSuccess": "సభ్య స్థితి విజయవంతంగా నవీకరించబడింది",
"failedUpdateMemberStatus": "సభ్య స్థితిని నవీకరించడంలో విఫలమైంది",
"failedLoadPayments": "చెల్లింపులను లోడ్ చేయడంలో విఫలమైంది",
"paymentRecordedSuccess": "చెల్లింపు విజయవంతంగా నమోదు చేయబడింది",
"failedRecordPayment": "చెల్లింపు నమోదు చేయడంలో విఫలమైంది",
"failedLoadGroupStatistics": "గ్రూప్ గణాంకాలను లోడ్ చేయడంలో విఫలమైంది",
"chitfundStartedSuccess": "చిట్‌ఫండ్ విజయవంతంగా ప్రారంభమైంది",
"failedStartChitGroup": "చిట్ గ్రూప్ ప్రారంభించడంలో విఫలమైంది",
"failedLoadMonthlyDraws": "నెలవారీ డ్రాలను లోడ్ చేయడంలో విఫలమైంది",
"failedCreateMonthlyDraw": "నెలవారీ డ్రా సృష్టించడంలో విఫలమైంది",
"failedLoadDrawStatistics": "డ్రా గణాంకాలను లోడ్ చేయడంలో విఫలమైంది",
"failedLoadFinancialData": "ఆర్థిక డేటాను లోడ్ చేయడంలో విఫలమైంది",
"signupFailedTitle": "నమోదు విఫలమైంది",
"signupFailedGeneric": "నమోదు విఫలమైంది. దయచేసి మళ్లీ ప్రయత్నించండి.",
"loginFailedTitle": "లాగిన్ విఫలమైంది",
"loginFailedGeneric": "లాగిన్ విఫలమైంది. దయచేసి మళ్లీ ప్రయత్నించండి.",
"passwordChangedSuccess": "పాస్‌వర్డ్ విజయవంతంగా మార్చబడింది",
"failedChangePassword": "పాస్‌వర్డ్ మార్చడంలో విఫలమైంది",
"stateSomethingWentWrong": "ఏదో తప్పు జరిగింది",
"emptyNoGroupsTitle": "ఇంకా చిట్ గ్రూప్‌లు లేవు",
"emptyNoGroupsMessage": "మీరు ఇంకా చిట్ గ్రూప్‌లు సృష్టించలేదు.\nమొదటి గ్రూప్ సృష్టించండి లేదా ఉన్నదాన్ని దిగుమతి చేయండి!",
"emptyNoGroupsAction": "గ్రూప్ సృష్టించు",
"emptyNoMembersTitle": "ఇంకా సభ్యులు లేరు",
"emptyNoMembersMessage": "ఈ గ్రూప్‌కు ఇంకా సభ్యులు లేరు.\nప్రారభిచడానికి సభ్యులను జోడించండి!",
"emptyNoMembersAction": "సభ్యులను జోడించు",
"emptyNoPaymentsTitle": "ఇంకా చెల్లింపులు లేవు",
"emptyNoPaymentsMessage": "చెల్లింపు రికార్డ్‌లు కనుగొనబడలేదు.\nనమోదు చేసిన తర్వాత ఇక్కడ కనిపిస్తాయి.",
"emptyNoPaymentsAction": "చెల్లింపు నమోదు",
"emptyNoActivitiesTitle": "ఇటీవలి కార్యకలాపాలు లేవు",
"emptyNoActivitiesMessage": "మీ ఇటీవలి కార్యకలాపాలు ఇక్కడ కనిపిస్తాయి.\nనవీకరణల కోసం యాప్‌ను ఉపయోగించండి!",
"emptyNoActivitiesAction": "రిఫ్రెష్",
"emptyNoResultsTitle": "ఫలితాలు లేవు",
"emptyNoResultsMessage": "మీరు వెతుకుతున్నది కనుగొనలేకపోయాము.\nశోధన లేదా ఫిల్టర్‌లను మార్చి ప్రయత్నించండి.",
"emptyNoResultsAction": "ఫిల్టర్‌లు క్లియర్",
"emptyErrorTitle": "అయ్యో! ఏదో తప్పు జరిగింది",
"emptyErrorMessage": "డేటా లోడ్ చేస్తూ లోపం వచ్చింది.\nదయచేసి మళ్లీ ప్రయత్నించండి.",
"emptyErrorAction": "మళ్లీ ప్రయత్నించు",
"emptyNoInternetTitle": "ఇంటర్నెట్ కనెక్షన్ లేదు",
"emptyNoInternetMessage": "దయచేసి మీ ఇంటర్నెట్ కనెక్షన్‌ను తనిఖీ చేసి\nమళ్లీ ప్రయత్నించండి.",
"emptyNoInternetAction": "మళ్లీ ప్రయత్నించు",
"dashboardTitle": "డాష్‌బోర్డ్",
"notificationsTooltip": "నోటిఫికేషన్‌లు",
"recordingsTooltip": "డ్రా రికార్డింగ్‌లు చూడండి",
"testDrawTooltip": "యానిమేటెడ్ డ్రా పరీక్ష",
"chitFundManagerRole": "చిట్ ఫండ్ మేనేజర్",
"menuDashboard": "డాష్‌బోర్డ్",
"menuMyChitfunds": "నా చిట్‌ఫండ్‌లు",
"menuMembers": "సభ్యులు",
"menuPayments": "చెల్లింపులు",
"menuLotteryDraws": "లాటరీ డ్రాలు",
"menuReports": "నివేదికలు",
"welcomeBackTitle": "మళ్లీ స్వాగతం!",
"welcomeBackSubtitle": "ఈ రోజు మీ చిట్ ఫండ్‌లతో ఏమి జరుగుతోందో ఇక్కడ ఉంది.",
"quickActionsTitle": "త్వరిత చర్యలు",
"qaCreateChitfundTitle": "కొత్త చిట్‌ఫండ్ సృష్టించు",
"qaCreateChitfundSubtitle": "కొత్త చిట్ ఫండ్ గ్రూప్ ప్రారంభించండి",
"qaImportChitfundTitle": "ఉన్న చిట్‌ఫండ్ దిగుమతి",
"qaImportChitfundSubtitle": "ఇప్పటికే ప్రారంభమైన గ్రూప్ జోడించండి",
"qaViewAllChitfundsTitle": "అన్ని చిట్‌ఫండ్‌లు చూడండి",
"qaViewAllChitfundsSubtitle": "మీ గ్రూప్‌లను నిర్వహించండి",
"qaManageMembersTitle": "సభ్యులను నిర్వహించు",
"qaManageMembersSubtitle": "సభ్యులను జోడించండి లేదా తొలగించండి",
"qaPaymentRecordsTitle": "చెల్లింపు రికార్డ్‌లు",
"qaPaymentRecordsSubtitle": "అన్ని లావాదేవీలను ట్రాక్ చేయండి",
"sectionMyChitfunds": "నా చిట్‌ఫండ్‌లు",
"viewAll": "అన్నీ చూడండి",
"noChitFundsYetShort": "ఇంకా చిట్ ఫండ్‌లు లేవు",
"groupStatusActive": "సక్రియం",
"groupStatusForming": "ఏర్పాటులో",
"groupStatusCompleted": "పూర్తయింది",
"unnamedGroup": "పేరులేని",
"actionRecord": "నమోదు",
"actionDraw": "డ్రా",
"actionView": "చూడండి",
"actionManageGroup": "గ్రూప్ నిర్వహణ",
"groupImportedMessage": "గ్రూప్ దిగుమతి అయ్యింది! ఇప్పుడు సభ్యులను జోడించి గత డేటా నింపండి.",
"groupImportedTitle": "విజయం",
"paymentsPageComingSoon": "చెల్లింపుల పేజీ తదుపరి అమలు చేయబడుతుంది",
"comingSoonTitle": "త్వరలో",
"pageMyChitfunds": "నా చిట్‌ఫండ్‌లు",
"createNewGroupMenu": "కొత్త గ్రూప్ సృష్టించు",
"importExistingGroupMenu": "ఉన్న గ్రూప్ దిగుమతి",
"appDisplayName": "LuckyChit",
"authLoginTagline": "చిట్ ఫండ్ నిర్వహణ సులభంగా అనిపించేలా.",
"authSignupScreenTitle": "ఖాతా సృష్టించు",
"authSignupTagline": "ఒక నిమిషంలో మీ ప్రొఫైల్ సెటప్ చేయండి.",
"labelMobileNumber": "మొబైల్ నంబర్",
"labelMobileNumberRequired": "మొబైల్ నంబర్ *",
"labelPassword": "పాస్‌వర్డ్",
"labelPasswordRequired": "పాస్‌వర్డ్ *",
"labelFullNameRequired": "పూర్తి పేరు *",
"labelEmailOptional": "ఇమెయిల్ (ఐచ్ఛికం)",
"labelAddressOptional": "చిరునామా (ఐచ్ఛికం)",
"labelEmergencyContactOptional": "అత్యవసర సంప్రదింపు (ఐచ్ఛికం)",
"labelConfirmPasswordRequired": "పాస్‌వర్డ్ నిర్ధారించు *",
"validatorEnterMobile": "దయచేసి మొబైల్ నంబర్ నమోదు చేయండి",
"validatorMobileTenDigits": "మొబైల్ నంబర్ 10 అంకెలు ఉండాలి",
"validatorMobileDigitsOnly": "మొబైల్ నంబర్‌లో అంకెలు మాత్రమే ఉండాలి",
"validatorEnterFullName": "దయచేసి మీ పూర్తి పేరు నమోదు చేయండి",
"validatorValidEmail": "దయచేసి సరైన ఇమెయిల్ చిరునామా నమోదు చేయండి",
"validatorEmergencyTenDigits": "అత్యవసర సంప్రదింపు 10 అంకెలు ఉండాలి",
"validatorEmergencyDigitsOnly": "అత్యవసర సంప్రదింపులో అంకెలు మాత్రమే ఉండాలి",
"validatorEnterPasswordAuth": "దయచేసి పాస్‌వర్డ్ నమోదు చేయండి",
"validatorPasswordMinSixAuth": "పాస్‌వర్డ్ కనీసం 6 అక్షరాలు ఉండాలి",
"validatorConfirmPassword": "దయచేసి పాస్‌వర్డ్ నిర్ధారించండి",
"validatorPasswordsMismatch": "పాస్‌వర్డ్‌లు సరిపోలడం లేదు",
"tooltipShowPassword": "పాస్‌వర్డ్ చూపు",
"tooltipHidePassword": "పాస్‌వర్డ్ దాచు",
"signInButton": "సైన్ ఇన్",
"createAccountButton": "ఖాతా సృష్టించు",
"alreadyHaveAccount": "ఇప్పటికే ఖాతా ఉందా? ",
"loginLink": "లాగిన్",
"loginInvalidCredentials": "తప్పు మొబైల్ నంబర్ లేదా పాస్‌వర్డ్. మళ్లీ ప్రయత్నించండి.",
"signupSuccessWelcome": "ఖాతా విజయవంతంగా సృష్టించబడింది! LuckyChitకు స్వాగతం.",
"signupFailedGenericUi": "ఖాతా సృష్టించడంలో విఫలమైంది. మళ్లీ ప్రయత్నించండి.",
"featureComingSoonMessage": "ఫీచర్ త్వరలో వస్తుంది",
"memberFallbackName": "సభ్యుడు",
"memberSubtitleEmpty": "మీ పెట్టుబడులను నిర్వహించడానికి చిట్ ఫండ్‌లో చేరండి.",
"memberSubtitleHasGroups": "మీ చిట్ ఫండ్ పెట్టుబడులు మరియు చెల్లింపులను ట్రాక్ చేయండి.",
"navHome": "హోమ్",
"navPayments": "చెల్లింపులు",
"navNotifications": "నోటిఫికేషన్‌లు",
"navProfile": "ప్రొఫైల్",
"memberEmptyChitTitle": "ఇంకా చిట్ ఫండ్‌లు లేవు",
"memberEmptyChitBody": "మీరు ఇంకా ఏ చిట్ ఫండ్‌లోనూ చేరలేదు.\nప్రారభిచడానికి మీ మేనేజర్‌ను సంప్రదించండి!",
"memberHowToStartTitle": "ఎలా ప్రారంభించాలి?",
"memberHowToStartBody": "1. మీ మేనేజర్ మిమ్మల్ని చిట్ గ్రూప్‌కు జోడిస్తారు\n2. మీకు నోటిఫికేషన్ వస్తుంది\n3. ఇక్కడ మీ చెల్లింపులను నిర్వహించండి!",
"unnamedGroupLong": "పేరులేని గ్రూప్",
"labelTotalValue": "మొత్తం విలువ",
"labelDuration": "కాలపరిమితి",
"monthsSuffix": "నెలలు",
"labelInstallment": "వాయిదా",
"labelStatus": "స్థితి",
"groupStatusPending": "పెండింగ్",
"payNowButton": "ఇప్పుడు చెల్లించు",
"detailsButton": "వివరాలు",
"memberInfoNotFound": "సభ్య సమాచారం కనుగొనబడలేదు"
}

View File

@ -56,8 +56,8 @@ dependencies:
# WebSocket for real-time updates
web_socket_channel: ^2.4.0
# Date and Time
intl: ^0.19.0
# Date and time (version must satisfy flutter_localizations intl pin — e.g. 0.20.2 on current stable Flutter)
intl: 0.20.2
# Charts and Data Visualization
fl_chart: ^0.66.0

View File

@ -1,30 +1,15 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:luckychit/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
testWidgets('Smoke: MaterialApp renders', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: Center(child: Text('LuckyChit')),
),
),
);
expect(find.text('LuckyChit'), findsOneWidget);
});
}

View File

@ -0,0 +1,178 @@
#!/usr/bin/env node
/**
* Merges lib/l10n/app_{en,te}.arb with app_{en,te}_more.arb (in-memory) and
* writes lib/l10n/app_localizations.dart, app_localizations_en.dart, app_localizations_te.dart.
*
* Run from package root: node tool/sync_l10n.mjs
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, "..");
const l10nDir = path.join(root, "lib", "l10n");
function loadArb(name) {
const p = path.join(l10nDir, name);
return JSON.parse(fs.readFileSync(p, "utf8"));
}
function merge(base, extra) {
return { ...base, ...extra };
}
function messageKeys(arb) {
return Object.keys(arb).filter((k) => !k.startsWith("@") && k !== "@@locale");
}
function placeholdersFor(arb, key) {
const meta = arb[`@${key}`];
if (!meta || !meta.placeholders) return null;
return Object.keys(meta.placeholders);
}
function dartEscapeSingle(s) {
return s
.replace(/\\/g, "\\\\")
.replace(/\r\n/g, "\n")
.replace(/\n/g, "\\n")
.replace(/'/g, "\\'");
}
/** Build Dart string literal expr from ICU-ish template (e.g. "Hi, {name}!"). */
function dartReturnExpr(template, params) {
let d = template;
for (const p of params) {
d = d.split(`{${p}}`).join(`$${p}`);
}
return `'${dartEscapeSingle(d)}'`;
}
function writeLocalizations(en, te) {
const keys = [...new Set([...messageKeys(en), ...messageKeys(te)])].sort();
const abstractDecls = [];
const enImpl = [];
const teImpl = [];
for (const key of keys) {
const ph = placeholdersFor(en, key) ?? placeholdersFor(te, key);
if (ph && ph.length) {
const args = ph.map((p) => `String ${p}`).join(", ");
abstractDecls.push(` String ${key}(${args});`);
const tplEn = en[key];
const tplTe = te[key] ?? tplEn;
enImpl.push(` @override\n String ${key}(${args}) {\n return ${dartReturnExpr(tplEn, ph)};\n }`);
teImpl.push(` @override\n String ${key}(${args}) {\n return ${dartReturnExpr(tplTe, ph)};\n }`);
} else {
abstractDecls.push(` String get ${key};`);
const vEn = en[key] ?? "";
const vTe = te[key] ?? vEn;
enImpl.push(` @override\n String get ${key} => '${dartEscapeSingle(vEn)}';`);
teImpl.push(` @override\n String get ${key} => '${dartEscapeSingle(vTe)}';`);
}
}
const header = `// GENERATED FILE — do not edit by hand.
// Source: lib/l10n/app_en.arb + app_en_more.arb (merged). Regenerate: node tool/sync_l10n.mjs
`;
const main = `${header}import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'app_localizations_en.dart';
import 'app_localizations_te.dart';
abstract class AppLocalizations {
AppLocalizations(String locale) : localeName = locale;
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
static const List<Locale> supportedLocales = <Locale>[
Locale('en'),
Locale('te'),
];
${abstractDecls.join("\n\n")}
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'te'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'te':
return AppLocalizationsTe();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "\$locale".',
);
}
`;
const enFile = `${header}import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
${enImpl.join("\n\n")}
}
`;
const teFile = `${header}import 'app_localizations.dart';
class AppLocalizationsTe extends AppLocalizations {
AppLocalizationsTe([String locale = 'te']) : super(locale);
${teImpl.join("\n\n")}
}
`;
fs.writeFileSync(path.join(l10nDir, "app_localizations.dart"), main);
fs.writeFileSync(path.join(l10nDir, "app_localizations_en.dart"), enFile);
fs.writeFileSync(path.join(l10nDir, "app_localizations_te.dart"), teFile);
}
const baseEn = loadArb("app_en.arb");
const baseTe = loadArb("app_te.arb");
const moreEn = loadArb("app_en_more.arb");
const moreTe = loadArb("app_te_more.arb");
const en = merge(baseEn, moreEn);
const te = merge(baseTe, moreTe);
writeLocalizations(en, te);
console.log("Wrote app_localizations*.dart from merged ARBs.");

View File

@ -1,80 +1,147 @@
#!/bin/bash
#!/usr/bin/env bash
#
# LuckyChit deployment — backend (Node/PM2) + frontend (Flutter web + PM2).
#
# Usage:
# ./scripts/deploy.sh # backend + frontend
# ./scripts/deploy.sh backend # API only
# ./scripts/deploy.sh frontend # Flutter web only
# ./scripts/deploy.sh backend --force # e.g. wipe node_modules / flutter clean
#
# Env (optional):
# LUCKYCHIT_ROOT Repo root (default: /home/luckychit/apps/chitfund)
# DEPLOY_BRANCH Git branch to fast-forward to (default: prod_i8n)
# LuckyChit Unified Deployment Script
# Usage: ./deploy.sh [backend|frontend|--force]
set -eo pipefail
set -e
PROJECT_DIR="/home/luckychit/apps/chitfund"
PROJECT_DIR="${LUCKYCHIT_ROOT:-/home/luckychit/apps/chitfund}"
BRANCH="${DEPLOY_BRANCH:-prod_i8n}"
FORCE_REBUILD=false
TARGET=""
# Check for force flag
if [ "$1" == "--force" ] || [ "$2" == "--force" ]; then
usage() {
echo "Usage: $0 [backend|frontend] [--force]"
echo " (no args) deploy backend and frontend"
echo " backend deploy Node API only (pm2: luckychit-api)"
echo " frontend build Flutter web + restart static server (pm2: luckychit-web)"
echo " --force aggressive clean (backend: rm node_modules; frontend: flutter clean)"
exit 1
}
for arg in "$@"; do
case "$arg" in
--force)
FORCE_REBUILD=true
fi
cd $PROJECT_DIR
echo "🚀 LuckyChit Deployment"
echo "======================="
echo ""
# Git pull
echo "📥 Pulling latest code..."
git stash 2>/dev/null || true
git pull origin prodnew
echo ""
# Deploy backend
if [ "$1" == "" ] || [ "$1" == "backend" ] || [ "$1" == "--force" ]; then
echo "🔧 Deploying Backend..."
cd $PROJECT_DIR/backend
if [ "$FORCE_REBUILD" = true ]; then
echo "🗑️ Force rebuild: Removing node_modules..."
rm -rf node_modules package-lock.json
;;
backend|frontend)
if [ -n "$TARGET" ] && [ "$TARGET" != "$arg" ]; then
echo "Error: specify only one of backend or frontend."
usage
fi
TARGET="$arg"
;;
-h|--help)
usage
;;
*)
echo "Unknown argument: $arg"
usage
;;
esac
done
npm install
pm2 restart luckychit-api
echo "✅ Backend deployed"
run_backend() {
[ "$TARGET" = "" ] || [ "$TARGET" = "backend" ]
}
run_frontend() {
[ "$TARGET" = "" ] || [ "$TARGET" = "frontend" ]
}
cd "$PROJECT_DIR"
echo "LuckyChit deployment"
echo "===================="
echo "Root: $PROJECT_DIR"
echo "Branch: $BRANCH"
echo "Target: ${TARGET:-backend+frontend} force=$FORCE_REBUILD"
echo ""
if [ "$(id -u)" -eq 0 ]; then
echo "Warning: running as root. Prefer a normal user so Flutter/npm caches stay consistent."
echo ""
fi
# Deploy frontend
if [ "$1" == "" ] || [ "$1" == "frontend" ] || [ "$1" == "--force" ]; then
echo "🎨 Deploying Frontend..."
cd $PROJECT_DIR/luckychit
echo "Pulling latest code..."
git fetch origin "$BRANCH"
if ! git rev-parse --verify "refs/remotes/origin/$BRANCH" >/dev/null 2>&1; then
echo "Error: origin/$BRANCH not found. Fetch failed or branch missing on remote."
exit 1
fi
git checkout "$BRANCH"
git merge --ff-only "origin/$BRANCH"
echo ""
if run_backend; then
echo "Deploying backend..."
cd "$PROJECT_DIR/backend"
if [ "$FORCE_REBUILD" = true ]; then
echo "🗑️ Force rebuild: Cleaning Flutter cache..."
echo "Force rebuild: removing node_modules and lockfile..."
rm -rf node_modules package-lock.json
fi
if [ -f package-lock.json ]; then
npm ci
else
npm install
fi
if pm2 describe luckychit-api >/dev/null 2>&1; then
pm2 restart luckychit-api --update-env
else
pm2 start ecosystem.config.js --env production
fi
echo "Backend done."
echo ""
fi
if run_frontend; then
echo "Deploying frontend (Flutter web)..."
cd "$PROJECT_DIR/luckychit"
if [ "$FORCE_REBUILD" = true ]; then
echo "Force rebuild: flutter clean..."
flutter clean
rm -rf .dart_tool build
fi
flutter pub get
BUILD_NUMBER=$(date +%s)
flutter build web --release --pwa-strategy=none --build-number=$BUILD_NUMBER
echo "📦 Build version: $BUILD_NUMBER"
if [ -f tool/sync_l10n.mjs ]; then
node tool/sync_l10n.mjs
fi
pm2 restart luckychit-frontend
echo "✅ Frontend deployed"
BUILD_NUMBER=$(date +%s)
flutter build web --release --pwa-strategy=none --build-number="$BUILD_NUMBER"
echo "Build number: $BUILD_NUMBER"
# Name must match luckychit/ecosystem.config.js (app name: luckychit-web)
if pm2 describe luckychit-web >/dev/null 2>&1; then
pm2 restart luckychit-web --update-env
else
pm2 start ecosystem.config.js --env production
fi
echo "Frontend done."
echo ""
fi
# Status
echo "📊 PM2 Status:"
echo "PM2 status:"
pm2 status
echo ""
echo "✅ Deployment complete!"
echo ""
echo "🧪 Test:"
echo " Backend: curl http://localhost:3000/health"
echo " Frontend: curl http://localhost:8080"
echo " Domain: https://chitfund.deepteklabs.com"
echo ""
echo "📝 Check logs: pm2 logs --lines 20"
echo "💡 Clear browser cache: Ctrl + Shift + R"
echo "Deployment complete."
echo "Smoke tests:"
echo " curl -sf http://localhost:3000/health || curl -sf http://localhost:3000/api/health"
echo " curl -sfI http://localhost:8080/"
echo "Logs: pm2 logs --lines 30"