feat(05-finances-fakturownia-import): complete Fakturownia mapping corrections

Phase 5 complete:
- add category mapping edit from operation edit for Fakturownia operations
- update current operation category immediately after mapping change
- support optional bulk update for matching imported operations
- close 05-05 and 05-06 PAUL summaries

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Codex
2026-05-04 22:57:55 +02:00
parent d0ab2a4f5f
commit 7acf22c71a
10 changed files with 672 additions and 27 deletions

View File

@@ -222,13 +222,21 @@ class FinancesController
$repo = self::repo();
$operationId = (int)\S::get( 'operation-id' );
$fakturowniaContext = null;
if ( $operationId > 0 )
$fakturowniaContext = $repo -> fakturowniaOperationContext( $operationId );
return \Tpl::view( 'finances/operation-edit', [
'operation' => $repo -> operationDetails( \S::get( 'operation-id' ) ),
'category_id' => \S::get( 'category-id' ),
'tags' => $repo -> tagsList( \S::get_session( 'finance-group' ) ),
'tags_json' => $repo -> tagsJson( \S::get_session( 'finance-group' ) ),
'operation_date' => \S::get_session( 'operation-date' ),
'clients' => $repo -> clientsList()
'clients' => $repo -> clientsList(),
'fakturownia_operation_context' => $fakturowniaContext,
'fakturownia_categories' => self::prepareCategoryOptions( $repo -> categoriesFlatList() )
] );
}
@@ -448,4 +456,53 @@ class FinancesController
header( 'Location: /finances/main_view/' );
exit;
}
public static function fakturowniaOperationMappingSave()
{
if ( !self::requireAuth() )
return false;
if ( !\S::csrf_verify() )
{
\S::alert( 'Nieprawidlowy token bezpieczenstwa. Odswiez strone i sproboj ponownie.' );
header( 'Location: /finances/main_view/' );
exit;
}
$operationId = (int)\S::get( 'operation_id' );
$financeCategoryId = (int)\S::get( 'finance_category_id' );
$applyToAll = (int)\S::get( 'apply_to_all' ) === 1;
$returnCategoryId = (int)\S::get( 'return_category_id' );
$repo = self::repo();
if ( $operationId <= 0 || $financeCategoryId <= 0 || !$repo -> categoryExists( $financeCategoryId ) )
{
\S::alert( 'Nie udalo sie zapisac dopasowania. Sprawdz dane formularza.' );
header( 'Location: /finances/main_view/' );
exit;
}
$context = $repo -> fakturowniaOperationContext( $operationId );
if ( !$context )
{
\S::alert( 'Ta operacja nie jest powiazana z mapowaniem Fakturownia.' );
header( 'Location: /finances/operations_list/category-id=' . $returnCategoryId );
exit;
}
$importRepo = self::importRepo();
$importRepo -> ensureTables();
$importRepo -> saveItemMapping( $context['external_item_key'], $context['external_name'], $financeCategoryId );
$repo -> updateOperationCategory( $operationId, $financeCategoryId );
$updatedCount = $repo -> updateFakturowniaOperationsCategoryByItemName( $context['item_name'], $financeCategoryId, $applyToAll );
if ( $applyToAll )
\S::alert( 'Dopasowanie zmienione. Zaktualizowano operacje: ' . (int)$updatedCount . '.' );
else
\S::alert( 'Dopasowanie zmienione dla tej operacji.' );
header( 'Location: /finances/operations_list/category-id=' . $financeCategoryId );
exit;
}
}

View File

@@ -155,6 +155,9 @@ class FinanceRepository
public function operationDetails( $operation_id )
{
$operation = $this -> mdb -> get( 'finance_operations', '*', [ 'id' => $operation_id ] );
if ( !$operation )
return [];
$operation['tags'] = $this -> mdb -> query(
'SELECT tag, tag_id FROM finance_operation_tags AS fot '
. 'INNER JOIN finance_tags AS ft ON fot.tag_id = ft.id '
@@ -164,6 +167,103 @@ class FinanceRepository
return $operation;
}
public function fakturowniaOperationContext( $operationId )
{
$operationId = (int)$operationId;
if ( $operationId <= 0 )
return null;
$operation = $this -> mdb -> get( 'finance_operations', '*', [ 'id' => $operationId ] );
if ( !$operation )
return null;
$imported = $this -> mdb -> query(
'SELECT id FROM fakturownia_imported_documents '
. 'WHERE FIND_IN_SET( :operation_id, finance_operation_ids ) LIMIT 1',
[ ':operation_id' => (string)$operationId ]
) -> fetch( \PDO::FETCH_ASSOC );
if ( !$imported )
return null;
$itemName = $this -> extractItemNameFromDescription( (string)( $operation['description'] ?? '' ) );
if ( $itemName === '' )
return null;
$externalItemKey = $this -> buildNameItemKey( $itemName );
$mapping = $this -> mdb -> get( 'fakturownia_item_mappings', '*', [
'external_item_key' => $externalItemKey
] );
if ( !$mapping )
$mapping = $this -> findItemMappingByExternalName( $itemName );
if ( !$mapping )
return null;
return [
'operation_id' => $operationId,
'item_name' => $itemName,
'external_item_key' => (string)$mapping['external_item_key'],
'external_name' => (string)$mapping['external_name'],
'finance_category_id' => (int)$mapping['finance_category_id']
];
}
public function updateOperationCategory( $operationId, $categoryId )
{
$this -> mdb -> update( 'finance_operations', [
'category_id' => (int)$categoryId
], [
'id' => (int)$operationId
] );
}
public function updateFakturowniaOperationsCategoryByItemName( $itemName, $categoryId, $includeAll )
{
$itemName = trim( (string)$itemName );
if ( $itemName === '' )
return 0;
if ( !$includeAll )
return 0;
$pattern = '%| ' . $itemName . ' |%';
$stmt = $this -> mdb -> query(
'UPDATE finance_operations AS fo '
. 'INNER JOIN fakturownia_imported_documents AS fid ON FIND_IN_SET( fo.id, fid.finance_operation_ids ) '
. 'INNER JOIN finance_categories AS fc_old ON fc_old.id = fo.category_id '
. 'INNER JOIN finance_categories AS fc_new ON fc_new.id = :category_id '
. 'SET fo.category_id = :category_id '
. 'WHERE fo.description LIKE :pattern AND fc_old.group_id = fc_new.group_id',
[ ':category_id' => (int)$categoryId, ':pattern' => $pattern ]
);
if ( method_exists( $stmt, 'rowCount' ) )
return (int)$stmt -> rowCount();
return 0;
}
public function operationGroupId( $operationId )
{
$row = $this -> mdb -> query(
'SELECT fc.group_id '
. 'FROM finance_operations AS fo '
. 'INNER JOIN finance_categories AS fc ON fc.id = fo.category_id '
. 'WHERE fo.id = :operation_id LIMIT 1',
[ ':operation_id' => (int)$operationId ]
) -> fetch( \PDO::FETCH_ASSOC );
return isset( $row['group_id'] ) ? (int)$row['group_id'] : 0;
}
public function categoryGroupId( $categoryId )
{
return (int)$this -> mdb -> get( 'finance_categories', 'group_id', [ 'id' => (int)$categoryId ] );
}
public function saveOperation( $operation_id, $category_id, $date, $amount, $description, $tags, $group_id, $client_id = null )
{
$data = [
@@ -434,4 +534,44 @@ class FinanceRepository
) -> fetchAll();
return $results[0][0];
}
private function extractItemNameFromDescription( $description )
{
$description = trim( (string)$description );
if ( $description === '' || strpos( $description, 'Fakturownia |' ) !== 0 )
return '';
$parts = explode( '|', $description );
if ( count( $parts ) < 3 )
return '';
return trim( (string)$parts[2] );
}
private function buildNameItemKey( $name )
{
$normalized = trim( (string)$name );
if ( function_exists( 'mb_strtolower' ) )
$normalized = mb_strtolower( $normalized, 'UTF-8' );
else
$normalized = strtolower( $normalized );
return 'name:' . $normalized;
}
private function findItemMappingByExternalName( $itemName )
{
$itemName = trim( (string)$itemName );
if ( $itemName === '' )
return null;
$row = $this -> mdb -> query(
'SELECT * FROM fakturownia_item_mappings '
. 'WHERE LOWER( external_name ) = LOWER( :external_name ) '
. 'ORDER BY id DESC LIMIT 1',
[ ':external_name' => $itemName ]
) -> fetch( \PDO::FETCH_ASSOC );
return $row ? $row : null;
}
}