feat(proj): init
This commit is contained in:
@@ -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 ?? 'Работа удалена');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user