feat(receipt-import): add refresh categories endpoint and UI integration
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Schemalagd Uppdatering av Kategorier",
|
||||
"type": "shell",
|
||||
"command": "echo \"0 0 * * 1 cd /opt/containers/recipe-app && node -e \\\"require('./dist/src/receipt-import/receipt-import.service').loadCategories()\\\"\" | crontab -",
|
||||
"isBackground": false,
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { ReceiptImportService } from './receipt-import.service';
|
||||
import { ParsedReceiptItem } from './dto/parsed-receipt-item.dto';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
const ALLOWED_MIMES = [
|
||||
'image/jpeg',
|
||||
@@ -52,4 +53,11 @@ export class ReceiptImportController {
|
||||
const userId = typeof req?.user?.id === 'number' ? req.user.id : undefined;
|
||||
return this.receiptImportService.parseReceipt(file, isPremium, userId);
|
||||
}
|
||||
|
||||
@Post('refresh-categories')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async refreshCategories() {
|
||||
await this.receiptImportService.loadCategories();
|
||||
return { message: 'Kategorier har uppdaterats.' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,12 +102,21 @@ function inferPackageDebugFromRawName(rawName: string): {
|
||||
@Injectable()
|
||||
export class ReceiptImportService {
|
||||
private readonly logger = new Logger(ReceiptImportService.name);
|
||||
private cachedCategories: any[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly aiService: AiService,
|
||||
private readonly categoriesService: CategoriesService,
|
||||
) {}
|
||||
) {
|
||||
this.loadCategories();
|
||||
}
|
||||
|
||||
async loadCategories() {
|
||||
this.cachedCategories = await this.prisma.category.findMany({
|
||||
include: { children: true },
|
||||
});
|
||||
}
|
||||
|
||||
async parseReceipt(file: Express.Multer.File, _isPremium = false, userId?: number): Promise<ParsedReceiptItem[]> {
|
||||
// Steg 1: Delegera AI-parsning till microservice-importer
|
||||
@@ -149,12 +158,8 @@ export class ReceiptImportService {
|
||||
} catch {
|
||||
// ignorera parse-fel
|
||||
}
|
||||
this.logger.error(`Importer-api kvittoparsfel: ${message}`);
|
||||
if (response.status >= 400 && response.status < 500) {
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
throw new ServiceUnavailableException(message);
|
||||
}
|
||||
|
||||
return response.json() as Promise<ParsedReceiptItem[]>;
|
||||
}
|
||||
|
||||
@@ -360,12 +360,6 @@ INSERT INTO `Category` (`name`, `parentId`)
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Allergi mejeri'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
|
||||
-- ── NIVÅ 3: under Mejeri, ost & ägg > Mjölk (standard) ───────────────
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Standardmjölk', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Mjölk'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
|
||||
-- ── NIVÅ 3: under Mejeri, ost & ägg > Matlagning ──────────────────
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Gräddfil & creme fraiche', c2.id FROM `Category` c1
|
||||
@@ -491,10 +485,6 @@ INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Färdigmat enportioner', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Färdigmat'
|
||||
WHERE c1.name = 'Fryst' AND c1.parentId IS NULL;
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Pizza, paj & piroger', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Färdigmat'
|
||||
WHERE c1.name = 'Fryst' AND c1.parentId IS NULL;
|
||||
|
||||
-- ── NIVÅ 3: under Skafferi > Bakning ────────────────────────
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
|
||||
@@ -25,6 +25,10 @@ class CategoryApiPaths {
|
||||
static const tree = '/categories/tree';
|
||||
}
|
||||
|
||||
class ReceiptImportApiPaths {
|
||||
static const refreshCategories = '/receipt-import/refresh-categories';
|
||||
}
|
||||
|
||||
class RecipeApiPaths {
|
||||
static const list = '/recipes';
|
||||
static String detail(int id) => '/recipes/$id';
|
||||
|
||||
@@ -41,4 +41,12 @@ class ProfileRepository {
|
||||
);
|
||||
return UserProfile.fromJson(data);
|
||||
}
|
||||
|
||||
Future<void> refreshCategories() async {
|
||||
final token = await _ref.read(authStateProvider.future);
|
||||
await guardedApiCall(
|
||||
_ref,
|
||||
() => _apiClient.postJson(ReceiptImportApiPaths.refreshCategories, body: {}, token: token),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = true;
|
||||
bool _isSaving = false;
|
||||
bool _isRefreshingCategories = false;
|
||||
String? _error;
|
||||
UserProfile? _profile;
|
||||
_ProfileTab _activeTab = _ProfileTab.profile;
|
||||
@@ -105,6 +106,24 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
context.go('/login');
|
||||
}
|
||||
|
||||
Future<void> _refreshCategories() async {
|
||||
setState(() => _isRefreshingCategories = true);
|
||||
try {
|
||||
await ref.read(profileRepositoryProvider).refreshCategories();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Kategorier har uppdaterats.')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(mapErrorToUserMessage(e, context))),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isRefreshingCategories = false);
|
||||
}
|
||||
}
|
||||
|
||||
List<_ProfileTab> _visibleTabs(bool isAdmin) {
|
||||
return [
|
||||
_ProfileTab.profile,
|
||||
@@ -212,6 +231,23 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
: Text(context.l10n.profileSaveAction),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _isRefreshingCategories ? null : _refreshCategories,
|
||||
icon: _isRefreshingCategories
|
||||
? const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
label: const Text('Uppdatera kategorier'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user