feat(proj): init

This commit is contained in:
vl.arkhangelskii
2026-09-21 04:06:43 +03:00
commit c956b94983
1076 changed files with 50876 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/core/format/formatters.dart';
import 'package:please_pay_me/data/models/job.dart';
import 'package:please_pay_me/features/work/job_form_sheet.dart';
import 'package:please_pay_me/features/work/jobs_controller.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:provider/provider.dart';
/// Jobs and their payday schedule.
class WorkScreen extends StatelessWidget {
const WorkScreen({super.key});
@override
Widget build(BuildContext context) {
final controller = context.watch<JobsController>();
return CupertinoPageScaffold(
backgroundColor: AppColors.of(context, AppColors.groupedBackground),
child: CustomScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: [
AppLargeNavBar(
title: 'Работа',
trailing: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () => showJobFormSheet(context: context, controller: controller),
child: const AppIcon(CupertinoIcons.add_circled, color: AppColors.accent),
),
),
CupertinoSliverRefreshControl(onRefresh: () => controller.load(silent: true)),
SliverToBoxAdapter(
child: controller.state.map(
loading: () => AppListSection(
children: List.generate(2, (_) => const AppSkeletonRow()),
),
error: (message) => AppErrorView(message: message, onRetry: controller.load),
data: (jobs) => jobs.isEmpty
? AppEmptyState(
icon: CupertinoIcons.briefcase,
title: 'Работа не добавлена',
message: 'Укажите оклад и дни выплат — приложение подскажет даты зарплаты.',
actionLabel: 'Добавить работу',
onAction: () =>
showJobFormSheet(context: context, controller: controller),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (final job in jobs) ...[
_JobCard(job: job, controller: controller),
const SizedBox(height: AppSpacing.s5),
],
],
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: AppSpacing.s7)),
],
),
);
}
}
class _JobCard extends StatelessWidget {
const _JobCard({required this.job, required this.controller});
final Job job;
final JobsController controller;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AppListSection(
header: job.name,
separatorIndent: 60,
children: [
AppListTile(
leading: const AppIconBadge(
icon: CupertinoIcons.briefcase_fill,
color: AppColors.accent,
),
title: 'Оклад',
value: formatMoney(job.salaryAmount, currency: job.currency),
showChevron: false,
),
AppListTile(
leading: const AppIconBadge(
icon: CupertinoIcons.calendar,
color: AppColors.systemOrange,
),
title: 'Дни выплат',
value: job.payDays.map((day) => '$day').join(' и '),
showChevron: false,
),
AppListTile(
leading: const AppIconBadge(
icon: CupertinoIcons.arrow_left_right,
color: AppColors.systemGray,
),
title: 'Выходные',
value: job.weekendPolicy.label,
showChevron: false,
),
AppListTile(
title: 'Настроить',
onTap: () => _openActions(context),
),
],
),
if (job.nextPays.isNotEmpty) ...[
const SizedBox(height: AppSpacing.s4),
AppListSection(
header: 'Ближайшие выплаты',
children: [
for (final pay in job.nextPays)
AppListTile(
title: formatDay(pay.date),
subtitle: '${pay.percent.round()}% оклада · ${pay.scheduledDay} числа',
value: formatMoney(pay.amount, currency: job.currency),
showChevron: false,
),
],
),
],
],
);
}
Future<void> _openActions(BuildContext context) async {
final index = await showAppActionSheet(
context: context,
title: job.name,
actions: const [
AppActionSheetAction(label: 'Редактировать', isDefault: true),
AppActionSheetAction(label: 'Удалить', destructive: true),
],
);
if (index == null || !context.mounted) return;
if (index == 0) {
await showJobFormSheet(context: context, controller: controller, initial: job);
return;
}
final confirmed = await showAppAlert(
context: context,
title: 'Удалить «${job.name}»?',
message: 'График выплат тоже будет удалён.',
confirmLabel: 'Удалить',
cancelLabel: 'Отмена',
destructive: true,
);
if (confirmed != true) return;
final error = await controller.delete(job.id);
if (context.mounted) {
await showAppToast(context, message: error ?? 'Работа удалена');
}
}
}