Compare commits
10 Commits
fa0e43885a
...
9a40854884
| Author | SHA1 | Date |
|---|---|---|
|
|
9a40854884 | |
|
|
c4439e76a8 | |
|
|
e404721818 | |
|
|
45a12ea5bc | |
|
|
b7b2aaf8a6 | |
|
|
6e19f57e27 | |
|
|
51e9c72464 | |
|
|
bf37c3c1ed | |
|
|
644b2cd367 | |
|
|
2ee17c2edc |
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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,41 +664,120 @@ const updateMonthlyDraw = async (req, res) => {
|
|||
|
||||
const updates = {};
|
||||
|
||||
// Update winner if provided
|
||||
if (winner_id) {
|
||||
// Verify the winner is a member of the group
|
||||
const member = await GroupMember.findOne({
|
||||
where: {
|
||||
group_id: monthlyDraw.ChitGroup.id,
|
||||
user_id: winner_id,
|
||||
status: 'active'
|
||||
},
|
||||
include: [{ model: User, attributes: ['id', 'full_name'] }]
|
||||
});
|
||||
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 (!member) {
|
||||
if (monthProvided || yearProvided) {
|
||||
if (Number.isNaN(nextMonth) || nextMonth < 1 || nextMonth > 12) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Winner must be an active member of this group'
|
||||
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;
|
||||
}
|
||||
|
||||
updates.winner_id = winner_id;
|
||||
if (!notes) {
|
||||
updates.notes = `Winner updated to: ${member.User.full_name}`;
|
||||
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) {
|
||||
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,
|
||||
user_id: winner_id,
|
||||
status: 'active'
|
||||
},
|
||||
include: [{ model: User, attributes: ['id', 'full_name'] }]
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Winner must be an active member of this group'
|
||||
});
|
||||
}
|
||||
|
||||
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 === 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -83,10 +135,20 @@ class _EditDrawDialogState extends State<EditDrawDialog> {
|
|||
);
|
||||
Get.back(result: true);
|
||||
} else {
|
||||
SnackbarUtil.showError(
|
||||
response['message'] ?? 'Failed to update draw',
|
||||
title: 'Error',
|
||||
);
|
||||
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()}');
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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": "సభ్య సమాచారం కనుగొనబడలేదు"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
@ -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
|
||||
FORCE_REBUILD=true
|
||||
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
|
||||
;;
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
cd $PROJECT_DIR
|
||||
|
||||
echo "🚀 LuckyChit Deployment"
|
||||
echo "======================="
|
||||
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 ""
|
||||
|
||||
# Git pull
|
||||
echo "📥 Pulling latest code..."
|
||||
git stash 2>/dev/null || true
|
||||
git pull origin prodnew
|
||||
echo ""
|
||||
if run_backend; then
|
||||
echo "Deploying backend..."
|
||||
cd "$PROJECT_DIR/backend"
|
||||
|
||||
# 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
|
||||
fi
|
||||
if [ "$FORCE_REBUILD" = true ]; then
|
||||
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
|
||||
pm2 restart luckychit-api
|
||||
echo "✅ Backend deployed"
|
||||
echo ""
|
||||
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
|
||||
|
||||
# Deploy frontend
|
||||
if [ "$1" == "" ] || [ "$1" == "frontend" ] || [ "$1" == "--force" ]; then
|
||||
echo "🎨 Deploying Frontend..."
|
||||
cd $PROJECT_DIR/luckychit
|
||||
if run_frontend; then
|
||||
echo "Deploying frontend (Flutter web)..."
|
||||
cd "$PROJECT_DIR/luckychit"
|
||||
|
||||
if [ "$FORCE_REBUILD" = true ]; then
|
||||
echo "🗑️ Force rebuild: Cleaning Flutter cache..."
|
||||
flutter clean
|
||||
rm -rf .dart_tool build
|
||||
fi
|
||||
if [ "$FORCE_REBUILD" = true ]; then
|
||||
echo "Force rebuild: flutter clean..."
|
||||
flutter clean
|
||||
rm -rf .dart_tool build
|
||||
fi
|
||||
|
||||
flutter pub get
|
||||
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"
|
||||
echo ""
|
||||
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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue